原文鏈接
JSContext
/ JSValue
JSContext
是運行 JavaScript 代碼的環境。一個 JSContext
是一個全局環境的實例,如果你寫過一個在瀏覽器內運行的
JavaScript,JSContext
類似於 window
。創建一個 JSContext
後,可以很容易地運行
JavaScript 代碼來創建變量,做計算,甚至定義方法:
JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"var num = 5 + 5"];
[context evaluateScript:@"var names = ['Grace', 'Ada', 'Margaret']"];
[context evaluateScript:@"var triple = function(value) { return value * 3 }"];
JSValue *tripleNum = [context evaluateScript:@"triple(num)"];
代碼的最後一行,任何出自 JSContext
的值都被包裹在一個 JSValue
對象中。像
JavaScript 這樣的動態語言需要一個動態類型,所以 JSValue
包裝了每一個可能的 JavaScript 值:字符串和數字;數組、對象和方法;甚至錯誤和特殊的 JavaScript 值諸如 null
和 undefined
。
JSValue
包括一系列方法用於訪問其可能的值以保證有正確的 Foundation 類型,包括:
JavaScript Type |
JSValue method |
Objective-C Type | Swift Type |
---|---|---|---|
string |
toString |
NSString |
String! |
boolean |
toBool |
BOOL |
Bool |
number |
toNumber toDouble toInt32 toUInt32 |
NSNumber double int32_t uint32_t |
NSNumber! Double Int32 UInt32 |
Date |
toDate |
NSDate |
NSDate! |
Array |
toArray |
NSArray |
[AnyObject]! |
Object |
toDictionary |
NSDictionary |
[NSObject : AnyObject]! |
Object |
toObject toObjectOfClass: |
custom type | custom type |
從上面的例子中得到 tripleNum
的值,只需使用適當的方法:
NSLog(@"Tripled: %d", [tripleNum toInt32]);
// Tripled: 30
下標值
對 JSContext
和 JSValue
實例使用下標的方式我們可以很容易地訪問我們之前創建的 context
的任何值。JSContext
需要一個字符串下標,而 JSValue
允許使用字符串或整數標來得到裏面的對象和數組:
JSValue *names = context[@"names"];
JSValue *initialName = names[0];
NSLog(@"The first name: %@", [initialName toString]);
// The first name: Grace
Swift 展示了它的青澀,在這裏,Objective-C 代碼可以利用下標表示法,Swift 目前只公開原始方法來讓下標成爲可能:
objectAtKeyedSubscript()
和objectAtIndexedSubscript()
。
調用方法
JSValue
包裝了一個 JavaScript 函數,我們可以從 Objective-C / Swift 代碼中使用 Foundation 類型作爲參數來直接調用該函數。再次,JavaScriptCore 很輕鬆的處理了這個橋接:
JSValue *tripleFunction = context[@"triple"];
JSValue *result = [tripleFunction callWithArguments:@[@5] ];
NSLog(@"Five tripled: %d", [result toInt32]);
錯誤處理
JSContext
還有另外一個有用的招數:通過設置上下文的 exceptionHandler
屬性,你可以觀察和記錄語法,類型以及運行時錯誤。 exceptionHandler
是一個接收一個 JSContext
引用和異常本身的回調處理:
context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
NSLog(@"JS Error: %@", exception);
};
[context evaluateScript:@"function multiply(value1, value2) { return value1 * value2 "];
// JS Error: SyntaxError: Unexpected end of script
JavaScript 調用
現在我們知道了如何從 JavaScript 環境中提取值以及如何調用其中定義的函數。那麼反向呢?我們怎樣才能從 JavaScript 訪問我們在 Objective-C 或 Swift 定義的對象和方法?
讓 JSContext
訪問我們的本地客戶端代碼的方式主要有兩種:block 和 JSExport
協議。
Blocks
當一個 Objective-C block 被賦給 JSContext
裏的一個標識符,JavaScriptCore 會自動的把 block 封裝在 JavaScript 函數裏。這使得在 JavaScript 中可以簡單的使用 Foundation 和 Cocoa 類,所有的橋接都爲你做好了。見證了 CFStringTransform
的強大威力,現在讓我們來看看
JavaScript:
context[@"simplifyString"] = ^(NSString *input) {
NSMutableString *mutableString = [input mutableCopy];
CFStringTransform((__bridge CFMutableStringRef)mutableString, NULL, kCFStringTransformToLatin, NO);
CFStringTransform((__bridge CFMutableStringRef)mutableString, NULL, kCFStringTransformStripCombiningMarks, NO);
return mutableString;
};
NSLog(@"%@", [context evaluateScript:@"simplifyString('안녕하새요!')"]);
在這兒,Swfit 還有一個坑,請注意,這僅適用於 Objective-C 的 block,而不是 Swift 的閉包。要在
JSContext
中使用 Swift 閉包,它需要(a)與@ objc_block
屬性一起聲明,以及(b)使用 Swift 那個令人恐懼的unsafeBitCast()
函數轉換爲AnyObject
。
內存管理
由於 block 可以保有變量引用,而且 JSContext
也強引用它所有的變量,爲了避免強引用循環需要特別小心。避免保有你的 JSContext
或一個
block 裏的任何 JSValue
。相反,使用 [JSContext currentContext]
得到當前上下文,並把你需要的任何值用參數傳遞。
JSExport
協議
另一種在 JavaScript 代碼中使用我們的自定義對象的方法是添加 JSExport
協議。無論我們在 JSExport
裏聲明的屬性,實例方法還是類方法,繼承的協議都會自動的提供給任何
JavaScript 代碼。我們將在下一節看到。
JavaScriptCore 實戰
讓我們做一個使用了所有這些不同的技術的示例 - 我們將定義一個 Person
模型符合 JSExport
子協議 PersonJSExports
的例子,然後使用
JavaScript 從 JSON 文件中創建並填充實例。都有一個完整的 JVM 在那兒了,誰還需要 NSJSONSerialization
?
1) PersonJSExports
和 Person
我們的 Person
類實現了 PersonJSExports
協議,該協議規定哪些屬性在 JavaScript 中可用。
由於 JavaScriptCore 沒有初始化,所以
create...
類方法是必要的,我們不能像原生的 JavaScript 類型那樣簡單地用var person = new Person()
。
// in Person.h -----------------
@class Person;
@protocol PersonJSExports <JSExport>
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property NSInteger ageToday;
- (NSString *)getFullName;
// create and return a new Person instance with `firstName` and `lastName`
+ (instancetype)createWithFirstName:(NSString *)firstName lastName:(NSString *)lastName;
@end
@interface Person : NSObject <PersonJSExports>
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property NSInteger ageToday;
@end
// in Person.m -----------------
@implementation Person
- (NSString *)getFullName {
return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
+ (instancetype) createWithFirstName:(NSString *)firstName lastName:(NSString *)lastName {
Person *person = [[Person alloc] init];
person.firstName = firstName;
person.lastName = lastName;
return person;
}
@end
2) JSContext
配置
之前,我們可以用我們已經創建的 Person
類,我們需要將其導出到 JavaScript 環境。我們也將藉此導入 Mustache
JS library,我們將應用模板到我們的 Person
對象。
// export Person class
context[@"Person"] = [Person class];
// load Mustache.js
NSString *mustacheJSString = [NSString stringWithContentsOfFile:... encoding:NSUTF8StringEncoding error:nil];
[context evaluateScript:mustacheJSString];
3) JavaScript 數據和進程
下面就來看看我們簡單的 JSON 例子,這段代碼將創建新的 Person
實例。
注意:JavaScriptCore 轉換的 Objective-C / Swift 方法名是 JavaScript 兼容的。由於 JavaScript 沒有參數 名稱,任何外部參數名稱都會被轉換爲駝峯形式並且附加到函數名後。在這個例子中,Objective-C 的方法
createWithFirstName:lastName:
變成了在JavaScript中的createWithFirstNameLastName()
。
var loadPeopleFromJSON = function(jsonString) {
var data = JSON.parse(jsonString);
var people = [];
for (i = 0; i < data.length; i++) {
var person = Person.createWithFirstNameLastName(data[i].first, data[i].last);
person.birthYear = data[i].year;
people.push(person);
}
return people;
}
4) 加到一起
剩下的就是加載 JSON 數據,調用 JSContext
將數據解析成 Person
對象的數組,並用 Mustache 模板呈現每個 Person
:
// get JSON string
NSString *peopleJSON = [NSString stringWithContentsOfFile:... encoding:NSUTF8StringEncoding error:nil];
// get load function
JSValue *load = context[@"loadPeopleFromJSON"];
// call with JSON and convert to an NSArray
JSValue *loadResult = [load callWithArguments:@[peopleJSON]];
NSArray *people = [loadResult toArray];
// get rendering function and create template
JSValue *mustacheRender = context[@"Mustache"][@"render"];
NSString *template = @"{{getFullName}}, born {{birthYear}}";
// loop through people and render Person object as string
for (Person *person in people) {
NSLog(@"%@", [mustacheRender callWithArguments:@[template, person]]);
}
// Output:
// Grace Hopper, born 1906
// Ada Lovelace, born 1815
// Margaret Hamilton, born 1936