如何在 Swift 的 JavaScriptCore 中导入模块?
How to import modules in Swift's JavaScriptCore?
我正在尝试使用 Swift 的 JavaScript 核心框架来利用现有的 JavaScript 库,该库使用 ES6 模块。具体来说,Stephen C Phillips 的 morse-pro。我已将文件添加到 Xcode 游乐场,然后使用此代码加载库并 运行 它在 JavaScript 上下文中:
import JavaScriptCore
var jsContext = JSContext()
// set up exception handler for javascript errors
jsContext?.exceptionHandler = { context, exception in
if let exc = exception {
print("JS Exception:", exc.toString())
}
}
// read the javascript files in and evaluate
if let jsSourcePath = Bundle.main.path(forResource: "morse-pro-master/src/morse-pro-message", ofType: "js") {
do {
let jsSourceContents = try String(contentsOfFile: jsSourcePath)
jsContext?.evaluateScript(jsSourceContents)
} catch {
print(error.localizedDescription)
}
}
这种方法适用于简单的 "Hello world" 类测试,但它会因 JavaScript 错误在 morse-pro 库中阻塞:
SyntaxError: Unexpected token '*'. import call expects
exactly one argument.
错误似乎是由 morse-pro-message.js:
中的这一行引起的
import * as Morse from './morse-pro';
我认为它正在尝试将所有 morse-pro 文件作为一个模块导入。
我不熟悉 ES6 模块,但该库似乎在正常 JavaScript 上下文中为其他人工作。我在 Swift 中加载库的方式有问题吗?还是模块是 JavaScriptCore 不支持的功能? (documentation 只是说它支持 "JavaScript",并没有更具体。)
如果有任何建议可以指导我在 JavaScriptCore VM 中获取此库 运行,我将不胜感激。
我在访问我自己的 JS 代码时也遇到了同样的问题,它使用了 3 个外部库(Lodash、Moment 和 Moment-range)。
最初我尝试在我的应用程序包中导入所有 4 个 .js 文件(1-我自己的文件和 3-库)。但是当我这样尝试时,它没有用。 我在将这些库导入到我自己的 JS 代码中时遇到了问题。
解决方法:
最后,我将这些库代码中的所有代码复制并粘贴到我自己的代码中,并在我的应用程序中只导入了一个 .js 文件(我自己的文件)。所以在这种情况下不需要从任何库中导入任何东西。所以我的代码成功运行了。
NOTE: I am not sure whether my workaround is good approach or not. I just thought to register my issue and workaround which helped me.
我的代码:
lazy var context: JSContext? = {
let context = JSContext()
guard let timeSlotJS = Bundle.main.path(forResource: "app", ofType: "js") else {
print("Unable to read resource file")
return nil
}
do {
let timeSlotContent = try String(contentsOfFile: timeSlotJS, encoding: String.Encoding.utf8)
_ = context?.evaluateScript(timeSlotContent)
} catch {
print("Error on extracting js content")
}
let _ = context?.evaluateScript("var console = { log: function(message) { _consoleLog(message) } }")
// Print log messages
let consoleLog: @convention(block) (String) -> Void = { message in
print("Javascript log: " + message)
}
context?.setObject(unsafeBitCast(consoleLog, to: AnyObject.self), forKeyedSubscript: "_consoleLog" as NSCopying & NSObjectProtocol)
// Print exception messages
context!.exceptionHandler = { context, exception in
print("JS Error: \(exception!)")
}
return context
}()
func accessingJSCode() {
let timeSlotScript = "DigitalVaultTimeSlotAvailabilityFuntion({\"services\": \(services)}, {\"response\": {\"result\": {\"CustomModule1\": {\"row\":\(rowValue)}}, \"uri\": \"/crm/private/json/CustomModule1/searchRecords\"}});"
print(timeSlotScript)
let timeSlots = context!.evaluateScript(timeSlotScript)
// Then here i parsed my output JSON from script.
}
谢谢!!
在黑暗中摸索了很多次之后,我找到了一种方法,使库可供 Swift 使用,而无需手动更改它。
首先,正如@estus 所建议的,我使用 NPM 安装了库,它将它转换为 ES5 但不解析依赖项。所以它仍然是一堆单独的文件,它们通过浏览器和 JavaScriptCore 都无法理解的 require
和 export
关键字相互调用。
然后我使用 Browserify 将所有依赖项捆绑到一个文件中,以便 JavaScriptCore 可以理解它。 Browserify 的正常运行隐藏了所有代码,因此我使用“--standalone”标志告诉它使标记的函数可用。如果直接导出 ES5 文件,它会创建一个通用对象并将导出的函数放在 .default
下。我希望它们更容易访问,所以我创建了一个新文件来列出导出,然后 运行 Browserify 就可以了。例如,一个名为 "morse-export.js" 的文件包含:
module.exports.MorseMessage = require('./lib/morse-pro-message.js').default;
然后我 运行 Browserify 像这样:
browserify ./morse-export.js --standalone Morse > ./morse-bundle.js
并使用 Bundle.main.path(forResource)
在我的 Swift 代码中包含 morse-bundle.js 文件。现在我可以使用 Morse.MorseMessage
访问 MorseMessage class,所以回到 Swift:
jsContext?.evaluateScript("var morseMessage = new Morse.MorseMessage()")
print(jsContext!.evaluateScript("morseMessage.translate('abc')"))
打印“.- -... -.-”如您所料。
这样做的缺点是您必须手动添加要导出到导出文件的任何 classes 和函数。不过,这似乎是最简单的方法。如果有更好的方法,我很想听听!
虽然无法对模块使用 import
。
您可以支持 require('path/filename');
语法。
这是通过将 require 作为函数提供给 JS 来完成的。 import
命令(不幸的是)太奇特,无法在 JSContext
.
的给定限制中实施
查看以下实现
Objective-C.
@interface MyContext ()
@property JSContext *context;
@implementation MyContext
- (void) setupRequire {
MyContext * __weak weakSelf = self;
self.context[@"require"] = ^(NSString *path) {
path = [weakSelf resolvePath:path];
if(![[NSFileManager defaultManager] fileExistsAtPath:path]) {
NSString *message = [NSString stringWithFormat:@"Require: File “%@” does not exist.", path];
weakSelf.context.exception = [JSValue valueWithNewErrorFromMessage:message inContext:weakSelf.context];
return;
}
[weakSelf loadScript:path];
};
}
- (NSString *) resolvePath:(NSString *)path {
path = path.stringByResolvingSymlinksInPath;
return path.stringByStandardizingPath;
}
- (void) loadScript:(NSString *)path {
NSError *error;
NSString *script = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
if (error) {
script = @"";
NSLog(@"Error: Could not read file in path “%@” to string. (%@)", path, error);
// Return void or throw an error here.
return
}
[self.context evaluateScript:script];
}
此示例基于 Phoenix https://github.com/kasper/phoenix
中的代码
https://github.com/kasper/phoenix/blob/master/Phoenix/PHContext.m#L195-L206
Swift 4
此示例基于 CutBox 中的代码 https://github.com/cutbox/CutBox
import JavascriptCore
class JSService {
let context = JSContext()
let shared = JSService()
let require: @convention(block) (String) -> (JSValue?) = { path in
let expandedPath = NSString(string: path).expandingTildeInPath
// Return void or throw an error here.
guard FileManager.default.fileExists(atPath: expandedPath)
else { debugPrint("Require: filename \(expandedPath) does not exist")
return nil }
guard let fileContent = try? String(contentsOfFile: expandedFilename)
else { return nil }
return JSService.shared.context.evaluateScript(fileContent)
}
init() {
self.context.setObject(self.require,
forKeyedSubscript: "require" as NSString)
}
func repl(_ string: String) -> String {
return self.context.evaluateScript(string).toString()
}
}
我正在尝试使用 Swift 的 JavaScript 核心框架来利用现有的 JavaScript 库,该库使用 ES6 模块。具体来说,Stephen C Phillips 的 morse-pro。我已将文件添加到 Xcode 游乐场,然后使用此代码加载库并 运行 它在 JavaScript 上下文中:
import JavaScriptCore
var jsContext = JSContext()
// set up exception handler for javascript errors
jsContext?.exceptionHandler = { context, exception in
if let exc = exception {
print("JS Exception:", exc.toString())
}
}
// read the javascript files in and evaluate
if let jsSourcePath = Bundle.main.path(forResource: "morse-pro-master/src/morse-pro-message", ofType: "js") {
do {
let jsSourceContents = try String(contentsOfFile: jsSourcePath)
jsContext?.evaluateScript(jsSourceContents)
} catch {
print(error.localizedDescription)
}
}
这种方法适用于简单的 "Hello world" 类测试,但它会因 JavaScript 错误在 morse-pro 库中阻塞:
SyntaxError: Unexpected token '*'. import call expects exactly one argument.
错误似乎是由 morse-pro-message.js:
中的这一行引起的import * as Morse from './morse-pro';
我认为它正在尝试将所有 morse-pro 文件作为一个模块导入。
我不熟悉 ES6 模块,但该库似乎在正常 JavaScript 上下文中为其他人工作。我在 Swift 中加载库的方式有问题吗?还是模块是 JavaScriptCore 不支持的功能? (documentation 只是说它支持 "JavaScript",并没有更具体。)
如果有任何建议可以指导我在 JavaScriptCore VM 中获取此库 运行,我将不胜感激。
我在访问我自己的 JS 代码时也遇到了同样的问题,它使用了 3 个外部库(Lodash、Moment 和 Moment-range)。
最初我尝试在我的应用程序包中导入所有 4 个 .js 文件(1-我自己的文件和 3-库)。但是当我这样尝试时,它没有用。 我在将这些库导入到我自己的 JS 代码中时遇到了问题。
解决方法:
最后,我将这些库代码中的所有代码复制并粘贴到我自己的代码中,并在我的应用程序中只导入了一个 .js 文件(我自己的文件)。所以在这种情况下不需要从任何库中导入任何东西。所以我的代码成功运行了。
NOTE: I am not sure whether my workaround is good approach or not. I just thought to register my issue and workaround which helped me.
我的代码:
lazy var context: JSContext? = {
let context = JSContext()
guard let timeSlotJS = Bundle.main.path(forResource: "app", ofType: "js") else {
print("Unable to read resource file")
return nil
}
do {
let timeSlotContent = try String(contentsOfFile: timeSlotJS, encoding: String.Encoding.utf8)
_ = context?.evaluateScript(timeSlotContent)
} catch {
print("Error on extracting js content")
}
let _ = context?.evaluateScript("var console = { log: function(message) { _consoleLog(message) } }")
// Print log messages
let consoleLog: @convention(block) (String) -> Void = { message in
print("Javascript log: " + message)
}
context?.setObject(unsafeBitCast(consoleLog, to: AnyObject.self), forKeyedSubscript: "_consoleLog" as NSCopying & NSObjectProtocol)
// Print exception messages
context!.exceptionHandler = { context, exception in
print("JS Error: \(exception!)")
}
return context
}()
func accessingJSCode() {
let timeSlotScript = "DigitalVaultTimeSlotAvailabilityFuntion({\"services\": \(services)}, {\"response\": {\"result\": {\"CustomModule1\": {\"row\":\(rowValue)}}, \"uri\": \"/crm/private/json/CustomModule1/searchRecords\"}});"
print(timeSlotScript)
let timeSlots = context!.evaluateScript(timeSlotScript)
// Then here i parsed my output JSON from script.
}
谢谢!!
在黑暗中摸索了很多次之后,我找到了一种方法,使库可供 Swift 使用,而无需手动更改它。
首先,正如@estus 所建议的,我使用 NPM 安装了库,它将它转换为 ES5 但不解析依赖项。所以它仍然是一堆单独的文件,它们通过浏览器和 JavaScriptCore 都无法理解的 require
和 export
关键字相互调用。
然后我使用 Browserify 将所有依赖项捆绑到一个文件中,以便 JavaScriptCore 可以理解它。 Browserify 的正常运行隐藏了所有代码,因此我使用“--standalone”标志告诉它使标记的函数可用。如果直接导出 ES5 文件,它会创建一个通用对象并将导出的函数放在 .default
下。我希望它们更容易访问,所以我创建了一个新文件来列出导出,然后 运行 Browserify 就可以了。例如,一个名为 "morse-export.js" 的文件包含:
module.exports.MorseMessage = require('./lib/morse-pro-message.js').default;
然后我 运行 Browserify 像这样:
browserify ./morse-export.js --standalone Morse > ./morse-bundle.js
并使用 Bundle.main.path(forResource)
在我的 Swift 代码中包含 morse-bundle.js 文件。现在我可以使用 Morse.MorseMessage
访问 MorseMessage class,所以回到 Swift:
jsContext?.evaluateScript("var morseMessage = new Morse.MorseMessage()")
print(jsContext!.evaluateScript("morseMessage.translate('abc')"))
打印“.- -... -.-”如您所料。
这样做的缺点是您必须手动添加要导出到导出文件的任何 classes 和函数。不过,这似乎是最简单的方法。如果有更好的方法,我很想听听!
虽然无法对模块使用 import
。
您可以支持 require('path/filename');
语法。
这是通过将 require 作为函数提供给 JS 来完成的。 import
命令(不幸的是)太奇特,无法在 JSContext
.
查看以下实现
Objective-C.
@interface MyContext ()
@property JSContext *context;
@implementation MyContext
- (void) setupRequire {
MyContext * __weak weakSelf = self;
self.context[@"require"] = ^(NSString *path) {
path = [weakSelf resolvePath:path];
if(![[NSFileManager defaultManager] fileExistsAtPath:path]) {
NSString *message = [NSString stringWithFormat:@"Require: File “%@” does not exist.", path];
weakSelf.context.exception = [JSValue valueWithNewErrorFromMessage:message inContext:weakSelf.context];
return;
}
[weakSelf loadScript:path];
};
}
- (NSString *) resolvePath:(NSString *)path {
path = path.stringByResolvingSymlinksInPath;
return path.stringByStandardizingPath;
}
- (void) loadScript:(NSString *)path {
NSError *error;
NSString *script = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
if (error) {
script = @"";
NSLog(@"Error: Could not read file in path “%@” to string. (%@)", path, error);
// Return void or throw an error here.
return
}
[self.context evaluateScript:script];
}
此示例基于 Phoenix https://github.com/kasper/phoenix
中的代码https://github.com/kasper/phoenix/blob/master/Phoenix/PHContext.m#L195-L206
Swift 4
此示例基于 CutBox 中的代码 https://github.com/cutbox/CutBox
import JavascriptCore
class JSService {
let context = JSContext()
let shared = JSService()
let require: @convention(block) (String) -> (JSValue?) = { path in
let expandedPath = NSString(string: path).expandingTildeInPath
// Return void or throw an error here.
guard FileManager.default.fileExists(atPath: expandedPath)
else { debugPrint("Require: filename \(expandedPath) does not exist")
return nil }
guard let fileContent = try? String(contentsOfFile: expandedFilename)
else { return nil }
return JSService.shared.context.evaluateScript(fileContent)
}
init() {
self.context.setObject(self.require,
forKeyedSubscript: "require" as NSString)
}
func repl(_ string: String) -> String {
return self.context.evaluateScript(string).toString()
}
}