在桌面應用程式中使用 WebKit (1) -雜論,以及用 Objective C 操作 DOM

不久前公司信箱收到封信。來信的這位朋友說,他這幾年做網站做得非常膩,也覺得非常無趣,所以想要改做 Mac 軟體。

想改做桌面軟體好擺脫 Web,這種想法老實說不怎麼現實,首先呢,如果現在是想要做 iPhone 還是 iPad,可能還有點道理,以目前的 Mac 軟體環境來看,做 Mac 軟體大概就像寒天飲冰水,光是靠「不想做 A 所以來做 B」這種理由,實在不知道可以維持多久的熱情。國際巨星李奧納多.狄卡皮歐在電影《全面啟動》裡頭就告訴我們,光是負面情感,是沒有辦法真正深植想法的,今天做 Web 覺得煩了,誰知道哪天又覺得做什麼別的也煩了。

而且,做桌面軟體也不可能擺脫 Web。姑且不論 Google Chrome OS、Palm WebOS 這種擺明就要用 Web 技術實作桌面或行動裝置作業系統上的應用程式架構,或是 Adobe 提出像 AIR 這種用 Flash、HTML 與 JavaScript 就可以寫桌面應用程式的方案,現在哪一套桌面應用程式,幾乎都有或多或少的 Web 整合。

現在做軟體開發,可說是 Web 與桌面應用程式交互相融,雖然網頁可以做很多事情,但是在繪圖效率方面還是與桌面應用程式有距離,所以 Google 在做 Native Client 橋接 Netscape plugin API,WebOS 也弄了一個 PDK,讓你在瀏覽器中的 plugin 裡頭使用 OpenGL 與 SDL。至於桌面或行動應用程式呢,要不就是需要用 HTTP 在網路上交換資料,在遊戲裡頭得了多少分,都還要可以上傳到 Twitter 還是 Facebook 裡頭,微軟的 Office 也要讓你可以直接從 Word 上傳檔案到 SkyDrive 要不就是拿瀏覽器,當做應用程式的使用者介面。

現在的桌面應用程式呢,首先要具備的就是透過網路自動更新升級的能力,如此軟體有什麼問題都可以透過升級解決,軟體下載透過 HTTP,你也需要在軟體中準備一個瀏覽器,讓使用者瀏覽改版說明,知道更新了哪些內容。所以桌面應用程式,不可能沒有 Web 方面的建置,而且,軟體也不可能沒有網站,想要賣軟體,也得透過 Web 上的電子商務。

瀏覽器元件是一個現成好用的圖文閱讀工具,所以,Mail.app 裡頭讀 Email 的視窗是個瀏覽器,iChat、Adium、Colloquy 與 Skype 的聊天視窗是個瀏覽器,在 GitX 或 Sofa 的 SVN 工具 Versions 裡頭,檢視版本記錄的畫面是瀏覽器,Mac OS X 系統內建的字典用了瀏覽器。瀏覽器在編輯模式下,又是一個好用的 WYSIWYG 的文書編輯器,所以在 Mail.app 裡頭寫信,那也是個瀏覽器,Espresso 這套 Editor 也使用瀏覽器,Evernote 這套筆記軟體呢,則雖然用 Native UI 呈現筆記列表,但也用瀏覽器製作編輯筆記的畫面,至於與遠端主機同步筆記內容,我想應該也是透過 HTTP。

同樣是瀏覽器,用瀏覽器瀏覽網頁,與在應用程式中使用瀏覽器元件,在方式上的確有一些差別,瀏覽網頁時,我們會輸入網址,要求瀏覽器載入遠端主機的資料,在應用程式中使用瀏覽器元件,則是應用程式本身處理好資料,產生 HTML,直接設定瀏覽器中的內容。在瀏覽網頁時,瀏覽器元件是對著遠端主機做種種操作,所謂的 WebApp 呢,大抵上瀏覽器是 view,某個主機扮演 controller,像 GAE 這種 framework 中大抵上都把某個網站想像成一個 controller 物件,網站中某個路徑指向這個物件的某個 method 或另外一個路徑;在桌面應用程式這邊,則是對著某個 local 的 controller 操作。

做網站呢,大抵上重點可能會放在從 server 怎麼產生動態網頁、怎樣壓榨 server 效能,怎樣降低 server 成本卻可以更快速回傳內容,怎樣 24 小時服務都不中斷。做桌面應用程式呢,則需要知道怎樣透過 Native API 操作瀏覽器,在 Mac OS X 上,便是怎樣用 Objectice C 等語言操作 WebKit。

