You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Swift中通过JavascriptCore将Node.js Stream写入本地文件?

Using a Node.js Stream NPM Package in Swift via JavaScriptCore to Write Local Files

Great question! Yes, you can absolutely make this work—but there’s a critical detail to call out upfront: JavaScriptCore (JSContext) doesn’t include Node.js’s native APIs (like fs or the core stream module). That means your original Node.js code won’t run directly, but we can work around this by adapting the NPM package and bridging stream data from JS to Swift for file writing.

Here’s a complete, actionable breakdown:

Key Background

JSContext behaves like a browser’s JavaScript environment, not a Node.js runtime. So we need to:

  1. Package the NPM bundle to remove Node.js dependencies.
  2. Bridge stream data chunks from the JS context to Swift, where we’ll handle the actual file I/O.

Step 1: Prepare the NPM Package for JSContext

First, you need to bundle your NPM package into a standalone JS file that works without Node.js:

  • Use Browserify or Webpack to bundle the package. For example, with Browserify:
    browserify your-package-entry-file.js --standalone myStream -o bundle.js
    
    The --standalone flag ensures the package’s exported function (myStream in your case) is available as a global variable in the JS context.
  • If your package relies on Node’s stream module, replace it with a browser-compatible alternative like readable-stream during bundling—most bundlers can handle this automatically with aliases.

Step 2: Build a Swift File Writer for Stream Chunks

We’ll create a Swift class to handle file writing, which we’ll inject into the JS context to receive stream data. This class will handle writing chunks, finishing the file, and error reporting:

import JavaScriptCore
import Foundation

class StreamFileWriter: NSObject {
    private let fileHandle: FileHandle
    private let completion: (Error?) -> Void
    
    init(fileURL: URL, completion: @escaping (Error?) -> Void) throws {
        self.fileHandle = try FileHandle(forWritingTo: fileURL)
        self.completion = completion
        super.init()
    }
    
    // Called from JS to write a single data chunk
    @objc func writeChunk(_ base64String: String) {
        guard let data = Data(base64Encoded: base64String) else {
            completion(NSError(domain: "StreamWriter", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid chunk data"]))
            return
        }
        fileHandle.write(data)
    }
    
    // Called from JS when the stream ends
    @objc func finish() {
        fileHandle.closeFile()
        completion(nil)
    }
    
    // Called from JS if the stream emits an error
    @objc func handleError(_ errorMessage: String) {
        fileHandle.closeFile()
        completion(NSError(domain: "StreamWriter", code: -2, userInfo: [NSLocalizedDescriptionKey: errorMessage]))
    }
}

Step 3: Wire Up JSContext and the Stream

Now, we’ll set up the JS context, inject our file writer, load the bundled NPM package, and connect the stream’s events to our Swift writer:

do {
    // 1. Set up the output file URL
    let documentsDir = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
    let outputFileURL = documentsDir.appendingPathComponent("test.txt")
    
    // Clean up existing file if needed
    if FileManager.default.fileExists(atPath: outputFileURL.path) {
        try FileManager.default.removeItem(at: outputFileURL)
    }
    FileManager.default.createFile(atPath: outputFileURL.path, contents: nil)
    
    // 2. Initialize the file writer
    let writer = try StreamFileWriter(fileURL: outputFileURL) { error in
        if let error = error {
            print("File write failed: \(error.localizedDescription)")
        } else {
            print("Success! File written to: \(outputFileURL.path)")
        }
    }
    
    // 3. Configure the JS context
    let context = JSContext()!
    // Inject our Swift writer into the JS environment as a global object
    context.setObject(writer, forKeyedSubscript: "SwiftFileWriter" as NSString)
    
    // 4. Load the bundled NPM package
    guard let bundlePath = Bundle.main.path(forResource: "bundle", ofType: "js") else {
        fatalError("bundle.js not found in app bundle")
    }
    let jsBundleSource = try String(contentsOfFile: bundlePath)
    context.evaluateScript(jsBundleSource)
    
    // 5. Pass your `myDetails` data to JS (adjust this to match your actual data structure)
    let myDetails = ["id": "123", "options": ["format": "text"]]
    context.setObject(myDetails, forKeyedSubscript: "myDetails" as NSString)
    
    // 6. Run JS code to handle the stream and send chunks to Swift
    let streamHandlerScript = """
    // Initialize the stream from your NPM package
    const stream = myStream(myDetails);
    
    // Listen for data chunks
    stream.on('data', (chunk) => {
        // Convert Buffer to base64 for safe transfer to Swift
        const base64Chunk = chunk.toString('base64');
        SwiftFileWriter.writeChunk(base64Chunk);
    });
    
    // Listen for stream completion
    stream.on('end', () => {
        SwiftFileWriter.finish();
    });
    
    // Handle stream errors
    stream.on('error', (err) => {
        SwiftFileWriter.handleError(err.message || 'Unknown stream error');
    });
    """
    context.evaluateScript(streamHandlerScript)
    
} catch {
    print("Initialization error: \(error.localizedDescription)")
}

Critical Notes

  • Data Transfer: We use Base64 encoding to pass binary data between JS and Swift because direct binary transfer via JSContext can be buggy. It adds a small performance overhead, but it’s reliable.
  • Error Handling: Always listen for the stream’s error event—unhandled JS errors will crash the JSContext, and Swift won’t catch them otherwise.
  • Memory Management: Ensure the StreamFileWriter instance is strongly referenced (don’t let it go out of scope while the stream is running) to avoid premature deallocation.
  • Bundle Testing: Double-check that your bundled bundle.js works in a browser first (e.g., by running it in a simple HTML file) to confirm it doesn’t rely on Node.js APIs.

内容的提问来源于stack exchange,提问作者Charlie Fish

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 03:48:06