[iOS] 使用UIWebView時objective-c與javascript互相調用 1


原文轉自:http://zonble.net/archives/2010_09/1385.php
在寫 JavaScript 的時候,可以使用一個叫做 window 的對象,像是我們想要從現在的網頁跳到另外一個網頁的時候,就會去修改 window.location.href 的位置;在我們的 Objective C 程序碼中,如果我們可以取得指定的 WebView 的指標,也就可以拿到這個出現在 JavaScript 中的 window 對象,也就是 [webView windowScriptObject]。

這個對象就是 WebView 里頭的 JS 與我們的 Obj C 程序之間的橋樑-window 對象可以取得網頁里頭所有的 JS 函數與對象,而如果我們把一個 Obj C 對象設定成 windowScriptObject 的 value,JS 也便可以調用 Obj C 對象的 method。於是,我們可以在 Obj C 程序里頭要求 WebView 執行一段 JS,也可以反過來讓 JS 調用一段用 Obj C 實作的功能。

※ 用 Objective C 取得與設定 JavaScript 對象

要從 Obj C 取得網頁中的 JavaScript 對象,也就是對 windowScriptObject 做一些 KVC 調用,像是 valueForKey: 與 valueForKeyPath:。如果我們在 JS 里頭,想要知道目前的網頁位置,會這麽寫:

1
  1. varlocation = window.location.href;  
varlocation = window.location.href;

用 ObjC 就可以這麽調用:

1
  1.       
  2. NSString*location = [[webView windowScriptObject] valueForKeyPath:@"location.href"];  
	
NSString*location = [[webView windowScriptObject] valueForKeyPath:@"location.href"];

如果我們要設定 window.location.href,要求開啟另外一個網頁,在 JS 里頭:

1
  1.       
  2. window.location.href ='http://spring-studio.net';  
	
window.location.href ='http://spring-studio.net';

Obj C:

1
 
  1. [[webView windowScriptObject] setValue:@"http://spring-studio.net"forKeyPath:@"location.href"];  
[[webView windowScriptObject] setValue:@"http://spring-studio.net"forKeyPath:@"location.href"];

由於 Obj C 與 JS 本身的語言特性不同,在兩種語言之間相互傳遞東西之間,就可以看到兩者的差別-

  • JS 雖然是 OO,但是並沒有 class,所以將 JS 對象傳到 Obj C 程序里頭,除了基本字串會轉換成 NSString、基本數字會轉成 NSNumber,像是 Array 等其他對象,在 Obj C 中,都是 WebScriptObject 這個 Class。意思就是,JS 的 Array 不會幫你轉換成 NSArray。
  • 從 JS 里頭傳一個空對象給 Obj C 程序,用的不是 Obj C 里頭原本表示「沒有東西」的方式,像是 NULL、nil、NSNull 等,而是專屬 WebKit 使用的 WebUndefined。

所以,如果我們想要看一個 JS Array 里頭有什麽東西,就要先取得這個對象里頭叫做 length 的 value,然后用 webScriptValueAtIndex: 去看在該 index 位置的內容。假如我們在 JS 里頭這樣寫:

1
2
3
4
 
  1. varJSArray = {'zonble','dot','net'};  
  2. for(vari = 0; i < JSArray.length; i++) {  
  3.     console.log(JSArray[i]);  
  4. }  
varJSArray = {'zonble','dot','net'};
for(vari = 0; i < JSArray.length; i++) {
    console.log(JSArray[i]);
}

 

Obj C 里頭就會變成這樣:

1
2
3
4
5
6
7
8
 
  1. WebScriptObject *obj = (WebScriptObject *)JSArray;  
  2. NSUIntegercount = [[obj valueForKey:@"length"] integerValue];  
  3. NSMutableArray*a = [NSMutableArrayarray];  
  4. for(NSUIntegeri = 0; i < count; i++) {  
  5.     NSString*item = [obj webScriptValueAtIndex:i];  
  6.     NSLog(@"item:%@", item);  
  7. }  
WebScriptObject *obj = (WebScriptObject *)JSArray;
NSUIntegercount = [[obj valueForKey:@"length"] integerValue];
NSMutableArray*a = [NSMutableArrayarray];
for(NSUIntegeri = 0; i < count; i++) {
    NSString*item = [obj webScriptValueAtIndex:i];
    NSLog(@"item:%@", item);
}

 