在 Mac OS X 上大概有幾個場合需要用 Native API 操作 WebKit,其一如上述,是應用程式中包含了一個 WebView,可能是純粹拿 WebView 當做桌面應用程式的 UI,或雖然是開啟遠端的網頁,但是遠端網頁中的某些連結,卻是拿來開啟桌面應用程式的某些功能。

其二,撰寫 WebKit plug-in。在 Safari 5 中有所謂的 Extension,不過 Extension 與 plug-in 不一樣,Extension 純粹使用 JavaScript 與 HTML 擴充瀏覽器的功能,例如方便你把什麼網頁貼到 Twitter 去,而 plug-in 的目的則是讓瀏覽器具備開啟原本所不支援的檔案類型的能力,雖然 HTML5 的發展方向是瀏覽器本身就可以透過 video、audio 等 HTML 標籤開啟媒體檔案,不過現在像 QuickTime Movie 仍然是透過 plug-in 開啟,而你哪天發明了一種想要在網路上流通的新格式,也可能需要另外寫一個 plug-in。

許多以瀏覽器當做介面的應用程式,也可能會有些私有的 plug-in,例如 Mail.app,我們在寫信的時候,可以直接從 Finder 中把某個檔案拖進編輯畫面,成為這封信件的附件,其實就是在瀏覽器中,用一個 plug-in 顯示拖入的附件。

其三,撰寫 Dashboard Widget 的 plug-in。在 Mac OS X 中按下 F12 按鍵後,就會進入 Dashboard,裡頭有像是天氣預報、時鐘等 Widget;每個 Widget 雖然是由 JavaScript 與 HTML 構成,而如果要從網路抓資料,像更新天氣預報資訊,用 JavaScript 透過 AJAX 便可以達成,不過,有些事情光靠 JavaScript 是做不到的,例如,在 Widget 中操作 iTunes 播放,Javascript 本身可沒辦法跟 iTunes 溝通,可能要想辦法呼叫 iTunes 的 AppleScript 介面,或是透過 Objective C API 做 IPC。

Widget plug-in 是一個用 Objective C 寫成的 loadable bundle,在執行 Widget 時,就會載入 plug-in 的內容,而 plug-in 裡頭的 Objective C 物件,也透過 WebKit 的橋接,變成 JavaScript 可以呼叫的物件,於是,Widget 可以先呼叫 plug-in,再讓 plug-in 去做原本沒辦法做的事情。

你的 controller 與 WebView 之間,大抵上可以透過幾種不同的方式溝通。從 controller 控制 WebView 的方向來看,方法有:

一、直接設定 WebView 中的 HTML 內容。也就是呼叫 WebFrame 的 loadHTMLString:baseURL:,把一段 NSString 送進去。

有些時候我們要改變 WebView 的內容,並不適合直接刷新整個網頁,而是要改變其中一部分,這時候可以考慮用下面的方法:二、要求 WebView 執行一段 JavaScript function。或,三、直接操作 WebView 的 DOM 物件。

而如果是從 WebView 中要呼叫我們的程式,大概有幾種方式,第一是網頁中使用我們自定的 URL scheme,如果我們在寫一個叫做 zonble 的應用程式,可以註冊一組叫做 zonble:// 的 URL scheme,這樣在點選這個連結時,就可以由 WebView 的 policy delegate 決定如何處理這個傳入的 URL,或是交由系統指派應該由那個應用程式負責,不過,Webkit Plug-in 與 Widget Plug-in 自然無法使用這種方法。二、就像前面在講 Widget Plug-in 時所說的,把我們的 Objective C 物件變成 JavaScript 可以呼叫的物件。

說起來大概是這樣,而實際玩這幾個東西,老實說還真是頂麻煩的。

跟 WebKit 相關的所有 Obejctive C API,不僅數量龐大-一般的 UI 元件的 delegate method 如果數量一多,頂多只是分成 delegate 與 data source,但是卻有五個 delegate protocol-而且與你熟悉的其他 Cocoa 或 iPhone 上的 API 不一樣,WebKit API 簡直是國中之國。

