如何在 Swift 的 JavaScriptCore 中导入模块?

Posted

技术标签:

【中文标题】如何在 Swift 的 JavaScriptCore 中导入模块?【英文标题】:How to import modules in Swift's JavaScriptCore? 【发布时间】:2018-01-20 10:04:33 【问题描述】:

我正在尝试使用 Swift 的 javascriptCore 框架来利用现有的使用 ES6 模块的 JavaScript 库。具体来说,morse-pro by Stephen C Phillips。我已将文件添加到 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 库:

语法错误:意外的标记“*”。导入调用期望 正是一个论点。

错误似乎是由 morse-pro-message.js 中的这一行引起的:

import * as Morse from './morse-pro';

我认为这是尝试将所有 morse-pro 文件作为模块导入。

我不熟悉 ES6 模块,但该库似乎可以在普通 JavaScript 上下文中为其他人工作。我在 Swift 中加载库的方式有问题吗?还是模块是 JavaScriptCore 不支持的功能? (documentation 只是说它支持“JavaScript”并且没有更具体。)

如果有任何建议能指引我在 JavaScriptCore VM 中运行这个库,我将不胜感激。

【问题讨论】:

不知道有没有帮助。尝试安装 babel 并将您的 javascript 文件转换为 es5 github.com/scp93ch/morse-pro 仅包含 src,即 ES6。不将 dist 提交到 repo 是很常见的。如果您克隆了它,预计您将使用npm build 自己将其构建为 ES5。为避免这种情况,请根据经验从 NPM 安装包。 我很确定 'import' 在 JavaScriptCore 中没有正确实现 【参考方案1】:

在黑暗中摸索了很多次之后,我找到了一种无需手动更改即可使库可供 Swift 使用的方法。

首先,正如@estus 所建议的,我使用 NPM 安装了该库,该库将其转换为 ES5,但不解决依赖关系。所以它仍然是一堆单独的文件,它们使用浏览器和 JavaScriptCore 都无法理解的 requireexport 关键字相互调用。

然后我使用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 类,所以回到 Swift 中:

jsContext?.evaluateScript("var morseMessage = new Morse.MorseMessage()")
print(jsContext!.evaluateScript("morseMessage.translate('abc')"))

打印“.- -... -.-.”如你所料。

这样做的缺点是您必须手动将要导出的任何类和函数添加到导出文件中。不过,这似乎是最简单的方法。如果有更好的方法,我很想听听!

【讨论】:

什么是新的 Morse.MorseMessage() ?你能解释一下js代码吗 @mirhpedanielle 我对此有点生疏,请原谅不准确之处。 MorseMessage 是来自 Stephen Phillips 的 morse-pro 的 javascript class。在这个答案中,我使用 Browserify 的 --standalone 标志创建了一个名为 Morse 的 javascript 对象并将 MorseMessage 附加到它,以便轻松导入 Swift 的 Javascript 上下文。一旦完成(并导入包),MorseMessage 就像任何普通的 Javascript 类一样工作。 谢谢哥们!!.. 但我怀疑是否有必要创建一个带有包名的包.. 并且调用 jsContext.evaluateScript 不起作用 它对我有用。如果您想出更好的不捆绑的方法,请发布答案。 我正在尝试在 wkwebview 中使用外部库。我需要遵循同样的方式吗?或者我应该只在路径中加载 bundle.js 文件。请指教。【参考方案2】:

虽然对模块使用import 是不可能的。

您可以支持require('path/filename'); 语法。

这是通过向 JS 提供 require 作为函数来完成的。 import 命令(不幸的是)太奇特而无法在JSContext 的给定限制中实现。

查看以下实现

目标-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];

本示例基于凤凰https://github.com/kasper/phoenix中的代码

https://github.com/kasper/phoenix/blob/master/Phoenix/PHContext.m#L195-L206

斯威夫特 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()
    

【讨论】:

【参考方案3】:

我在访问我自己的 JS 代码时也有同样的问题,它使用 3 个外部库(Lodash、Moment 和 Moment-range)。

最初我尝试在我的应用程序包中导入所有 4 个 .js 文件(1-我自己的文件和 3-库)。但是当我这样尝试时它不起作用。 我在自己的 JS 代码中导入了这些库时遇到了问题

解决方法:

最后,我将这些库代码中的所有代码复制并粘贴到我自己的代码中,并在我的应用程序中只导入了一个 .js 文件(我自己的文件)。所以在这种情况下,不需要从任何库中导入任何东西。所以我的代码成功了。

注意:我不确定我的解决方法是否是好方法。我只是想注册我的问题和解决方法,这对我有帮助。

我的代码:

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 的 JavaScriptCore 中导入模块?的主要内容,如果未能解决你的问题,请参考以下文章

如何在OC中使用Swift如何在Swift中使用OC

Swift - 如何在 Swift 中获取另一个应用程序的 NSDockTile 对象?

如何在 swift 3.0 中编写此代码,特别是如何在 swift3 中使用宏?

如何引用在不同的 .swift 文件中定义的数据类型?

如何引用在不同的 .swift 文件中定义的数据类型?

swift 如何在Swift中为UITextField设置填充。