※ 用 Objective C 調用 JavaScript function

要用 Obj C 調用網頁中的 JS function,大概有幾種方法。第一種是直接寫一段跟你在網頁中會撰寫的 JS 一模一樣的程序,叫 windowScriptObject 用 evaluateWebScript: 執行。例如,我們想要在網頁中產生一個新的 JS function,內容是:

1
2
3
 
  1. functionx(x) {  
  2.     returnx + 1;  
  3. }  
functionx(x) {
    returnx + 1;
}

 

所以在 Obj C 中可以這樣寫;

1
  1.       
  2. [[webView windowScriptObject] evaluateWebScript:@"function x(x) { return x + 1;}"];  
	
[[webView windowScriptObject] evaluateWebScript:@"function x(x) { return x + 1;}"];

接下來我們就可以調用 window.x():

1
2
 
  1. NSNumber*result = [[webView windowScriptObject] evaluateWebScript:@"x(1)"];  
  2. NSLog(@"result:%d", [result integerValue]);// Returns 2  
NSNumber*result = [[webView windowScriptObject] evaluateWebScript:@"x(1)"];
NSLog(@"result:%d", [result integerValue]);// Returns 2

 

由於在 JS 中,每個 funciton 其實都是對象,所以我們還可以直接取得 window.x 叫這個對象執行自己。在 JS 里頭如果這樣寫:

1
  1.       
  2. window.x.call(window.x, 1);  
	
window.x.call(window.x, 1);

Obj C 中便是這樣:

1
2
 
  1. WebScriptObject *x = [[webView windowScriptObject] valueForKey:@"x"];  
  2. NSNumber*result = [x callWebScriptMethod:@"call"withArguments:[NSArrayarrayWithObjects:x, [NSNumbernumberWithInt:1],nil]];  
WebScriptObject *x = [[webView windowScriptObject] valueForKey:@"x"];
NSNumber*result = [x callWebScriptMethod:@"call"withArguments:[NSArrayarrayWithObjects:x, [NSNumbernumberWithInt:1],nil]];

 

這種讓某個 WebScriptObject 自己執行自己的寫法,其實比較不會用於從 Obj C 調用 JS 這一端,而是接下來會提到的,由 JS 調用 Obj C,因為這樣 JS 就可以把一個 callback function 送到 Obj C 程序里頭。

如果我們在做網頁,我們只想要更新網頁中的一個區塊,就會利用 AJAX 的技巧,只對這個區塊需要的資料,對 server 發出 request,並且在 request 完成的時候,要求執行一段 callback function,更新這一個區塊的顯示內容。從 JS 調用 Obj C也可以做類似的事情,如果 Obj C 程序里頭需要一定時間的運算,或是我們可能是在 Obj C 里頭抓取網路資料,我們便可以把一個 callback function 送到 Obj C 程序裡,要求 Obj C 程序在做完工作后,執行這段 callback function。

※ DOM

WebKit 里頭,所有的 DOM 對象都繼承自 DOMObject,DOMObject 又繼承自 WebScriptObject,所以我們在取得了某個 DOM 對象之后,也可以從 Obj C 程序中,要求這個 DOM 對象執行 JS 程序。

假如我們的網頁中,有一個 id 叫做 “#s” 的文字輸入框(text input),而我們希望現在鍵盤輸入的焦點放在這個輸入框上,在 JS 里頭會這樣寫:

1
 
  1. document.querySelector('#s').focus();  
document.querySelector('#s').focus();

Obj C:

1
2
 
  1. DOMDocument *document = [[webView mainFrame] DOMDocument];  
  2. [[document querySelector:@"#s"] callWebScriptMethod:@"focus"withArguments:nil];  
DOMDocument *document = [[webView mainFrame] DOMDocument];
[[document querySelector:@"#s"] callWebScriptMethod:@"focus"withArguments:nil];

※ 用 JavaScript 存取 Objective C 的 Value

