JavaScriptCore举例(Objective-C)

前言

本文翻译自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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容