本文紧接着上一篇:JavaScriptCore 开发相关 - 目录 开始来介绍 JavaScriptCore 这个框架。
JavaScriptCore(之后简称 JSCore)是一个开源的框架,是 WebKit 的一部分,用最简单的话描述这个框架,它大概提供了两种能力:
在原生代码里面执行 JavaScript,而不用通过浏览器
把原生对象注入到 JavaScript 环境里面去
上面这两句话归纳一下就是:提供了 JS 代码与原生代码交互的能力,通过 JSCore 可以更好的进行两端的对象暴露,这使得代码可以不断地在 JS 环境和原生环境穿梭。
有一点需要注意的是,JSCore 只是实现了标准 JavaScript 语言,所以也自然就没有浏览器对象(BOM),也就是说在 JSCore 里面执行代码,是没有 window, 没有 dom, 没有 XMLHTTPRequest 这些内容的。
# JSContext
JSContext 将会是我们要用到最主要的类,一个最简单的运行 JS 的例子:
JSContext *context = [[JSContext alloc] init]; [context evaluateScript:@"var array = ['a', 'b', 'c']"]; NSString *string = [[context evaluateScript:[NSString stringWithFormat:@"array[%d]", 1]] toString];
这三行代码创建了一个 JS 环境:context,然后在 JS 环境里面创建了一个数组:array,然后通过 evaluateScript 取出了 array[1],所以最后 string 的值是 b。
这个用法其实很简单也很直观,最后做一个 toString 是因为 evaluate 返回的是一个 JSValue,我们需要把它转换成 Native 类型。
# JSValue
JSValue 就是跑在 JS 环境里面的对象,当你在 Native 环境使用一个 JSValue 的时候,他实际上有可能是任何类型,例如一个数字,一个字符串,一个数组甚至是一个函数,类型问题永远是最痛苦的问题,在整个 Pin 3.0 开发过程中很多时候就是在处理两端的类型问题。
实际上 Apple 代码里面有一个对应表:
Objective-C type | JavaScript type --------------------+--------------------- nil | undefined NSNull | null NSString | string NSNumber | number, boolean NSDictionary | Object object NSArray | Array object NSDate | Date object NSBlock (1) | Function object (1) id (2) | Wrapper object (2) Class (3) | Constructor object (3)
上面这个表是一个默认行为,但是在实际中我们要处理的情况会比这个要复杂的多,所以我们在之后会有专门的一篇文章来讲类型系统。
JSValue 对象带了一些 toXXX 的方法,用于将 JS 对象转换到 Native 对象,同时还有一些 isXXX 方法用于来判断是否是一个 XXX 对象。(请注意 isArray 和 isDate 是 iOS 9 才有的,如果你需要支持更老的系统,你需要自己实现这两个函数)
# 几个细节
我给 JSValue 写了几个 Category 方法用于更好的判断数据,比如:
- (BOOL)isNilObject { return self.isNull || self.isUndefined; }
另外当你在使用 toString 方法时,你永远不会得到一个 null 对象,对于 JS 里面的空字符串 toString 之后你会在 Native 得到 "undefined" 这个字符串,所以:
- (NSString *)toStringOrNil { return self.isNilObject ? nil : self.toString; }
这样你能确保自己该得到 nil 的时候就得到 nil,类似的还有 date。
# 下标操作
JSContext 和 JSValue 都支持下标操作,所以我们可以轻易地通过下标操作来取到上面的对象,比如上述代码中我们分配在 context 上面的 array:
NSArray *array = [context[@"array"] toArray];
这样就将 JS 对象转换成了 Native 对象。
此外下标操作是一个读写操作,类似的还可以将 Native 对象注入到 JS 环境里:
context[@"array"] = @[@"A", @"B", @"C"];
得益于这个便利的操作,在大部分时候我们用 JS 对象和 Native 对象可以看起来一样。
# 对象枚举
当一个 JSValue 是 Array 或 Map 时,你可能需要枚举这个对象,这是很常见的需求,遗憾的是貌似 JSCore 并没有提供类似的方法,所以还是将其扩展到 Category 里面去:
typedef void (^JSValueForEachBlock) (NSInteger idx, JSValue *value); typedef void (^JSValueForKeyValueBlock) (NSString *key, JSValue *value); - (NSInteger)count { for (NSInteger idx=0; ; ++idx) { JSValue *value = self[idx]; if (value == nil || value.isNilObject) { return idx; } } return 0; } - (void)forEach:(JSValueForEachBlock)block { for (NSInteger idx=0; idx# 回调
通过 JS 代码调用 Native 代码,Native 进行一大波操作之后得到了一个结果,将这个结果回调给 JS 的方法,就是一个完整的交互过程。
上面其实提到过,JSValue 可能是一个函数,也就是说 JSContext 或者 JSValue 里面包含的 JSValue,可能是一个函数(上面这句话有点绕),那么假设我们在 JSContext 里面分配了这样一个函数:
[context evaluateScript:@"function add(a, b) { return a + b; }"];我们直接通过下标在 Native 里面获得这个函数:
JSValue *func = context[@"add"];并且通过 callWithArguments 来调用他:
JSValue *result = [func callWithArguments:@[@(1), @(2)]];上述 result 是一个封装的数字 3,于是这就完成了 JS 和 Native 调来调去的过程。
上面这个例子是整个系统里面最重要基础,他表示当我们在任何时候拿到一个 JS 环境中的函数,都可以通过这个方法完成回调。
# 一个易错的细节
callWithArguments 的参数是一个 NSArray,这个参数在 JS 环境里面其实是会被转换成一个参数序列,正如上述例子中的 1 和 2 是分别被分配到了 add(a, b) 中的 a 和 b 里面去(而不是 add(array))。
所以当你真的要给 JS 环境传递一个数组时,也一定要通过 @[] 来把它变成一个二位数组。
否则的话这个数组会被分配成 (arg1, arg2, arg3 ...),当然,简化这些问题最好的版本还是用 Category 封装一下。
# Block
在 JSContext 上面我们除了给他注入 Native 对象,也可以直接注入一个 Block:
context[@"log"] = ^(NSString *string) { NSLog(@"%@", string); };这种注入本质上是直接给 JS 环境增加函数,直接调用 Native 的代码,例如上述代码你可以在 JS 环境中使用 log("") 来输出内容。
这个东西也有很多细节问题,我将会在之后讲类型系统的章节里面具体描述。
# JSExport
JSExport 是整个 JSCore 里面最神奇的部分,也正是 JSExport 让我们把 Native 对象暴露给 JS 环境异常的容易,简单说,实现 JSExport 协议,可以把 Native 对象的属性和方法暴露给 JS 环境,例如:
@protocol NativeObjectExport @property (nonatomic, assign) BOOL property1; - (void)method1:(JSValue *)arguments; @end @interface NativeObject : NSObject@property (nonatomic, assign) BOOL property1; @property (nonatomic, strong) id property2; - (void)method1:(JSValue *)arguments; - (void)method2; @end上面这个 NativeObject 对象可以是任意的对象,只要他实现了上面的 NativeObjectExport,那么这个协议里面的属性和对象就可以直接被 JS 环境使用,例如上面的 property1 和 method1,这丝毫不影响他作为一个对象可以做的其他事情。
我们只要在 context 里面注入这么一个对象,就可以在 JS 环境放肆的与 Native 进行交互了:
context[@"helper"] = [NativeObject new];JS 环境中可以使用这些方法了:
var prop = helper.property1; helper.method1({ handler: function(object) { } });上面的 handler 可以拿来做什么?
当 Native 代码执行完 method1 之后,可以通过这个 handler 回调到 JS 环境:
[arguments[@"handler"] callWithArguments:@[object]];JS 环境通过 function 的 object 拿到返回结果,这就是一个完整的流程。
# JSExportAs
这个东西我会讲一下原理但我只会在极少数的情况下用到这个东西,简单说这是一个宏,用于把 Objective-C 风格的函数起一个 JS 风格的别名:
JSExportAs(show, - (void)showMessage:(NSString *)message subtitle:(NSString *)subtitle);这样我们在 Native 代码里面还是使用 Objective-C 风格的每个参数都显示参数名的形式,但是在 JS 代码里面的调用却成了:
show(message, subtitle);感兴趣可以看一下这个宏是如何实现的。
我刚刚说我基本不会用到这个方法,是因为这样去实现接口并不灵活,比如说你实现一个 HTTP 接口,参数可以是非常多变的,今天有可能 5 个参数明天有可能支持 6 个,在这样的假定下,我会把所有的参数传递都通过 Dictionary 来完成,比如这样:
- (void)show:(JSValue *)arguments { NSString *message = [arguments[@"message"] toString]; NSString *subtitle = [arguments[@"subtitle"] toString]; JSValue *handler = arguments[@"handler"]; }在 JS 端调用时:
show({ message: "hello", subtitle: "world", handler: function() { } })这样的话可以更方便的增减参数,同时也可以更好的处理回调,这种 API 风格会贯穿整个 Pin 3.0 的开发。
# class_addProtocol
这是 Objective-C Runtime 的一个方法,他可以给一个类动态的添加一个 Protocol,这是非常重要的一个特性,上述例子中的 NativeObject 是我们新建的一个类,所以我们当然可以让他实现 JSExport 来导出我们要的属性,而通过 class_addProtocol 我们可以对已有的类导出属性,例如在之后的例子中我们将会构建 UI 系统,JS 环境里面会到处跑 UIView 对象,而 UIView 对象的各种属性,都是可以在 JS 环境里面直接访问的。
这个方法的用法十分简单,构建一个实现了 JSExport 的 Protocol:
@protocol UIViewExport @property (nonatomic, assign) CGFloat alpha; @end然后给 UIView 添加这个 Protocol 即可:
class_addProtocol(UIView.class, @protocol(UIViewExport));当我们在 JS 环境里面拿到一个 UIView 时,他的 alpha 属性我们可以直接拿到了,这将省掉很多类型处理上的麻烦。
# 结语
一篇以基础为主的文章再写下去就太长了,在之后的文章里面再深入讨论数据类型、UI 系统等内容,本篇就此打住。
PS: 广告时间,看 Pin 3.0 的 JS 系统能做什么,请访问:xTeko,下载 Pin 进入扩展商店查看目前支持的 JS 扩展,关注 xTeko GitHub 查看最新的源码。
PS2: 由于知乎的代码编辑器实在太烂,上述内容基本靠手打,只保证原理正确。
- EOF -