要讓網頁中的 JS 程序可以調用 Obj C 對象,方法是把某個 Obj C 對象注冊成 JS 中 window 對象的屬性。之后,JS 便也可以調用這個對象的 method,也可以取得這個對象的各種 Value,只要是 KVC 可以取得的 Value,像是 NSString、NSNumber、NSDate、NSArray、NSDictionary、NSValue…等。JS 傳 Array 到 ObjC 時,還需要特別做些處理才能變成 NSArray,從 Obj C 傳一個 NSArray 到 JS 時,會自動變成 JS Array。

首先我們要注意的是將 Obj C 對象注冊給 window 對象的時機,由於每次重新載入網頁,window 對象的內容都會有所變動-畢竟每個網頁都會有不同的 JS 程序,所以,我們需要在適當的時機做這件事情。我們首先要指定 WebView 的 frame loading delegate(用 setFrameLoadDelegate:),並且實作 webView:didClearWindowObject:forFrame:,WebView 只要更新了 windowScriptObject,就會調用這一段程序。假如我們現在要讓網頁中的 JS 可以使用目前的 controller 對象,會這樣寫:

1
2
3
4
 
  1. - (void)webView:(WebView *)sender didClearWindowObject:(WebScriptObject *)windowObject forFrame:(WebFrame *)frame  
  2. {  
  3.     [windowObject setValue:selfforKey:@"controller"];  
  4. }  
- (void)webView:(WebView *)sender didClearWindowObject:(WebScriptObject *)windowObject forFrame:(WebFrame *)frame
{
    [windowObject setValue:selfforKey:@"controller"];
}

 

如此一來,只要調用 window.controller,就可以調用我們的 Obj C 對象。假如我們的 Obj C Class 里頭有這些成員變數:

01
02
03
04
05
06
07
08
09
10
11
12
13
 
  1. @interfaceMyController :NSObject  
  2. {  
  3.     IBOutletWebView *webView;  
  4.     IBOUtlet NSWindow*window;  
  5.     NSString*stringValue;  
  6.     NSIntegernumberValue;  
  7.     NSArray*arrayValue;  
  8.     NSDate*dateValue;  
  9.     NSDictionary*dictValue;  
  10.     NSRectframeValue;  
  11. }  
  12. @end  
@interfaceMyController :NSObject
{
    IBOutletWebView *webView;
    IBOUtlet NSWindow*window;
    NSString*stringValue;
    NSIntegernumberValue;
    NSArray*arrayValue;
    NSDate*dateValue;
    NSDictionary*dictValue;
    NSRectframeValue;
}
@end

 

指定一下 Value:

1
2
3
4
5
6
 
  1. stringValue =@"string";  
  2. numberValue = 24;  
  3. arrayValue = [[NSArrayarrayWithObjects:@"text", [NSNumbernumberWithInt:30],nil] retain];  
  4. dateValue = [[NSDatedate] retain];  
  5. dictValue = [[NSDictionarydictionaryWithObjectsAndKeys:@"value1",@"key1",@"value2",@"key2",@"value3",@"key3",nil] retain];  
  6. frameValue = [window frame];  
stringValue =@"string";
numberValue = 24;
arrayValue = [[NSArrayarrayWithObjects:@"text", [NSNumbernumberWithInt:30],nil] retain];
dateValue = [[NSDatedate] retain];
dictValue = [[NSDictionarydictionaryWithObjectsAndKeys:@"value1",@"key1",@"value2",@"key2",@"value3",@"key3",nil] retain];
frameValue = [window frame];

 

用 JS 讀讀看:

01
02
03
04
05
06
07
08
09
10
11
12
 
  1. varc = window.controller;  
  2. varmain = document.getElementById('main');  
  3. varHTML ='';  
  4. if(c) {  
  5.     HTML +='<p>'+ c.stringValue +'<p>';  
  6.     HTML +='<p>'+ c.numberValue +'<p>';  
  7.     HTML +='<p>'+ c.arrayValue +'<p>';  
  8.     HTML +='<p>'+ c.dateValue +'<p>';  
  9.     HTML +='<p>'+ c.dictValue +'<p>';  
  10.     HTML +='<p>'+ c.frameValue +'<p>';  
  11.     main.innerHTML = HTML;  
  12. }  
