最近更新: 2010-11-15

JavaScript 與 Desktop - Desktop and WebKit

本文是《JavaScript 與 Desktop》系列最後一篇。前兩篇文章中,分別述敘了在 gjs/seed 中呼叫系統函數庫與調用 WebKit 處理圖形化使用介面的工作。

但是在這個架構中,實際上存在了兩個 JavaScript host (host 是 ECMAScript/JavaScript 規範術語,意指 JavaScript 語言解譯器寄宿的環境,故有人將之譯為「宿主」) 。一個是 gjs/seed,另一個便是 WebKit JavaScriptCore 。這兩個 host 都是獨立的環境空間,彼此之間的資源不能直接互通。例如 gjs/seed 這個 host 提供的資源可以載入 DBus 服務,調用 DBus 方法;但是 WebKit JavaScriptCore 並不提供這類資源,所以不能調用 DBus 方法。是以我們需要找出一個互通訊息的途徑,讓這兩個 host的程式碼可以互動。本文將說明其中一種基於事件觸發的途徑。

內外部互動模式

在《JavaScript 與 Desktop - WebKit》一文中,我們讓 gjs/seed 載入 libwebkit 函數庫,透過該函數庫配置了一個 WebKit 環境空間。在該 WebKit 環境中,它包含了一個獨立的 HTML render 與 JavaScript host。在這個自成一格的內部空間中,它本身就能獨自處理 HTML 與相關資源,運作自己的一套 JavaScript host。如果我們想要從外部干涉這個內部空間,或是讓內部空間中的狀況可以為外部所得知,我們就要依靠 libwebkit 所提供的 API 才能實現(WebKit 專案有許多分支,其中有些分支直接提供了內外部互動的 API 。例如 OS X 的 WebKit 或是 Nokia 的 QtWebKit。那些分支提供的特殊 API 不具相容性,不適用於本文的開發環境)。

libwebkit 提供了一些特定的 API 讓我們存取 WebKit 內部的屬性狀態。例如: webkit_web_view_get_title(), webkit_web_view_get_uri(), signal:notify:load-status, signal:title-changed。但這些 API 的用途有限。如果我們要實現更複雜的互動架構,我們需要分兩方面討論。一方面是由外部調用內部資源,另一方面是由內部調用外部資源。

外部調用內部資源

由外部調用內部資源最直接的方式,就是由 gjs 端呼叫 WebKit API 的 execute_script()。這個動作相當於在 WebKit 端呼叫 eval()。舉例來說,如果我想要從 gjs 端要求 WebKit 端顯示一個 alert 視窗,我可以在 gjs 端執行 view.execute_script("alert('hello')")

execute_script() 可以從外部送出任何程式碼給內部執行,但是並不能將結果傳給外部。我們要再配合下一段「內部調用外部資源」的方法。

內部調用外部資源

WebKit 端沒有提供任何讓 WebKit host 內的程式碼接觸到外部資源的正式途徑。所以我們必須思考一下偏門技巧。我首先想到的就是利用 WebKit 中的 Signals 機制,藉由訊號觸發的方式,讓外部傾聽與接收內部訊號送出的訊息,再將其分析為內部調用外部資源的指令。而在 WebKit 提供的 Signals 中,唯一沒有副作用,而且可以傳遞長字串訊息的訊號,則是 title-changedtitle-changed 接受一個任意內容的字串,做為訊號送出時附帶的訊息。

在內部 (WebKit 端),可以藉由設定 document.title 的內容,觸發 title-changed 訊號,並將 document.title 的新值做為訊號送出時附帶的訊息。外部 (gjs端) 只要去傾聽 title-changed 訊號,就可以接收到內部送出的指令請求,再根據請求內容調用外部資源。至於外部資源的回傳結果,可以參考前段的 execute_script() 送回內部。通常我們會採用 Ajax 常見的 callback 設計模式,呼叫內部訊息指定的回呼函數,將外部資源的回傳結果送回。

我在尋找解決方案時,找到《HOWTO Create Python GUIs using HTML》這篇文章。它採用的內外部互動模式,就是本文所採用的模式。如果我的說明不足以令你了解此互動模式的運作方式,請再閱讀該文內容。

實作

