JavaScriptCore Framework

我們在 Mac OS X 上面,也可以透過 C API,要求 WebView 執行Javascript。我們可以使用 JavaScriptCore Framework。

先簡單示範一下。如果我們想要簡單改一下 window.location.href

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);

JavaScriptCore 簡介

JavaScriptCore 是 WebKit 的 JavaScript 引擎,一般來說,在 Mac OS X上,我們想要製作各種網頁與 Native API 程式互動的功能,大概不會選擇使用 JavaScriptCore,因為現在寫 Mac OS X 的桌面應用程式,多半會直接選擇使用 Objective-C 語言與 Cocoa API,各種需要的功能,都有像在前面提到的 Objective-C 方案-使用 WebKit Framework 中的 WebScriptObject 與各種 DOM 物件。

在 Objective-C 程式中當然可以使用 JavaScriptCore 所提供的 C API,但是在實際撰寫程式時,遠比使用 Objective-C 呼叫麻煩-你的主要controller 物件還是用 Objective-C 撰寫的,如果直接用 JavaScriptCore 產生 JavaScriptCore 可以呼叫的 callback,這些 callback C function還是會需要向 Objective-C 物件要資料,等於是多繞了一圈。

然而如果真的有需要的話,我們的確可以混用 JavaScriptCore 與 WebKit 的 Objective-C API,每個 WebScriptObject 中,都可以用 JSObject 這個 method 取得對應的 JSObjectRef。就跟許多 Mac OS X 或 iOS 上的 framework 一樣,Objective-C 物件中往往包含 C 的 API,我們於是可以從 UIColor 得到CGColor,從 UIImage 得到 CGImage,在 WebScriptObject 中,就是JSObjectRef

不過,在 WebKitGTK+ 上,由於 GTK 本身是 C API,所以透過JavaScriptCore,看來就變成 GTK 應用程式與網頁內的 JS程式相互呼叫、傳遞資料最重要的方法(不太熟悉GTK,所以不太確定還有哪些其他的方法)。

在 GTK 網站上有一個簡單的 範例程式,除了示範怎樣產生一個新 Window,裡頭放一個 WebView開始執行外,還包括怎樣產生一個網頁中 JavaScript 可以呼叫的 C function,並且將這個 C function 註冊到成 JavaScript 的 window物件的某個成員function。看這個程式還頂有趣的-絕大部分程式的命名規則與風格,都是屬於GTK 的風格,但是在呼叫到 JavaScriptCore 的時候,卻又是蘋果的CoreFoundation 的風格。

JavaScriptCore 雖然是 WebKit 的 JS 引擎,但是其實並不一定需要WebKit。 JavaScriptCore 中所有的 API 都是對著一個 context操作,我們要操控 WebView,就是對著 WebView 中某個 frame 的 global context 操作( [webFrame globalContext] ),而這個 context可以不需要有一個 WebFrame物件就可以自己產生,我們可以在完全沒有圖形介面、不用到 Web排版引擎的狀況下,產生、執行 JavaScript 程式碼。

所以可以看到,不少專案將這個引擎從 WebKit 中拆分出來,另外橋接其他的 library,在 GTK 上就有 SEED,讓你可以用 JavaScript 呼叫 GObject,用 JavaScript 寫 GTK 應用程式。在 Mac OS X 上則有橋接 JavaScript 與各種 Cocoa 物件的專案,像是 JSCocoa,以及以 JSCocoa 為基礎,作為代替 AppleScript 的 JSTalk,後來則出現了 CocoaScript,像如果你要寫 Sketch的 plug-in,就會用到 CocoaScript 語言。

不過,在這便只大概講一下用 WebView 開啟的網頁,怎麼透過 JavaScriptCore 呼叫 C Function。

JavaScriptCore 裡頭的物件