varc = window.controller;
varmain = document.getElementById('main');
varHTML ='';
if(c) {
    HTML +='<p>'+ c.stringValue +'<p>';
    HTML +='<p>'+ c.numberValue +'<p>';
    HTML +='<p>'+ c.arrayValue +'<p>';
    HTML +='<p>'+ c.dateValue +'<p>';
    HTML +='<p>'+ c.dictValue +'<p>';
    HTML +='<p>'+ c.frameValue +'<p>';
    main.innerHTML = HTML;
}

 

結果如下:

  1. string24text,302010-09-09 00:01:04 +0800{ key1 = value1; key2 = value2; key3 = value3; }NSRect: {{275, 72}, {570, 657}}  
string24text,302010-09-09 00:01:04 +0800{ key1 = value1; key2 = value2; key3 = value3; }NSRect: {{275, 72}, {570, 657}}

不過,如果你看完上面的范例,就直接照做,應該不會直接成功出現正確的結果,而是會拿到一堆 undefined,原因是,Obj C 對象的 Value 預設被保護起來,不會讓 JS 直接存取。要讓 JS 可以存取 Obj C 對象的 Value,需要實作 +isKeyExcludedFromWebScript: 針對傳入的 Key 一一處理,如果我們希望 JS 可以存取這個 key,就回傳 NO:

1
2
3
4
5
6
7
 
  1. + (BOOL)isKeyExcludedFromWebScript:(constchar*)name  
  2. {  
  3.     if(!strcmp(name,"stringValue")) {  
  4.         returnNO;  
  5.     }  
  6.     returnYES;  
  7. }  
+ (BOOL)isKeyExcludedFromWebScript:(constchar*)name
{
    if(!strcmp(name,"stringValue")) {
        returnNO;
    }
    returnYES;
}

 

除了可以讀取 Obj C 對象的 Value 外,也可以設定 Value,相當於在 Obj C 中使用 setValue:forKey:,如果在上面的 JS 程序中,我們想要修改 stringValue,直接調用 c.stringValue = ‘new value’ 即可。像前面提到,在這裡傳給 Obj C 的 JS 對象,除了字串與數字外,class 都是 WebScriptObject,空對象是 WebUndefined。

※ 用 JavaScript 調用 Objective C method

Obj C 的語法沿襲自 SmallTalk,Obj C 的 selector,與 JS 的 function 語法有相當的差異。WebKit 預設的實作是,如果我們要在 JS 調用 Obj C selector,就是把所有的參數往后面擺,並且把所有的冒號改成底線,而原來 selector 如果有底線的話,又要另外處理。假使我們的 controller 對象有個 method,在 Obj C 中寫成這樣:

1
  1.       
  2. - (void)setA:(id)a b:(id)b c:(id)c;  
	
- (void)setA:(id)a b:(id)b c:(id)c;

在 JS 中就這麽調用:

[cpp] view plain copy print ?
  •       
  • controller.setA_b_c_('a','b','c');  
controller.setA_b_c_('a','b','c');
1
controller.setA_b_c_('a','b','c');

實在有點丑。所以 WebKit 提供一個方法,可以讓我們把某個 Obj C selector 變成好看一點的 JS function。我們要實作 webScriptNameForSelector:

1
2
3
4
5
6
7
 
  1. + (NSString*)webScriptNameForSelector:(SEL)selector  
  2. {  
  3.     if(selector ==@selector(setA:b:c:)) {  
  4.         return@"setABC";  
  5.     }  
  6.     returnnil;  
  7. }  
+ (NSString*)webScriptNameForSelector:(SEL)selector
{
    if(selector ==@selector(setA:b:c:)) {
        return@"setABC";
    }
    returnnil;
}

 

以后就可以這麽調用:

1
 
  1. controller.setABC('a','b','c');  
controller.setABC('a','b','c');

我們同樣可以決定哪些 selector 可以給 JS 使用,哪些要保護起來,方法是實作 isSelectorExcludedFromWebScript:。而我們可以改變某個 Obj C selector 在 JS 中的名稱,我們也可以改變某個 value 的 key,方法是實作 webScriptNameForKey:。

有幾件事情需要注意一下:

