在 iOS Safari 执行任意的 JavaScript

Safari Extensions

这篇文章我来炒一个冷饭,但是冷饭中又带有一点新意。我们讨论的内容基于我最近做的一个功能,能够让 iOS 上面的 Safari 加载自定义的 JavaScript。

你可能会觉得这个没什么,但我们要做的实际上是能够让 Safari 加载动态的、自定义的 JavaScript,而不仅仅是应用打包时候就放好了的。

首先我们看下这个 JSBox 扩展的效果:




我们在 Safari 上面加载了 Eruda 这个强大的 devtool,可以让你直接就在手机上调试页面,可以说是十分酷炫了。

同样的原理我们还可以应用到别的 JavaScript 库,例如 Firebug Lite, vConsole 等等。这个 JSBox 扩展可以在这里体验:Safari Extensions

基本原理

事实上 iOS 上面是并没有 Safari Extension 这一说的,我们要做的其实是一个 Action Extension,这个 Extension 在 Safari 上面启动的时候,可以通过一个内置的 JavaScript 文件实现这样一个流程:

  • 获取 Safari 环境的数据
  • 在 iOS 应用里面运行一些代码
  • 将结果数据回传给 Safari 环境

实现这些依靠的是在 Info.plist 里面通过 NSExtensionJavaScriptPreprocessingFile 指定 JavaScript 文件,这个文件的格式长这样:

var MyExtensionJavaScriptClass = function() {};

MyExtensionJavaScriptClass.prototype = {
  run: function(arguments) {
    // Pass the baseURI of the webpage to the extension.
    arguments.completionFunction({"baseURI": document.baseURI});
  },
  // Note that the finalize function is only available in iOS.
  finalize: function(arguments) {
    // arguments contains the value the extension provides in [NSExtensionContext completeRequestReturningItems:completion:].
    // In this example, the extension provides a color as a returning item.
    document.body.style.backgroundColor = arguments["bgColor"];
  }
};

// The JavaScript file must contain a global object named "ExtensionPreprocessingJS".
var ExtensionPreprocessingJS = new MyExtensionJavaScriptClass;

这个官方的模板已经说的很清楚了,run 函数是入口,通过 completionFunction 进入到 Native 环境,finalize 函数是 Native 环境回来的地方,按照这个逻辑我们已经可以实现上述我们说的流程。

但是,NSExtensionJavaScriptPreprocessingFile 这个文件是写在 Bundle 里面的,它必然是只读的,所以要实现执行任意的 JavaScript 代码我们需要通过将代码动态地从 Native 环境传过来:

NSExtensionItem *item = [NSExtensionItem new];
NSDictionary *data = @{ NSExtensionJavaScriptFinalizeArgumentKey: @{ @"script": script } };
item.attachments = @[[[NSItemProvider alloc] initWithItem:data typeIdentifier:(id)kUTTypePropertyList]];
[self.extensionContext completeRequestReturningItems:@[item] completionHandler:nil];

在 JavaScript 文件里面我们要做的事情就更简单了:

finalize: function(arguments) {
  eval(arguments["script"]);
}

script 就是我们要动态执行的脚本,在 JSBox 里面这个脚本可以任意由用户定义,到了 finalize 函数里面我们取出来 eval 一下即可。

听起来是很简单的一件事情,当你去执行一些本地脚本的时候,也确实没有什么问题(JSBox 的接口通过 $safari.inject 封装),例如:

top Created with Sketch.