在你的經驗中,要產生一個繼承自 NSObject 的 Objective C 物件,不外乎就是呼叫 alloc 與 init,像是 [[NSObject alloc] init],但是 WebKit 裡頭的 DOM 物件,如果呼叫 init,只會告訴你 init 這個 method「should never be used」。

更討厭的是,蘋果在 Xcode 中所提供的文件裡頭,對於 WebKit 裡頭很多地方並沒有完整解釋,尤其是 DOM 物件相關的部份-算了一下,在 WebKit 裡頭是 DOM 開頭的 Header,大概有 130 個檔案,都沒有文件,連 Webkit.org 上也沒有多少解釋。

還好大部分 ObjC 操作 DOM 的 API,都與 JavaScript API 一致,所以要查 DOM 要怎麼操作,可以去查像是 Mozilla 網站上的文件。而要使用 ObjC 操作 DOM 物件,大概就是,先想想如果要做同樣的事情,用 JavaScript 會怎麼寫,然後查一下對應的 DOM 物件的 Header,翻譯成 ObjC 語法,不過,由於 ObjC 是一種具有 Class 的 OO 語言,不像 JavaScript 用 prototype,大概要花點力氣了解 JavaScript 物件對應到 WebKit 裡頭的 ObjC 物件的哪個 Class。

一些 JavaScript 物件大概對應到:

  • document 對應到 DOMDocument
  • document 中的 element 會對應到 DOMElement,HTML 物件的話則是 DOMHTMLElement。
  • 每個 element 的 style 會對應到 DOMCSSStyleDeclaration

來簡單講講 Objective C 怎麼操作 DOM。假如我們現在有一個簡單的 HTML 檔案:

<div id="main">		
</div>

那麼,在 JavaScript 裡頭,我們會這樣取得 main 這個 div 的 DOM 物件:

var main = document.getElementById('main');

我們便可以把這段程式翻譯成 ObjC:

DOMDocument *document = [[webView mainFrame] DOMDocument];
DOMHTMLElement *main = (DOMHTMLElement *)[document getElementById:@"main"];

我們也可以用一些比較新的 API,例如 querySelector。JS:

var main = document.querySelector('#main');
main.style.backgroundColor = '#AAA';

ObjC:

DOMDocument *document = [[webView mainFrame] DOMDocument];
DOMHTMLElement *main = (DOMHTMLElement *)[document querySelector:@"#main"];

假如我們想要在 main 這個 div 中,產生、並加入一個新的 child node,會這樣寫:

var main = document.getElementById('main');
var child = document.createElement('div');
child.innerHTML = 'child:' + (main.childElementCount + 1);
main.appendChild(child);

話說 childElementCount 也是一個比較新的 API,WebKit 要 4.0 之後才支援,Firefox 則是在 3.5 之後才支援。把上面那段程式翻譯成 ObjC 的話:

DOMDocument *document = [[webView mainFrame] DOMDocument];
DOMHTMLElement *main = (DOMHTMLElement *)[document getElementById:@"main"];
DOMHTMLElement *newElement = (DOMHTMLElement *)[document createElement:@"div"];
newElement.innerHTML = [NSString stringWithFormat:@"child:%d", main.childElementCount + 1];
[main appendChild:newElement];

如果要修改某個 HTML element 的 CSS 樣式,在 JavaScript 是這樣:

var main = document.getElementById('main');
main.style.backgroundColor = '#AAA';

用 ObjC 寫是這樣:

DOMDocument *document = [[webView mainFrame] DOMDocument];
DOMHTMLElement *main = (DOMHTMLElement *)[document getElementById:@"main"];	
DOMCSSStyleDeclaration *style = main.style;
[style setProperty:@"background-color" value:@"#AAA" priority:@""];
// 也可以寫成 [style setBackgroundColor:@"#AAA"];

5 thoughts on “在桌面應用程式中使用 WebKit (1) -雜論,以及用 Objective C 操作 DOM

  1. In simple Chinese, I guess UTF-8 is OK.

    ==============
    最近关注到一套叫SEEDKIT的项目,在GTK下,将JAVASCRIPT和GOBEJCT绑定了,用JS直接调用GTK和GNOME的底层函数,将WEBKITS融入,简直就是混搭编程的典范。

    个人觉得现在入JQUERY,DOJO等JS库做界面越来越强大,用这种混搭风格来写也不失为一种好尝试,当然JS的效率很低,所以可能关键性的应用还得调用编译型语言写得函数来完成。

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.