用 JavaScript 調用 Objective C 2.0 的 property

在上面,我們用 JS 調用 window.controller.stringValue,與設定里頭的 value 時,這邊很像我們使用 Obj C 2.0 的語法,但其實做的是不一樣的事情。用 JS 調用 controller.stringValue,對應到的 Obj C 語法是 [controller valueForKey:@"stringValue"],而不是調用 Obj C 對象的 property。

如果我們的 Obj C 對象有個 property 叫做 stringValue,我們知道,Obj C property 其實會在編譯時,變成 getter/setter method,在 JS 里頭,我們便應該要調用 controller.stringValue() 與 controller.setStringValue_()。

Javascript 中,Function 即對象的特性

JS 的 function 是對象,當一個 Obj C 對象的 method 出現在 JS 中時,這個 method 在 JS 中,也可以或多或少當做對象處理。我們在上面產生了 setABC,也可以試試看把它倒出來瞧瞧:

1
 
  1. console.log(controller.setABC);  
console.log(controller.setABC);

我們可以從結果看到:

  1. function setABC() { [native code] }  
function setABC() { [native code] }

這個 function 是 native code。因為是 native code,所以我們無法對這個 function 調用 call 或是 apply。

另外,在把我們的 Obj C 對象注冊成 window.controller 后,我們會許也會想要讓 controller 變成一個 function 來執行,像是調用 window.controller();或是,我們就只想要產生一個可以讓 JS 調用的 function,而不是整個對象都放進 JS 里頭。我們只要在 Obj C 對象中,實作 invokeDefaultMethodWithArguments:,就可以回傳在調用 window.controller() 時想要的結果。

現在我們可以綜合練習一下。前面提到,由於我們可以把 JS 對象以 WebScriptObject 這個 class 傳入 Obj C 程序,Obj C 程序中也可以要求執行 WebScriptObject 的各項 function。我們假如想把 A 與 B 兩個數字丟進 Obj C 程序里頭做個加法,加完之后出現在網頁上,於是我們寫了一個 Obj C method:

1
2
3
4
5
 
  1. - (void)numberWithA:(id)a plusB:(id)b callback:(id)callback  
  2. {  
  3.     NSIntegerresult = [a integerValue] + [b integerValue];  
  4.     [callback callWebScriptMethod:@"call"withArguments:[NSArrayarrayWithObjects:callback, [NSNumbernumberWithInteger:result],nil]];  
  5. }  
- (void)numberWithA:(id)a plusB:(id)b callback:(id)callback
{
    NSIntegerresult = [a integerValue] + [b integerValue];
    [callback callWebScriptMethod:@"call"withArguments:[NSArrayarrayWithObjects:callback, [NSNumbernumberWithInteger:result],nil]];
}

 

JS 里頭就可以這樣調用:

1
2
3
4
 
  1. window.controller.numberWithA_plusB_callback_(1, 2,function(result) {  
  2.     varmain = document.getElementById('main');  
  3.     main.innerText = result;  
  4. });  
window.controller.numberWithA_plusB_callback_(1, 2,function(result) {
    varmain = document.getElementById('main');
    main.innerText = result;
});

 

※ 其他平台上 WebKit 的實作

除了 Mac OS X,WebKit 這幾年也慢慢移植到其他的作業系統與 framework 中,也或多或少都有 Native API 要求 WebView 執行 Js,以及從 JS 調用 Native API 的機制。

跟 Mac OS X 比較起來,iPhone 上 UIWebView 的公開 API 實在少上許多。想要讓 UIWebView 執行一段 JS,可以透過調用 stringByEvaluatingJavaScriptFromString:,只會回傳字串結果,所以能夠做到的事情也就變得有限,通常大概就拿來取得像 window.title 這些資訊。在 iPhone 上我們沒辦法將某個 Obj C 對象變成 JS 對象,所以,在網頁中觸發了某些事件,想要通知 Obj C 這一端,往往會選擇使用像「zonble://」這類 Customized URL scheme。

