β

利用 JavaScriptCore 让 Web 与 Native 交互更便捷

六瘾码士的随笔 382 阅读

还记得以前在网页上用 iframe 指定一个 URL 并通过 native 的 - webView:shouldStartLoadWithRequest:navigationType: 回调方法拦截请求,再蛋疼的各种截取和判断字符串,结合 json 结构的字符串来获取 web 端发来的数据。
通过 - stringByEvaluatingJavaScriptFromString: 来拼接老长的字符串来传递数据给 web 端吗。。。

现在我们可以对这种方式说 「爱过~」 了。

先看一下 Objective-C 与 JavaScript 之间类型的对应关系

Objective-C type JavaScript type
nil undefined
NSNull null
NSString string
NSNumber number, boolean
NSDictionary Object object
NSArray Array object
NSDate Date object
NSBlock Function object
id Wrapper object
Class Constructor object

返回的 JSValue 转换为 Objective-C 类型的方法

- (BOOL)toBool;

- (double)toDouble;

- (int32_t)toInt32;

- (uint32_t)toUInt32;

- (NSNumber *)toNumber;

- (NSString *)toString;

- (NSDate *)toDate;

- (NSArray *)toArray;

- (NSDictionary *)toDictionary;

- (id)toObject;

- (id)toObjectOfClass:(Class)expectedClass;

我们主要使用的是 JSContext 这个类,下面通过几个简单的例子来演示。

首先需要加入 JavaScriptCore.framework 并引入。

#import <JavaScriptCore/JavaScriptCore.h>

Objective-C 调用纯 JavaScript

例如有一段计算阶乘的脚本

function factorial(n) {
  if (n < 0){
      return;
  }
  if (n === 0){
      return 1;
  }
  return n * factorial(n - 1)
};

将脚本文件拖入项目。

注意在项目的 TARGETS > Build Phases 中,把 *.js 文件从 Compile Sources 都拖到 Copy Bundle Resources,否则会有编译警告。

通过 - evaluateScript: 方法把脚本引入 JSContext 对象

NSString *path = [[[NSBundle mainBundle] bundlePath]  stringByAppendingPathComponent:@"test.js"];

NSString *testScript = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];

self.context = [[JSContext alloc] init];
[self.context evaluateScript:testScript];

通过 - objectForKeyedSubscript: 传入 function 的名称以取到该方法, 再通过 - callWithArguments: 传入参数并得到返回值(如果有)。

NSNumber *inputNumber = [NSNumber numberWithInteger:[self.inputNumberTextField.text integerValue]];

JSValue *function = [self.context objectForKeyedSubscript:@"factorial"];
JSValue *result = [function callWithArguments:@[inputNumber]];

NSNumber *number = [result toNumber];

JavaScript 调用 native 代码

首先通过一个协议来关联 JavaScript 的 function 和 native 方法,

还可通过 JSExportAs(functionName, Selector); 来指定 function 的别名。

@protocol TestJSExport <JSExport>

JSExportAs
(functionNameForJS /** JavaScript function 的别名 */,

- (void)handleFactorialCalculateWithNumber:(NSNumber *)number

);

- (void)pushToNextViewControllerWithTitle:(NSString *)title;

@end

然后在 UIWebView 回调方法 - webViewDidFinishLoad: 中把 web 的 javaScriptContext 与我们用来操作的 JSContext 对象关联,并把 self 设为 JavaScript 调用 function 的对象。

self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

// 以 JSExport 协议关联 native 的方法
self.context[@"native"] = self;
//此处有误,关联到 self 会造成 self 这个对象无法释放的问题,建议使用后面的 block 形式,或者关联到非 self 本身的对象。(weakSelf 也不行)

这样在 web 中我们就可以通过所注册的 native 对象来调用到 native 方法。

<textarea  id="input" style="font-size:10pt;color:black;"></textarea>

<input type="button" value="计算阶乘" onclick="native.functionNameForJS(input.value);" />

<a id="push" href="http://albertgh.github.io/blog/2014/04/11/JavaScriptCoreSample/"#" onclick="native.pushToNextViewControllerWithTitle("'Next VC');">
    push to next ViewController
</a>

建议使用后面的 block 形式,或者关联到非 self 本身的对象。(weakSelf 也不行)

同样的,先关联 javaScriptContext ,然后直接给 JSContext 对象 - setObject:forKeyedSubscript: 来设定相应 block。

self.context[@"log"] =
^(NSString *str)
{
    NSLog(@"%@", str);
};

这样在 web 中我们就可以直接调用上面注册的 function

<input type="button" value="测试log" onclick="log('测试');" />

使用 Block 要注意避免循环引用

JSContextexceptionHandler 属性赋予相应 Block

self.context.exceptionHandler =
^(JSContext *context, JSValue *exceptionValue)
{
    context.exception = exceptionValue;
    NSLog(@"%@", exceptionValue);
};

我们可以来利用一些开源的 JavaScript 库来做一些比 native 代码更方便和易于实现的东西

随便找了一个 Highcharts 图表库来做例子。

注意:Highcharts 开源但不完全免费。个人用户及非商业用途免费,商业用途需要购买许可。

大致的调用代码如下

NSArray *the1024Data = @[@33, @41, @32, @51, @42, @103, @136];
NSDictionary *the1024Dict = @{@"name": @"1024", @"data": the1024Data};

NSArray *theCCAVData = @[@8, @11, @21, @13, @20, @52, @43];
NSDictionary *theCCAVDict = @{@"name": @"CCAV", @"data": theCCAVData};

NSArray *seriesArray = @[the1024Dict, theCCAVDict];

[self.context[@"drawChart"] callWithArguments:@[seriesArray]];
function drawChart (seriesArray)
{
  ...
}

详细的代码就不贴了,见项目 JavaScriptCoreSample

作者:六瘾码士的随笔
原文地址:利用 JavaScriptCore 让 Web 与 Native 交互更便捷, 感谢原作者分享。