原文转自: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 里头,想要知道目前的网页位置,会这麽写:

1varlocation = window.location.href;

用 ObjC 就可以这麽调用:

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

如果我们要设定 window.location.href,要求开启另外一个网页,在 JS 里头:

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

Obj C:

1[[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 里头这样写:

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

Obj C 里头就会变成这样:

12345678WebScriptObject *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,内容是:

123functionx(x) {    returnx + 1;}

所以在 Obj C 中可以这样写;

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

接下来我们就可以调用 window.x():

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

由于在 JS 中,每个 funciton 其实都是对象,所以我们还可以直接取得 window.x 叫这个对象执行自己。在 JS 里头如果这样写:

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

Obj C 中便是这样:

12WebScriptObject *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 里头会这样写:

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

Obj C:

12DOMDocument *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 对象,会这样写:

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

如此一来,只要调用 window.controller,就可以调用我们的 Obj C 对象。假如我们的 Obj C Class 里头有这些成员变数:

01020304050607080910111213@interfaceMyController :NSObject{    IBOutletWebView *webView;    IBOUtlet NSWindow*window;    NSString*stringValue;    NSIntegernumberValue;    NSArray*arrayValue;    NSDate*dateValue;    NSDictionary*dictValue;    NSRectframeValue;}@end

指定一下 Value:

123456stringValue =@"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 读读看:

010203040506070809101112varc = window.controller;varmain = document.getElementByIdx_x('main');varHTML ='';if(c) {    HTML +='

'+ c.stringValue +'

';    HTML +='

'+ c.numberValue +'

';    HTML +='

'+ c.arrayValue +'

';    HTML +='

'+ c.dateValue +'

';    HTML +='

'+ c.dictValue +'

';    HTML +='

'+ c.frameValue +'

';    main.innerHTML = HTML;}

结果如下:

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:

1234567+ (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- (void)setA:(id)a b:(id)b c:(id)c;

在 JS 中就这麽调用:

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

实在有点丑。所以 WebKit 提供一个方法,可以让我们把某个 Obj C selector 变成好看一点的 JS function。我们要实作 webScriptNameForSelector:

1234567+ (NSString*)webScriptNameForSelector:(SEL)selector{    if(selector ==@selector(setA:b:c:)) {        return@"setABC";    }    returnnil;}

以后就可以这麽调用:

1controller.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
console.log(controller.setABC);

我們可以從結果看到:

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
- (void)numberWithA:(id)a plusB:(id)b callback:(id)callback
{
    NSInteger result = [a integerValue] + [b integerValue];
    [callback callWebScriptMethod:@"call" withArguments:[NSArray arrayWithObjects:callback, [NSNumber numberWithInteger:result], nil]];
}

JS 裡頭就可以這樣呼叫:

1
2
3
4
window.controller.numberWithA_plusB_callback_(1, 2, function(result) {
    var main = document.getElementByIdx_x('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
JSGlobalContextRef globalContext = [[webView mainFrame] globalContext];
JSValueRef exception = NULL;
JSStringRef script = JSStringCreateWithUTF8CString("window.location.href='http://zonble.net'");
JSEvaluateScript(globalContext, script, NULL, NULL, 0, &exception);
JSStringRelease(script);

如果我們想要讓 WebView 裡頭的 JS,可以呼叫我們的 C Function:

1
2
3
4
5
6
7
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
JSValueRef myFunc(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
{
    return JSValueMakeNumber(ctx, 42);
}
Be Sociable, Share!
arrow
arrow
    全站熱搜

    戮克 發表在 痞客邦 留言(0) 人氣()