ChromeOS 完全以 WebKit 製作使用者介面,不過我們沒辦法在 ChomeOS 上寫我們在這邊所討論的桌面或行動應用程序,所以不在我們討論之列。(順道岔題,ChromeOS 是設計來給 Netbook 使用的作業系統,可是像 Toshiba 都已經用 Android,做出比 Netbook 更小的 Smartbook,而且應用程序更多,ChromeOS 的產品做出來的話,實在很像 Google 拿出兩套東西,自己跟自己對打)。

Android 的 WebView 對象提供一個叫做 addJavascriptInterface() 的 method,可以將某個 Java 對象注冊成 JS 的 window 對象的某個屬性,就可以讓 JS 調用 Java 對象。不過,在調用 Java 對象時,只能夠傳遞簡單的文字、數字,複雜的 JS 對象就沒辦法了。而在 Android 上想要 WebView 執行一段 JS,在文件中沒看到相關資料,網路上面找到的說法是,可以透過 loadUrl(),把某段 JS 用 bookmarklet 的形式傳進去。

在 QtWebKit 里頭,可以對 QWebFrame 調用 addToJavaScriptWindowObject,把某個 QObject 暴露在 JS 環境中,我不清楚 JS 可以傳遞哪些東西到 QObject 里頭就是了。在 QtWebKit 中也可以取得網頁里頭的 DOM 對象(QWebElement
、QWebElementCollection),我們可以對 QWebFrame 還有這些 DOM 對象調用 evaluateJavaScript,執行 Javascript。

GTK 方面,因為是 C API,所以在應用程序與 JS 之間,就不是透過操作包裝好的對象,而是調用 WebKit 里頭 JavaScript Engine 的 C API。

※ JavaScriptCore Framework

我們在 Mac OS X 上面,也可以透過 C API,要求 WebView 執行 Javascript。首先要 import 。如果我們想要簡單改一下 window.location.href:

1
2
3
4
5
 
  1. JSGlobalContextRef globalContext = [[webView mainFrame] globalContext];  
  2. JSValueRef exception = NULL;  
  3. JSStringRef script = JSStringCreateWithUTF8CString("window.location.href='http://spring-studio.net'");  
  4. JSEvaluateScript(globalContext, script, NULL, NULL, 0, &exception);  
  5. JSStringRelease(script);  
JSGlobalContextRef globalContext = [[webView mainFrame] globalContext];
JSValueRef exception = NULL;
JSStringRef script = JSStringCreateWithUTF8CString("window.location.href='http://spring-studio.net'");
JSEvaluateScript(globalContext, script, NULL, NULL, 0, &exception);
JSStringRelease(script);

 

如果我們想要讓 WebView 里頭的 JS,可以調用我們的 C Function:

1
2
3
4
5
6
7
8
 
  1. - (void)webView:(WebView *)sender didClearWindowObject:(WebScriptObject *)windowObject forFrame:(WebFrame *)frame  
  2. {  
  3.     JSGlobalContextRef globalContext = [frame globalContext];  
  4.     JSStringRef name = JSStringCreateWithUTF8CString("myFunc");  
  5.     JSObjectRef obj = JSObjectMakeFunctionWithCallback(globalContext, name, (JSObjectCallAsFunctionCallback)myFunc);  
  6.     JSObjectSetProperty (globalContext, [windowObject JSObject], name, obj, 0,NULL);  
  7.     JSStringRelease(name);  
  8. }  
- (void)webView:(WebView *)sender didClearWindowObject:(WebScriptObject *)windowObject forFrame:(WebFrame *)frame
{
    JSGlobalContextRef globalContext = [frame globalContext];
    JSStringRef name = JSStringCreateWithUTF8CString("myFunc");
    JSObjectRef obj = JSObjectMakeFunctionWithCallback(globalContext, name, (JSObjectCallAsFunctionCallback)myFunc);
    JSObjectSetProperty (globalContext, [windowObject JSObject], name, obj, 0,NULL);
    JSStringRelease(name);
}

 

那麽,只要 JS 調用 window.myFunc(),就可以取得們放在 myFunc 這個 C function 中回傳的結果:

1
2
3
4
 
  1. JSValueRef myFunc(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject,size_targumentCount,constJSValueRef arguments[], JSValueRef* exception)  
  2. {  
  3.     returnJSValueMakeNumber(ctx, 42);  
  4. }  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM