javascript與Objective-C的交互

在iOS開發中, 蘋果api味咱們提供了多種javascript和Objective-C交互的方法, 使用仍是比較簡單的.javascript

1. 普通的方式實現javascript和Objective-C交互

1.1 oc原生代碼調用js代碼

經過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';"];

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代碼.方法一: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"]];

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對象而後調用前邊的初始化方法。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    

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也能夠經過下標直接取值和賦值。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
     )
     */

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

一、建立環境 
 
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/

相關文章
相關標籤/搜索