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-changed。title-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: https://rocksaying.github.io/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>

Reference
- JavaScript 與 Desktop - DBus - 本系列文章第一篇
- JavaScript 與 Desktop - WebKit - 本系列文章第二篇
- HOWTO Create Python GUIs using HTML - 基於 PyWebKit 的參考文章
- Hello HTML5 and XULRunner
- GTK+ Reference Manual - GTK 視窗 C API 參考文件.
- WebKitGTK+ Reference Manual - WebKitGtk+ C API 參考文件.
樂多舊回應