結合前兩篇的範例以及本文所述的互動模式,我撰寫了一個 gtk-webkit-2.js。將第一篇文章中調用 DBus 的函數,加到第二篇文章調用 WebKit 處理 GUI 的範例之中,並增加一個 notify 動作。當使用者在 UI 的文字框中輸入關鍵字後,除了原本查詢 flickr 顯示圖片的動作之外,還會在桌面上彈出一個訊息方塊。

pan class="cp">
#!/usr/bin/gjs
// 一、定義 dbus 函數
// see: dbus-notify.js
// link: /archives/14229429.html
const DBus = imports.dbus;
function Notifications() { // 定義代理個體的類別
    this._init();
};
Notifications.prototype = {
    _init: function() {
	DBus.session.proxifyObject (this,
				   'org.freedesktop.Notifications',
				   '/org/freedesktop/Notifications');
    }
};
DBus.proxifyPrototype(Notifications.prototype, // 將介面內容注入代理類別
{   // 描述 org.freedesktop.Notifications 的介面內容 
    name: 'org.freedesktop.Notifications',
    methods: [
        { name: 'GetServerInformation', inSignature: '', outSignature: 'ssss'},
        { name: 'Notify', inSignature: 'susssasa{sv}i', outSignature: 'u' }
    ],
    signals: [
        { name: 'NotificationClosed', inSignature: '', outSignature: 'uu' }
    ]
});
var notifier = new Notifications(); // 建立遠端個體在本地端的代理者


var GLib = imports.gi.GLib;
var Gtk = imports.gi.Gtk;
var WebKit = imports.gi.WebKit;
// apt-get install git1.0-gtk-2.0 gir1.0-webkit-1.0, gir1.0-soup-2.4

GLib.set_prgname('hello webkit 2');
Gtk.init(0, null);

var w = new Gtk.Window();
w.connect("destroy", Gtk.main_quit);
w.resize(300,200);

var view = new WebKit.WebView();
w.add(view);

//var settings = view.get_settings();
//print(settings.userAgent);

//二、定義UI頁面載入完成後(onLoad)的處理函數。
const WebKitLoadStatus = {
    WEBKIT_LOAD_PROVISIONAL: 0,
    WEBKIT_LOAD_COMMITTED: 1,
    WEBKIT_LOAD_FINISHED: 2,
    WEBKIT_LOAD_FIRST_VISUALLY_NON_EMPTY_LAYOUT: 3,
    WEBKIT_LOAD_FAILED: 4
};

view.connect("notify::load-status", function() {
    if (view.loadStatus == WebKitLoadStatus.WEBKIT_LOAD_FINISHED) {
        print("loaded");
        view.execute_script("function notify(message) { document.title = message; }");

        var frame = view.get_main_frame();
        print(frame.get_uri());
    }
});

//三、定義內部調用外部資源的訊號事件處理函數。
// see: http://www.aclevername.com/articles/python-webgui/#message-passing-with-webkit
view.connect("title-changed", function(widget, frame, title) {
    if (view.loadStatus != WebKitLoadStatus.WEBKIT_LOAD_FINISHED)
        return;
    notifier.NotifyRemote(
        "appname", 0, "message-im", "Test", title, [], {}, -1,
        function(result) {
            view.execute_script("alert('notify id: " + result + "');");
        }
    );
});

view.load_uri("file:///home/rock/workspace/hello_xulruner/hello_xulrunner/chrome/content/index2.html");
w.set_position(1); //GTK_WIN_POS_CENTER
w.show_all();
Gtk.main();

第一步,我先定義調用 DBus Notify 方法的函數,參考《JavaScript 與 Desktop - DBus》。

第二步,我要由外部要求內部定義一個notify() 轉接函數供內部端呼叫。這個動作利用 execute_script() 方法實現。此轉接函數實際上將觸發 title-changed 事件訊號,讓外部接收內部的請求,進而調用 DBus Notify 方法在桌面上顯示訊息方塊。一如我們過往在 Ajax 中學到的經驗,若我們想要額外地、動態地定義 JavaScript 函數,最好是等到頁面載入完成,瀏覽器發出 onload 事件之後。對於外部的 gjs 端而言,則是透過 WebKit 的 notify::load-status 訊號捕抓 onload 事件。所以我將定義 notify() 轉接函數的動作,寫在 view.loadStatus == WebKitLoadStatus.WEBKIT_LOAD_FINISHED 成立之後的程式區塊中。