我對 JavaScriptCore 這個東西不算熟,就像前面說的,真的在寫 Cocoa 程式往往直接呼叫 WebScriptObject。JavaScriptCore 裡頭有幾種基本的資料:

  • JSGlobalContextRef :執行 JavaScript 的 context
  • JSValueRef :在 JavaScript 中所使用的各種資料,包括字串、數字以及 function,都會包裝成 Value,我們可以從數字、JSStringRef 或 JSObject 產生 JSValueRef,也可以轉換回來。需要特別注意的是,JS 裡頭的 null 也是一個 JSValueRef(JSValueMakeUndefined 與 JSValueMakeNull)。
  • JSStringRef :JavaScriptCore 使用的字串。用完記得要 release。
  • JSObjectRef :JavaScript Array、Function 等。

來寫點程式:

- (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);
}

因為每次重新載入網頁,JavaScript 裡頭的 window這個物件的內容就會更新一次,所以我們要等待 WebView 告訴我們應該要更新windowObject 的時候,我們才做我們要做的事情-在 window中加入一個可以讓 JavaScript 呼叫的 C function。我們首先要做兩件事情,第一是取得 WebFrame 裡頭的 globalContext[frame globalContext] ),還有 windowObject 這個Objective-C 物件裡頭的 JSObject[windowObject JSObject] )。

接著,我們要用 C 產生一個 JavaScript function 物件,在這邊用的是 JSObjectMakeFunctionWithCallback,代表我們想要產生一個可以用來呼叫 C Function 的 JavaScript function,我們要提供這個 JavaScript function的名稱,還有對應到哪個 C function。如果我們想產生的 JavaScript function 不需要呼叫 C function,可以改用 JSObjectMakeFunction;最後,我們把這個function 物件,註冊給 windowObject 的 JSObject上,於是我們現在便可以在 JS 中呼叫 window.myFunc() 了。

來個綜合練習-我們現在在 JS 中傳入兩個數字,透過 C function加完之後,執行一段 JS callback。我們的 JS 程式這麼寫-

window.myFunc(1, 1, function(result) {
    var main = document.getElementById('main');
    main.innerText = result;
});

在 myFunc 中,我們來練習一下 JavaScriptCore 裡頭的一些東西:

JSValueRef myFunc(JSContextRef ctx, JSObjectRef function,
    JSObjectRef thisObject, size_t argumentCount,
    const JSValueRef arguments[], JSValueRef* exception)
{
    if (argumentCount < 3) {
        JSStringRef string = JSStringCreateWithUTF8CString("UTF8String");
        JSValueRef result = JSValueMakeString(ctx, string);
        JSStringRelease(string);
        return result;
    }
    if (!JSValueIsNumber(ctx, arguments[0])) {
        JSStringRef string =
        JSStringCreateWithCFString((CFStringRef)@"NSString");
        JSValueRef result = JSValueMakeString(ctx, string);
        JSStringRelease(string);
        return result;
    }
    if (!JSValueIsNumber(ctx, arguments[1])) {
        return JSValueMakeNumber(ctx, 42.0);
    }
    if (!JSValueIsObject(ctx, arguments[2])) {
        return JSValueMakeNull(ctx);
    }

    double leftOperand = JSValueToNumber(ctx, arguments[0], exception);
    double rightOperand = JSValueToNumber(ctx, arguments[1], exception);
    JSObjectRef callback = JSValueToObject(ctx, arguments[2], exception);
    JSValueRef result = JSValueMakeNumber(ctx, leftOperand + rightOperand);
    JSValueRef myArguments[1] = {result};
    JSObjectCallAsFunction(ctx, callback, thisObject, 1,
        myArguments, exception); return JSValueMakeNull(ctx);
}

我們希望至少要有三個參數傳進來,前兩個參數是數字,最後一個參數是 JSObject,如果不是的話,就簡單回傳一點東西-在這邊可以看到, JSStringRef除了可以用 UTF8 字串產生,也可以從 CFString 產生。

我們接下來把 JSValue 轉成 double,簡單做個加法,最後用 JSObjectCallAsFunction,執行 JavaScript callback-其實這邊還應該要用 JSObjectIsFunction,來檢查一下這個 JSObject 到底是不是 function才是。

results matching ""

    No results matching ""