在iOS开发中, 苹果api味咱们提供了多种javascript和Objective-C交互的方法, 使用仍是比较简单的.javascript
经过webView的stringByEvaluatingJavaScriptFromString:方法调用js代码. 此方法能够无限制的执行任何的js代码.html
/** 原生调用js, 普通的方法: * 经过webView的stringByEvaluatingJavaScriptFromString:方法, 能够无限制的执行任意的js代码,能够经过js代码操控webView上面的任意元素, 也能够直接经过js调用webView中的js代码. 实现从原生代码到javascript的联系. * */ [self.webView stringByEvaluatingJavaScriptFromString:@"showAlert('javascript message')"]; [self.webView stringByEvaluatingJavaScriptFromString:@"document.getElementById('abshdfnb').style.backgroundColor = 'blue';"];
普通的方法是实现代理协议, 在shouldStartLoadWithRequest:方法中拦截发生在webView上的请求,判断这个请求前缀,根据前缀和参数不一样,能够作不一样的处理和操做.这种方式在iOS7如下也是通用的.前端
/** * 是否要加载连接 */ - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { /********************************************************************************** * js调用原生代码, 普通的方式: * 在将要开始请求网址时候, 拦截请求, 判断请求信息并作相应的处理. */ NSURL *url = [request URL]; NSString *scheme = [url scheme]; if ([scheme isEqualToString:@"firstclick"]) { NSArray *args = [url.query componentsSeparatedByString:@"&"]; for (NSString *arg in args) { NSLog(@"arg : %@", arg); } return NO; } return YES; }
(1), 获取js上下文, 经过evaluateScript:方法调用js代码.方法一:java
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; NSString *script = @"showAlert('JSContext message');document.getElementById('abshdfnb').style.backgroundColor = 'blue';"; [context evaluateScript:script];
(2), 原生代码调用js代码方法二:ios
JSValue *jsFunc = context[@"showAlert"]; JSValue *result = [jsFunc callWithArguments:@[@"message from JSValue"]];
//使用javascriptCore框架, 实现js调用原生代码. JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; //定义好JS要调用的方法, share就是调用的share方法名 context[@"share"] = ^() { NSLog(@"+++++++Begin Log+++++++"); NSArray *args = [JSContext currentArguments]; //回到主线程更新UI dispatch_async(dispatch_get_main_queue(), ^{ UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"方式二" message:@"这是OC原生的弹出窗" delegate:self cancelButtonTitle:@"收到" otherButtonTitles:nil]; [alertView show]; }); for (JSValue *jsVal in args) { NSLog(@"%@", jsVal.toString); } NSLog(@"-------End Log-------"); };
NSURLRequest
类获取NSURL对象[request URL]
.// 执行一段script代码, 返回值是javascript代码最后的返回值 - (JSValue *)evaluateScript:(NSString *)script; //从一个javascript源文件中执行一个javascript方法. 第一个参数是要执行的js代码, 第二个参数是js源文件位置的URL. - (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)sourceURL NS_AVAILABLE(10_10, 8_0); //经过类方法,获取js代码的上下文对象. + (JSContext *)currentContext; //类方法获取js的t方法的his对象 + (JSValue *)currentThis; //获取当前的参数, 好比js调用原生代码时候,穿了几个参数,经过此类方法能够获取参数数组, + (NSArray *)currentArguments; //异常处理的block,当执行js遇到异常以后会执行这里的代码. @property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception); // 打印异常,因为JS的异常信息是不会在OC中被直接打印的,因此咱们在这里添加打印异常信息, self.context.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) { context.exception = exceptionValue; NSLog(@"%@", exceptionValue); };
JSVirtualMachine为JavaScript的运行提供了底层资源,JSContext就为其提供着运行环境,经过- (JSValue *)evaluateScript:(NSString *)script;方法就能够执行一段JavaScript脚本,而且若是其中有方法、变量等信息都会被存储在其中以便在须要的时候使用。而JSContext的建立都是基于JSVirtualMachine:- (id)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;,若是是使用- (id)init;进行初始化,那么在其内部会自动建立一个新的JSVirtualMachine对象而后调用前边的初始化方法。web
JSValue则能够说是JavaScript和Object-C之间互换的桥梁,它提供了多种方法能够方便地把JavaScript数据类型转换成Objective-C,或者是转换过去。其一一对应方式可见下表:编程
Objective-C | JavaScript | JSValue Convert | JSValue Constructor |
---|---|---|---|
nil | undefined | valueWithUndefinedInContext | |
NSNull | null | valueWithNullInContext: | |
NSString | string | toString | |
NSNumber | number, boolean | toNumber toBool toDouble toInt32 toUInt32 | valueWithBool:inContext: valueWithDouble:inContext: valueWithInt32:inContext: valueWithUInt32:inContext: |
NSDictionary | Object object | toDictionary | valueWithNewObjectInContext: |
NSArray | Array object | toArray | valueWithNewArrayInContext: |
NSDate | Date object | toDate | |
NSBlock | Function object | ||
id | Wrapper object | toObject toObjectOfClass: | valueWithObject:inContext: |
Class | Constructor object |
// 基本数据类型转换 JSContext *context1 = [[JSContext alloc] init]; JSValue *jsVal = [context1 evaluateScript:@"21+7"]; int iVal = [jsVal toInt32]; NSLog(@"JSValue: %@, int: %d", jsVal, iVal); // 输出: JSValue: 28, int: 28
还能够存一个JavaScript变量在JSContext中,而后经过下标来获取出来。而对于Array或者Object类型,JSValue也能够经过下标直接取值和赋值。api
//能够存一个JavaScript变量在JSContext中,而后经过下标来获取出来。而对于Array或者Object类型,JSValue也能够经过下标直接取值和赋值。 JSContext *ctx = [[JSContext alloc] init]; [ctx evaluateScript:@"var arr = [21, 4, 'Move', 'HAHA']"]; // JSValue *jsArr = ctx[@"arr"]; JSValue *jsArr = [ctx objectForKeyedSubscript:@"arr"]; NSLog(@"jsArr = %@ length = %@", jsArr, jsArr[@"length"]); //能够像操做js数据同样操做JSValue对象 jsArr[3] = @"blog"; jsArr[5] = @5; NSLog(@"jsArr = %@ length = %@", jsArr, jsArr[@"length"]); //将js数组转换成NSArray数组 NSArray *nsArray = [jsArr toArray]; NSLog(@"%@", nsArray); /** 输出: jsArr = 21,4,Move,HAHA length = 4 2016-05-05 11:04:52.887 TestJSOC[1286:1121702] jsArr = 21,4,Move,blog,,5 length = 6 2016-05-05 11:04:52.887 TestJSOC[1286:1121702] ( 21, 4, Move, blog, "<null>", 5 ) */
各类数据类型能够转换,Objective-C的Block也能够传入JSContext中当作JavaScript的方法使用。好比在前端开发中经常使用的log方法,虽然JavaScritpCore没有自带(毕竟不是在网页上运行的,天然不会有window、document、console这些类了),仍然能够定义一个Block方法来调用NSLog来模拟:数组
JSContext *context = [[JSContext alloc] init]; context[@"log"] = ^() { NSLog(@"+++++++Begin Log+++++++"); NSArray *args = [JSContext currentArguments]; for (JSValue *jsVal in args) { NSLog(@"%@", jsVal); } JSValue *this = [JSContext currentThis]; NSLog(@"this: %@",this); NSLog(@"-------End Log-------"); }; [context evaluateScript:@"log('ider', [7, 21], { hello:'world', js:100 });"];
经过Block成功的在JavaScript调用方法回到了Objective-C,并且依然遵循JavaScript方法的各类特色,好比方法参数不固定。也由于这样,JSContext提供了类方法来获取参数列表(+ (NSArray *)currentArguments)和当前调用该方法的对象(+ (JSValue *)currentThis)。上边的例子中对于"this"输出的内容是GlobalObject,这也是JSContext对象方法- (JSValue *)globalObject;所返回的内容。由于咱们知道在JavaScript里,全部全局变量和方法其实都是一个全局变量的属性,在浏览器中是window,在JavaScriptCore是什么就不得而知了。浏览器
Block能够传入JSContext做方法,可是JSValue没有toBlock方法来把JavaScript方法变成Block在Objetive-C中使用。毕竟Block的参数个数和类型已经返回类型都是固定的。虽然不能把方法提取出来,可是JSValue提供了- (JSValue *)callWithArguments:(NSArray *)arguments;方法能够反过来将参数传进去来调用方法。
// 获取javascript上下文中的方法或者值的两种方法: // 方法一. JSValue *function = [self.context objectForKeyedSubscript:@"factorial"]; // 方法二. JSValue * function = self.context[@"factorial"]; JSValue *result = [function callWithArguments:@[inputNumber]]; self.showLable.text = [NSString stringWithFormat:@"%@", [result toNumber]];
从以前的例子和介绍应该有体会到Block在JavaScriptCore中起到的强大做用,它在JavaScript和Objective-C之间的转换 创建起更多的桥梁,让互通更方便。可是要注意的是不管是把Block传给JSContext对象让其变成JavaScript方法,仍是把它赋给exceptionHandler属性,在Block内都不要直接使用其外部定义的JSContext对象或者JSValue,应该将其当作参数传入到Block中,或者经过JSContext的类方法+ (JSContext *)currentContext;来得到。不然会形成循环引用使得内存没法被正确释放。
Objective-C的异常会在运行时被Xcode捕获,而在JSContext中执行的JavaScript若是出现异常,只会被JSContext捕获并存储在exception属性上,而不会向外抛出。时时刻刻检查JSContext对象的exception是否不为nil显然是不合适,更合理的方式是给JSContext对象设置exceptionHandler,它接受的是^(JSContext *context, JSValue *exceptionValue)形式的Block。其默认值就是将传入的exceptionValue赋给传入的context的exception属性
JSContext并不能让Objective-C和JavaScript的对象直接转换,毕竟二者的面向对象的设计方式是不一样的:前者基于class,后者基于prototype。但全部的对象其实能够视为一组键值对的集合,因此JavaScript中的对象能够返回到Objective-C中当作NSDictionary类型进行访问。
JSValue *obj =[context evaluateScript:@"var jsObj = { number:7, name:'Ider' }; jsObj"]; NSLog(@"%@, %@", obj[@"name"], obj[@"number"]); NSDictionary *dic = [obj toDictionary]; NSLog(@"%@, %@", dic[@"name"], dic[@"number"]); //Output: // Ider, 7 // Ider, 7
一样的,NSDicionary和NSMutableDictionary传入到JSContext以后也能够直接当对象来调用:
NSDictionary *dic = @{@"name": @"Ider", @"#":@(21)}; context[@"dic"] = dic; [context evaluateScript:@"log(dic.name, dic['#'])"]; //OutPut: // Ider // 21
JavaScript能够脱离prototype继承彻底用JSON来定义对象,可是Objective-C编程里可不能脱离类和继承了写代码。因此JavaScriptCore就提供了JSExport做为两种语言的互通协议。JSExport中没有约定任何的方法,连可选的(@optional)都没有,可是全部继承了该协议(@protocol)的协议(注意不是Objective-C的类(@interface))中定义的方法,均可以在JSContext中被使用。语言表述起来有点绕,仍是用例子来讲明会更明确一点。
@protocol PersonProtocol <JSExport> @property (nonatomic, retain) NSDictionary *urls; - (NSString *)fullName; @end @interface Person :NSObject <PersonProtocol> @property (nonatomic, copy) NSString *firstName; @property (nonatomic, copy) NSString *lastName; @end; @implementation Person @synthesize firstName, lastName, urls; - (NSString *)fullName { return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName]; } @end
在上边的代码中,定义了一个PersonProtocol,并让它继承了神秘的JSExport协议,在新定义的协议中约定urls属性和fullName方法。以后又定义了Person类,除了让它实现PersonProtocol外,还定义了firstName和lastName属性。而fullName方法返回的则是两部分名字的结合。
下边就来建立一个Person对象,而后传入到JSContext中并尝试使用JavaScript来访问和修改该对象。
// initialize person object Person *person = [[Person alloc] init]; context[@"p"] = person; person.firstName = @"Ider"; person.lastName = @"Zheng"; person.urls = @{@"site": @"http://www.iderzheng.com"}; // ok to get fullName [context evaluateScript:@"log(p.fullName());"]; // cannot access firstName [context evaluateScript:@"log(p.firstName);"]; // ok to access dictionary as object [context evaluateScript:@"log('site:', p.urls.site, 'blog:', p.urls.blog);"]; // ok to change urls property [context evaluateScript:@"p.urls = {blog:'http://blog.iderzheng.com'}"]; [context evaluateScript:@"log('-------AFTER CHANGE URLS-------')"]; [context evaluateScript:@"log('site:', p.urls.site, 'blog:', p.urls.blog);"]; // affect on Objective-C side as well NSLog(@"%@", person.urls); //Output: // Ider Zheng // undefined // undefined // site: // http://www.iderzheng.com // blog: // undefined // -------AFTER CHANGE URLS------- // site: // undefined // blog: // http://blog.iderzheng.com // { // blog = "http://blog.iderzheng.com"; // }
从输出结果不难看出,当访问firstName和lastName的时候给出的结果是undefined,由于它们跟JavaScript没有JSExport的联系。但这并不影响从fullName()中正确获得两个属性的值。和以前说过的同样,对于NSDictionary类型的urls,能够在JSContext中当作对象使用,并且还能够正确地给urls赋予新的值,并反映到实际的Objective-C的Person对象上。
JSExport不只能够正确反映属性到JavaScript中,并且对属性的特性也会保证其正确,好比一个属性在协议中被声明成readonly,那么在JavaScript中也就只能读取属性值而不能赋予新的值。
对于多参数的方法,JavaScriptCore的转换方式将Objective-C的方法每一个部分都合并在一块儿,冒号后的字母变为大写并移除冒号。好比下边协议中的方法,在JavaScript调用就是:doFooWithBar(foo, bar);
@protocol MultiArgs <JSExport> - (void)doFoo:(id)foo withBar:(id)bar; @end
若是但愿方法在JavaScript中有一个比较短的名字,就须要用的JSExport.h中提供的宏:JSExportAs(PropertyName, Selector)。
@protocol LongArgs <JSExport> JSExportAs(testArgumentTypes, - (NSString *)testArgumentTypesWithInt:(int)i double:(double)d boolean:(BOOL)b string:(NSString *)s number:(NSNumber *)n array:(NSArray *)a dictionary:(NSDictionary *)o ); @end
好比上边定义的协议中的方法,在JavaScript就只要用testArgumentTypes(i, d, b, s, n, a, dic);来调用就能够了。
虽然JavaScriptCore框架尚未官方编程指南,可是在JSExport.h文件中对神秘协议的表述仍是比较详细的,其中有一条是这样描述的:
By default no methods or properties of the Objective-C class will be exposed to JavaScript, however methods and properties may explicitly be exported. For each protocol that a class conforms to, if the protocol incorporates the protocol JSExport, then the protocol will be interpreted as a list of methods and properties to be exported to JavaScript.
这里面有个incorporate一词值得推敲,通过验证只有直接继承了JSExport的自定义协议(@protocol)才能在JSContext中访问到。也就是说好比有其它的协议继承了上边的PersonProtocol,其中的定义的方法并不会被引入到JSContext中。从源码中也能看出JavaScriptCore框架会经过class_copyProtocolList方法找到类所遵循的协议,而后再对每一个协议经过protocol_copyProtocolList检查它是否遵循JSExport协议进而将方法反映到JavaScript之中。
对于自定义的Objective-C类,能够经过以前的方式自定义继承了JSExport的协议来实现与JavaScript的交互。对于已经定义好的系统类或者从外部引入的库类,她们都不会预先定义协议提供与JavaScript的交互的。好在Objective-C是能够在运行时实行对类性质的修改的。
好比下边的例子,就是为UITextField添加了协议,让其能在JavaScript中能够直接访问text属性。该接口以下:
@protocol JSUITextFieldExport <JSExport> @property(nonatomic,copy) NSString *text; @end
以后在经过class_addProtocol为其添加上该协议:
- (void)viewDidLoad { [super viewDidLoad]; textField.text = @"7"; class_addProtocol([UITextField class], @protocol(JSUITextFieldExport)); }
为一个UIButton添加以下的事件,其方法只要是将textField传入到JSContext中而后读取其text值,自增1后从新赋值:
- (IBAction)pressed:(id)sender { JSContext *context = [[JSContext alloc] init]; context[@"textField"] = textField; NSString *script = @"var num = parseInt(textField.text, 10);" "++num;" "textField.text = num;"; [context evaluateScript:script]; }
一、建立环境 JSContext *context=[[JSContext alloc] init]; 二、调用evaluateScript建立JavaScript代码 [context evaluateScript:@"var num=5+5"]; [context evaluateScript:@"var names=['Lusieke','Lemon','Honey']"]; [context evaluateScript:@"var triple=function(value){return value * 3}"]; [context evaluateScript:@"var sum=function(num1,num2){return num1+num2}"]; 三、任何出自JSContext的值都被包裹在一个JSValue对象中。像JavaScript这样的动态语言须要一个动态类型(var),因此JSValue包装了每个可能的JavaScript值:字符、数字、数组、对象和方法。JSValue包括一系列方法用于访问其可能的值以保证有正确的Foundation类型 JSValue *tripNum=[context evaluateScript:@"num"]; //输出10,即定义的JavaScript脚本中var num=5+5的值 NSLog(@"triple :%d",[tripNum toInt32]); JSValue *names=[context evaluateScript:@"names"]; //输出names数组的值 NSLog(@"names :%@",[names toArray]); 四、JSContext和JSValue实例使用下标的方法,咱们能够很容易的访问到咱们以前建立的context的任何值。JSContext须要一个字符串下标,而JSValue容许字符串或整数下标来获得里面的对象和数组 JSValue *names=context[@"names"]; JSValue *initialName=names[0]; NSLog(@"The first name :%@",[initialName toString]); 五、如何调用定义的JavaScript函数?JSValue包装了一个JavaScript函数,咱们能够在Object-c代码中使用Foundation类型做为参数来直接调用该函数 JSContext *context=[[JSContext alloc] init]; [context evaluateScript:@"var sum = function sum(num1,num2){return num1+num2;}"]; JSValue *sumFunc=[context[@"sum"] callWithArguments:@[@2,@3]]; NSLog(@"2+3的和=%d",[sumFunc toInt32]); 输出内容: 2015-03-12 11:23:32.787 javaScriptCore[742:140647] 2+3的和=5 程序中JSValue类型的变量context[@"sum"]经过callWithArguments:传递Foundation类型的变量@2,@3来调用JavaScript函数sum,并输出计算后的值
#References:
http://www.2cto.com/kf/201503/381906.html
http://blog.iderzheng.com/introduction-to-ios7-javascriptcore-framework/
http://blog.iderzheng.com/ios7-objects-management-in-javascriptcore-framework/