第三步,利用 title-changed 接收內部送出的訊息方塊顯示請求,調用 DBus Notify 方法。接著再利用 execute_script() 調用回呼函數,將外部資源的回傳結果送回。本範例中並未定義回呼函數的處理方式,只是簡單地以 alert() 代表回呼函數。

最後,我繼續使用《Hello HTML5 and XULRunner》中已經存在的文件,只是改成載入 index2.html (複製自 index.html,並加了一行程式碼)。

至於 UI 的部份,我將《Hello HTML5 and XULRunner》的 index.html 的內容另存為 index2.html,並在其 event_change_name()() 之中,多加了一個呼叫 notify(name 的動作。這個 notify() 函數,是由 gjs 從外部額外加進來的。文件內容摘要於下:

<script type="text/javascript">
    $(document).ready(function() {
        function event_change_name() {
            var name = $('#entry_name').val();
            $('#name').text(name);

            notify(name);
            //document.title = name;

            var flickr = "http://api.flickr.com/services/feeds/photos_public.gne?tags=" +
                name + "&tagmode=any&format=json&jsoncallback=?";
            $("#images").empty().text("loading...");
            $.getJSON(flickr, function(data){
                $("#images").empty();
                $.each(data.items, function(i,item){
                    $("<img/>").attr("src", item.media.m).appendTo("#images");
                    if ( i == 2 ) return false;
                });
            });
        }
        
        $('#entry_ok').click(event_change_name);
        $('#entry_name').change(event_change_name);
        
    });
    </script>
gtk-webkit-2.js 執行範例圖

Reference

樂多舊網址: http://blog.roodo.com/rocksaying/archives/14456843.html

樂多舊回應
lemonhall@gmail.com(lemonhall) (#comment-21675903)
Tue, 22 Mar 2011 09:57:33 +0800
whyareyoureadingthisurl.wordpress.com

这个小伙子想用JS调用本地代码,然后有人写了WEBKITS的JSCORE的VALA BINDING,一下子,JS就可以调用VALA代码了。。。。

不用绕你这么大的弯子了......
未留名 (#comment-21676049)
Tue, 22 Mar 2011 11:38:40 +0800
這篇文章說的是一個行程中存在兩個 JS host 的狀況。
一個是可以直接調用本地代碼的 gjs/seed;另一個則是被隔離的 gtk.webkit.

如果你的 JS 代碼運行在 gjs 或 seed 上,那麼你的 JS 代碼本來就可以調用本地代碼。不需要弄一個 vala binding.
lemonhall@gmail.com(lemonhall) (#comment-21676771)
Tue, 22 Mar 2011 15:40:07 +0800
多说无益,直接看这个例子,你应该就明白我在说什么了。

大陆人和台湾人语言上还是有一些障碍,按我理解无论是GJS/SEED/VALA来调用WEBKITS本质上都是一样的。

我关心的就是WEBKITS内部的JS调用外部资源,不是通过改变标题这种类似桥接的方式,而是直接呼叫外部函数名。

直接上例子吧,我是搜VALA搜到你这里的,你应该能直接看懂这个例子的。

http://ubuntuone.com/p/hzb/
未留名 (#comment-21682045)
Fri, 25 Mar 2011 11:23:46 +0800
基本上,我在閱讀大陸地區的網站時並沒有文字障礙。因為我每天大概都有一小時以上在逛大陸的動漫論壇 wwwww

只是你最初給的那個網址,我沒找到在哪談用JS调用本地代码的事。所以我假定他說的是gjs/seed的JS調用本地代碼。而這本來就不用多做什麼手腳。

看到你給的第二個例子後,我就知道了。那確實是最直接的方法。

其實我當初在搜尋資料時,也有找到 JavaScriptCore Framework。但是從 webkit 網頁的連結過去後,卻是連到 Apple developers 的網頁。故我以為 JavaScriptCore Framework 是 Apple 添加的、只在 OS X 才有的內容。就略過了。
http://developer.apple.com/library/mac/#documentation/Carbon/Reference/WebKit_JavaScriptCore_Ref/
為什麼連到 apple developer 網頁呢?這真是誤會了。

待我整理之後,就會再發一篇直接用 JavaScriptCore Framework 達成目標的文。在此先謝過了。