前言
本文翻译自JaJavaScriptCore by Example
翻译的不对的地方还请多多包涵指正,谢谢~
之前翻译了JavaScriptCore在Swift中的使用,如有兴趣请戳这里~
JavaScriptCore 举例
JavaScriptCore并不是一个新的框架。事实上从Mac OS X 10.2版本就一直存在。从iOS7和Mac操作系统10.9,苹果为JavaScriptCore框架引入了原生的Objective-C的接口。苹果确实也通过以开发者评论的形式在头文件中提供了一些文档,但这些接口的文档还是很少。我们在去年秋天第一次写JavaScriptCore和iOS 7,但今天我想说明在iOS应用中怎样及为什么要使用JavaScript。最终你会获得这些能力:
- 在Objective-C代码中创建及调用JavaScript函数;
- 捕获JavaScript异常;
- JavaScript回调给Objective-C;
- 更改WebView的JavaScript上下文;
完整的工程例子在这里。
我的联系人
我们先从一个例子开始吧~ 我已经写好了一个iOS联系人管理的简单应用。应用预填充了我几个最亲密的几个朋友的联系人信息。
此时,应用能展示联系人列表且支持基本的重排序和删除操作。这里我们来看看BNRContact数据模型的公共头文件:
@interface BNRContact : NSObject
@property (nonatomic, readonly) NSString *name;
@property (nonatomic, readonly) NSString *phone;
@property (nonatomic, readonly) NSString *address;
+ (instancetype)contactWithName:(NSString *)name
phone:(NSString *)phone
address:(NSString *)address;
@end
联系人格式匹配
在当前展示状态中,应用充分相信手机号的格式完全没问题,但并不是如此。注意到联系人Zapp Brannigan的手机号只有一个数字。但我们只希望这些手机号是我们过滤好的。为达到这个目的,引入以下一个JavaScript函数:
var isValidNumber = function(phone) {
var phonePattern = /^[0-9]{3}[ ][0-9]{3}[-][0-9]{4}$/;
return phone.match(phonePattern) ? true : false;
}
这个JavaScript函数使用正则表达式来检测是否是我们期望的手机号格式。每次在BNRContact类中调用contactWithName:phone:address:
方法都会调用这个JavaScript函数去判断手机号的有效性。
+ (instancetype)contactWithName:(NSString *)name phone:(NSString *)phone address:(NSString *)address
{
if ([self isValidNumber:phone]) {
BNRContact *contact = [BNRContact new];
contact.name = name;
contact.phone = phone;
contact.address = address;
return contact;
} else {
NSLog(@"Phone number %@ doesn't match format", phone);
return nil;
}
}
+ (BOOL)isValidNumber:(NSString *)phone
{
// getting a JSContext
JSContext *context = [JSContext new];
// defining a JavaScript function
NSString *jsFunctionText =
@"var isValidNumber = function(phone) {"
" var phonePattern = /^[0-9]{3}[ ][0-9]{3}[-][0-9]{4}$/;"
" return phone.match(phonePattern) ? true : false;"
"}";
[context evaluateScript:jsFunctionText];
// calling a JavaScript function
JSValue *jsFunction = context[@"isValidNumber"];
JSValue *value = [jsFunction callWithArguments:@[ phone ]];
return [value toBool];
}
让我们来看看isValidNumber: method
函数采取的步骤。
获得JSContext
JSContext是JavaScriptCore框架中的入口关键点。JSContext对象代表了你的JavaScript环境状态。你可以在JSContext内定义对象,原型及函数。这些实体将一直存在知道JSContext被释放。在创建JSContext时可以指定一个JSVirtualMachine(JavaScript虚拟机)。当你希望你的JavaScript并行时,你需要指定JSContext的运行的虚拟机环境,因为每个JSVirtualMachine只能运行在单线程内,我们代码默认当前默认虚拟机即可。
定义JavaScript函数
我们使用JSContext的evaluateScript
方法:用于定义JavaScript函数。这个方法用一个字符串来包含JavaScript的代码。因此我们第一步就是将JavaScript函数载入到一个字符串。有很多中方法来写JavaScript代码。如果你需要写很多JavaScript代码,推荐你使用JavaScript编辑器并保存到文件。XCode并不是一个JavaScript编辑器,我选择直接把函数放至字符串。这步完成后,我们的JSContext已存在一个叫isValidNumber
的函数。
调用JavaScript函数
下一步,我们需要JSContext中isValidNumber
函数的持有(类似于指针)。这个持有以JSValue的方式返回。JSValue提供callWithArguments:
方法直接可调用JavaScript函数。isValidNumber
函数入参仅有一个手机号,返回值是一个Bool值。JavaScriptCore框架自动把Bool值包装成JSValue对象。不仅仅是Bool类型,其他类型(不管是原型还是对象类型)都支持,包括NSString,NSDate,NSDictionary,NSArray
等等。想了解更多支持的类型,请看JSValue头文件中的开发者注释。JavaScriptCore提供了很多将JavaScript类型转换为Objective-C类型的便利函数。isValidNumber
函数最后一行toBool
函数就是一个例子。
现在无论什么时候我们添加一个新的联系人,手机号都会被校验。如果不符合我们的手机格式,联系人将不会被创建。让我们来看看实际情况
Zapp Brannigan联系人这次不被添加到列表内。他的手机号没通过isValidNumber
函数的检查。
来抓我呀
在继续JavaScript深入探索前,我们来看一下错误处理。当异常发生的时候JavaScriptCore框架允许指定一个Objective-C代码块(block)作为回调。在isValidNumber
函数中,我们添加这样一个block来捕获JavaScript异常:
[context setExceptionHandler:^(JSContext *context, JSValue *value) {
NSLog(@"%@", value);
}];
现在无论何时JavaScript异常发生,异常信息(这个值会被传递到block)都能写入日志。这些异常会给我们一些有帮助的关于JavaScript代码运行出错的信息。例如,如果我们忘记用右括号来结束一个函数调用,异常将会发生,而且JavaScriptCore框架会告诉我们符号丢失。即使这点琐碎的错误处理也能让我们在漫长的漆黑的看起来没有发生任何事的暴风雨夜晚有个微弱的灯塔指引。
狂野的网站
在Objective-C应用内使用JavaScript的一个主要原因是在UIWebView内与网页内容交互。自从iOS2开始,仅有的官方方法是使用UIWebView的stringByEvaluatingJavaScriptFromString:方法。不幸的是,在引入JavaScriptCore时这个方法并没有改变。
一些忠告
尽管苹果给了一个不可思议的方式来处理JavaScript,但他们似乎不情愿让我们用它来处理UIWebView的网页内容。作为开发者,我们看到了可能性也希望为我们的应用来使用这些能力。但记住这里只是向你展示如何从UIWebView内获取JSContext,但这些苹果可能并不希望你这么做。在这里我已经给你警告了。
小小KVC,大大的作用
至此,我们已经和匆忙创建的JSContext对象打过交道了。UIWebView实例有它自己的JSContext对象,为了操作网页内容,我们需要获取UIWebView的JSContext。苹果并没有提供获取UIWebView的JSContext属性的方法,幸运的是,我恩可以通过KVC实现。使用KVC,可以获取一个UIWebView实例的JSContext属性。另一种获取UIWebView中的JSContext属性的方法在这个工程内有说明。
这个方法实现的比较有技术性,但可能因违反苹果私有API政策而不能提交到AppStore。我并不是一名律师,建议如果要在一个发布的应用使用该方法的话,最好去调研一下潜在的风险。但或许以后就对这个方法放开了也说不定。
3-2-1 通讯录
假设我创建了一个和我的iOS应用功能一个的Web应用。我希望两个应用能够协同工作,这样便可以保持我的通讯录同步。当用户在列表上方点击『添加』按钮,我希望iOS应用能从Web应用推出一个『添加联系人』的Web页面。使用JavaScriptCore框架,我们会为『添加联系人』提交动作提供一个新的JavaScript监听。这个函数会回调到Objective-C代码。通过这种方式,新的联系人会被同步地添加到Web和iOS应用。
在JavaScript函数回调Objective-C应用前,我们必须先告知JavaScriptCore框架任何期望回调的函数。这个是通过使用JSExport协议实现的。
首先,我们在BNRContactApp类中导出addContact:
方法:
@protocol BNRContactAppJS <JSExport>
- (void)addContact:(BNRContact *)contact;
@end
@interface BNRContactApp : NSObject <BNRContactAppJS>
...
@end
通过在BNRContactAppJS协议内申明addContact:
方法,该方法在JavaScriptCore框架就可见了。所有BNRConactApp其他属性或方法都会被隐藏。
下一步我们把BNRContact类中的contactWithName:phone:address:
方法导出:
@protocol BNRContactJS <JSExport>
+ (instancetype)contactWithName:(NSString *)name
phone:(NSString *)phone
address:(NSString *)address;
@end
@interface BNRContact : NSObject <BNRContactJS>
...
@end
现在我们需要为我么的WebView实现webViewDidFinishLoad:
的代理方法:
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
// get JSContext from UIWebView instance
JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// enable error logging
[context setExceptionHandler:^(JSContext *context, JSValue *value) {
NSLog(@"WEB JS: %@", value);
}];
// give JS a handle to our BNRContactApp instance
context[@"myApp"] = self.app;
// register BNRContact class
context[@"BNRContact"] = [BNRContact class];
// add function for processing form submission
NSString *addContactText =
@"var contactForm = document.forms[0];"
"var addContact = function() {"
" var name = contactForm.name.value;"
" var phone = contactForm.phone.value;"
" var address = contactForm.address.value;"
" var contact = BNRContact.contactWithNamePhoneAddress(name, phone, address);"
" myApp.addContact(contact);"
"};"
"contactForm.addEventListener('submit', addContact);";
[context evaluateScript:addContactText];
}
首先,我们从UIWebView内获取JSContext属性,并且打开错误日志(你会需要错误日志,因为JavaScript错误很难发现)。
之后我们创建一个BNRContactApp实例的一个持有。之后将使用该持有来调用addContact:
方法。下一步用JSContext注册BNRContact类。这一步之后将允许我们调用contactWithName:phone:address:
方法。
预备工作做好,是时候定义JavaScript函数来处理Web表单了。首先创建JavaScript变量指向表单,再从表单获取参数,用这些参数生成BNRContact对象。JavaScriptCore自动将contactWithName:phone:address:
Objective-C方法映射成JavaScript的contactWithNamePhoneAddress(name, phone, address)
方法。新的联系人创建完成后,我们希望将它添加到BNRContactApp中。addContact:
Objective-C方法被自动映射成JavaScript的addContact(contact)
让我们来看看结果!
结束了嘛?
我已经阐明了如何用JSContext的evaluateScript:
方法及JSValue的callWithArguments:
方法在Objective-C代码中调用JavaScript函数。展示了如何捕获JavaScript异常(强烈推荐你在应用中这么做)。使用KVC,你能够获取UIWebView中JSContext属性。最后,通过使用JSExport协议,我们看到了如何将Objective-C方法暴露给JavaScript。
现在轮到你了,使用你学到的在你的工程中使用一些JavaScript。但记住,苹果不希望你在发布的App中使用私有API。