javascript与Objective-C的交互

大城市里の小女人 提交于 2019-11-28 22:31:08

在iOS开发中, 苹果api味我们提供了多种javascript和Objective-C交互的方法, 使用还是比较简单的.

1. 普通的方式实现javascript和Objective-C交互

1.1 oc原生代码调用js代码

通过webView的stringByEvaluatingJavaScriptFromString:方法调用js代码. 此方法可以无限制的执行任何的js代码.

/**
     原生调用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';"];

1.2 通过js调用js代码

普通的方法是实现代理协议, 在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;
}

2. 使用JavaScriptCore框架实现javascript和Objective-C的交互

2.1 原生代码调用js代码

(1), 获取js上下文, 通过evaluateScript:方法调用js代码.方法一:

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代码方法二:

JSValue *jsFunc = context[@"showAlert"];
JSValue *result = [jsFunc callWithArguments:@[@"message from JSValue"]];

2.2 js代码调用oc原生代码

//使用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-------");
    };

3. javascript和Objective-C交互使用的其它方法

3.1 NSURL类中

  • 通过NSURLRequest类获取NSURL对象[request URL].
  • scheme: url的scheme表示url的前缀.
  • query: url的query表示url的其它参数.

4. JSContext和JSValue

4.1 JSCOntext提供的一些基本方法

// 执行一段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);
};

4.2 JSValue基本

JSVirtualMachine为JavaScript的运行提供了底层资源,JSContext就为其提供着运行环境,通过- (JSValue *)evaluateScript:(NSString *)script;方法就可以执行一段JavaScript脚本,并且如果其中有方法、变量等信息都会被存储在其中以便在需要的时候使用。而JSContext的创建都是基于JSVirtualMachine:- (id)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;,如果是使用- (id)init;进行初始化,那么在其内部会自动创建一个新的JSVirtualMachine对象然后调用前边的初始化方法。

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    

4.3基本数据类型转换

// 基本数据类型转换
    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也可以通过下标直接取值和赋值。

    //可以存一个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
     )
     */

4.4方法的转换

各种数据类型可以转换,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]];

4.5使用block注意事项

从之前的例子和介绍应该有体会到Block在JavaScriptCore中起到的强大作用,它在JavaScript和Objective-C之间的转换 建立起更多的桥梁,让互通更方便。但是要注意的是无论是把Block传给JSContext对象让其变成JavaScript方法,还是把它赋给exceptionHandler属性,在Block内都不要直接使用其外部定义的JSContext对象或者JSValue,应该将其当做参数传入到Block中,或者通过JSContext的类方法+ (JSContext *)currentContext;来获得。否则会造成循环引用使得内存无法被正确释放。

4.6异常处理

Objective-C的异常会在运行时被Xcode捕获,而在JSContext中执行的JavaScript如果出现异常,只会被JSContext捕获并存储在exception属性上,而不会向外抛出。时时刻刻检查JSContext对象的exception是否不为nil显然是不合适,更合理的方式是给JSContext对象设置exceptionHandler,它接受的是^(JSContext *context, JSValue *exceptionValue)形式的Block。其默认值就是将传入的exceptionValue赋给传入的context的exception属性

4.7键值对编程—Dictionary

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

5. JSExport协议

5.1 JSExport协议基本使用

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之中。

5.2对已定义类扩展协议— class_addProtocol

对于自定义的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];
}

6. 通过原生代码创建javascript的上下文环境并执行javascript

1、创建环境 
 
JSContext *context=[[JSContext alloc] init];
 
2、调用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}"];
3、任何出自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]);
 
4、JSContext和JSValue实例使用下标的方法,我们可以很容易的访问到我们之前创建的context的任何值。JSContext需要一个字符串下标,而JSValue允许字符串或整数下标来得到里面的对象和数组
 
 
 JSValue *names=context[@"names"];
    JSValue *initialName=names[0];
    NSLog(@"The first name :%@",[initialName toString]);
5、如何调用定义的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/

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!