應廣大(?)讀者要求,接下來聊聊 WebKit 的 Plug-in。
首先,瀏覽器的 Plug-in 與所謂的 Extensions、Add-Ons、Accelerators 是不一樣的。Plug-in 的用途是讓瀏覽器可以開啟原本所無法開啟的檔案,而 Extension 則往往是用 HTML、JavaScript、CSS 或其他技術,增加瀏覽器的功能。比方說,我們想要在瀏覽器畫面上方增加一條工具列,這個工具列上面有一堆按鈕,讓你可以把現在瀏覽的內容貼到 Twitter 上,讓你快速使用某個搜尋引擎,或是把現在的網頁丟到某個 HTML validator 檢查看看有哪些問題,這是 Extensions。
而瀏覽器原本只能瀏覽 HTML 檔案、純文字檔案,還有 GIF、Jpeg、PNG 等圖形檔案,沒辦法直接播放 mp3(其實現在的瀏覽器可以支援 audio tag)或影片檔案(現在當然也有支援 video tag 的瀏覽器),所以,瀏覽器就提供了一個架構,在遇到這類型的檔案時,如果知道某個 plug-in 可以處理這種檔案,就在畫面中挖出一塊空間,讓 plug-in 處理這些檔案,然後把 plug-in 的畫面填到預留的空白中。或這麼說-我們在 HTML 中使用 embed 語法的時候,就是要使用 plug-in 的時候。
在不同平台上,plug-in 會使用所在平台上的 Native API 實作,所以在不同平台上會有哪些 plug-in,原本就該是不一樣的。所以,如果我們在某個平台上的某個應用程式裡,發現有哪些東西是瀏覽器引擎所沒有的,這時候用個 plug-in 增加需要的功能,比較算是 plug-in 該做的事情-例如,在 Mail.app 這類的郵件軟體裡頭,我們會使用瀏覽器閱讀 HTML 格式的電子郵件,但是郵件附件的部份想要做些特別的處理,我們便可以用個 plug-in 負責顯示附件。
在不同平台上的 plug-in 應該因為不同平台的環境與需求而不同,如果想要透過瀏覽器 plug-in 跨平台,這種想法實在很莫名其妙。如果真的希望所有平台都可以使用某個網路服務,而瀏覽器本身就提供這方面的功能,這種事情應該交給瀏覽器,而不是 plug-in。
在 Mac OS X 上,大概有幾種不同的 plug-in,一種是 NPAPI plug-in,是 Netscape 當年所制定的 plug-in 架構,現在 Safari、Firefox、Opera、Camino 等瀏覽器,都支援這個架構。另一種是 Safari 的 WebKit plug-in,而各種使用 WebKit 的 Cocoa 應用程式,也都可以使用這種 plug-in。另外就是 Google Chrome 自家的 Native Client,而在 Qt 應用程式中,也可以直接把某個 QWidget 放在 QWebView 中,當做 plug-in 使用。
我非常不熟 Qt,在這邊只討論 Cocoa,而我們自己的應用程式的 WebView 中,沒辦法呼叫 Native Client,所以只討論 NPAPI plug-in 與 WebKit plug-in。
※ NPAPI plug-in 與 WebKit plug-in
我個人認為 NPAPI plug-in 與 WebKit plug-in 的最大差別,在於 NPAPI plug-in 的原理是瀏覽器提供 plug-in 一個繪圖的 context,plug-in 在這個 context 裡頭用各種繪圖指令,而 WebKit plug-in 則是一個 NSView,在嵌入 WebKit plug-in 時,會把一個 NSView 疊在 WebView 的 content view 上面。
因此,在使用 NPAPI plug-in 的網頁中,我們可以透過 CSS 的 z-index 順序,把其他的 HTML element 疊在 plug-in 上面,但是 WebKit plug-in 則不行。在 WebView 中,會把一個 frame 裡頭的 HTML,render 在一個 view 裡頭,NPAPI plug-in 的畫面會一起 render 在同一個 context 裡,但是 WebKit plug-in 則是把一整個 view,疊在 WebFrame 的 view 上面,不管 CSS、HTML 怎麼調整,HTML 裡頭的東西,都不能疊上去。
※ NPAPI Plug-in
NPAPI plug-in 裡頭怎麼畫圖,最近有些變化,要知道有哪些變化,就要去看一下Mozilla 網站,看看版本演進:在 Gecko 1.9 裡頭,才開始支援 CoreGraphics API,計畫逐步取代用 QuickDraw 的 plug-in,Gecko 1.9.3 才開始支援 CoreAnimation 的繪圖,1.9.4 開始要停止支援 QuickDraw API 與 Carbon Event,而現在最新的穩定版本,是 3.6 系列,用的還是 Gecko 1.9.2(連結)。
而將 plug-in API 從 QuickDraw 往 CoreGraphics 與 CoreAnimation 推展,當然也有蘋果工程師的參與,在網站上就可以看到 Tim Omernick 的名字-這樣說來,推動 NPAPI plug-in 改用 CoreGraphics,都不知道是幾年前的事情了,查了一下資料,Tim Omernick 是最早的 iPhone 工程師,後來又去 ngmoco 做遊戲,算一算 Tim Omernick 還在蘋果的時候,應該是 2008 年之前的事情。
而現在呢,雖然網站上面說 Gecko 已經實作了 CoreGraphics 的 plug-in 架構,但是實際玩玩看之後,還是沒辦法用 Firefox 使用 CoreGraphics 的 plug-in。 Mozilla 有一些 plug-in 的範例程式,裡頭有 Windows(Win32 API)、*nix(用 GTK),還有個 Mac OS X 的版本,裡頭只使用 CoreGraphics,編起來,用幾個不同的瀏覽器試試看,只有 Safari 可以看到結果-根據前面提到的 CoreGraphics NPAPI 規格,plug-in 可以選擇在瀏覽器不提供 CoreGraphics context 的時候,fallback 到 QuickDraw API,但是這個範例程式沒做這件事情,所以,Firefox 沒有 CoreGraphics context,就沒辦法執行。
所以,不久前某家公司更新了 plug-in 軟體,在 Mac OS X 上增加了什麼 GPU 加速影片解碼之類的,但是像 Firefox、Opera 這些瀏覽器還是不支援 CoreGraphics,所以最後還是用 QuickDraw 這個 80 年代出現在 68k 機器上、現在不支援 64 位元環境、早就應該掃進歷史的垃圾堆裡的繪圖 API,把東西畫出來。-鄉親啊,Bill Atkinson 不寫程式,跑去當自然生態攝影師都不知道多久,甚至漂亮的照片多到可以拿來寫一個幫你送電子明信片的 iPad 應用程式,連 QuickDraw 的程式碼,都已經被送進山景城的電腦發展史博物館了。
在現在這種規格在改變,但是實作速度緩慢的尷尬時刻,實在不是寫個 NPAPI plug-in 的好時機,而老實說,以前開個 mp3 之類的媒體檔案,還要透過 Media Player 或 QuickTime 的 plug-in,現在瀏覽器自己都可以處理這些檔案,以前 Netscape 推出這個 plug-in 架構,預想的未來是桌面應用程式都可以用 Java Applet 取代,但之後的發展卻變成網頁應用程式幾乎是 JavaScript 的天下,JavaScript framework 百家爭鳴(不過最近似乎也局勢已定?),在瀏覽 Internet 內容時,實在沒有必要用到 plug-in,如果要跨平台的話,現在哪個平台沒有 WebKit?
現在瀏覽器會用到的 plug-in,也就只剩下那麼一個 plug-in,而這個 plug-in 實在問題不少,在 Mac OS X 上尤其讓人抓狂-如果你在 Xcode 裡頭,寫一個用到 WebView 的應用程式,用 Debug 模式執行,程式往往會莫名其妙停止,原因是-某個 plug-in 在一直亂呼叫 Debug(),要求停止你正在 Debug 的應用程式。
這家公司說起來還頂有趣。Google 在 Google I/O 上宣布要做 Google TV 的之後,這家公司馬上做了一段影片,告訴大家,在 Google TV 上面可以用這個 plug-in,所以就可以在電視上開啟瀏覽器,連到 BBC 網站上,用這個 plug-in 觀賞 BBC 的新聞影片。-我還以為打開電視,原本就應該可以看到 BBC。
至於怎麼寫 NPAPI plug-in 呢?前面已經提到 Mozilla 的範例程式在哪裡,check out 出來改就對了,在 Windows 上面看到哪裡在用 Win32 畫圖就改哪段,在 Mac OS X 上面看到哪裡呼叫了 CoreGraphics 就改哪段,而看到哪邊在處理各種事件,就去改那一段。細節不要問我,我不想寫這玩意。
※ WebKit Plug-in
WebKit plug-in 是個 NSView,雖然你沒辦法把其他的 HTML 元件疊在上面,但是卻能夠做許多 NPAPI 不能做的事情。
1. 可以用 Interface Builder 製作 plug-in 的畫面-WebKit plug-in 本身是個 bundle,我們可以把 nib 檔案放進 bundle 中,在 plug-in 啟動時,呼叫 NSBundle 的 loadNib:。如此一來,我們就可以在網頁中,使用 Interface Builder 拉出來的 UI。
2. 可以使用各種繼承自 NSView 的 UI 元件-就算我們不用 Interface Builder,只要所有 NSView 可以用 addSubview: 加進去的東西,我們都可以用。要表格有 NSTableView,要 3D 有 NSOpenGLView。你也可以用任何繼承自 NSView 的 view 當做 plug-in。
我們直接用 NSTableView 當做 plug-in 玩玩看:
3. Drag and Drop-只要我們的 plug-in 實作 NSDraggingSource,我們就可以把 plug-in 裡頭的東西拖出去,而實作 NSDraggingDestination 就可以把東西拖進 plug-in 裡頭。
總之,WebKit plug-in 可以直接當做一個沒有 window 的 Cocoa 應用程式來寫,所有的應用程式介面,都在一個 view 裡頭。而真的需要用到一些 window,像是想要用個 NSOpenPanel 來選個檔案,我們也知道,NSView 有個 method 叫做 window,可以拿到 view 所在的 window,我們也就可以把 NSOpenPanel attach 到目前的瀏覽器 window 上面。
這個 WebKit plug-in 由於是在 Mac OS X 上獨家,所以蘋果用這種方式做出一些特別的 plug-in,也就只有 Mac OS X 的版本,像是瀏覽 Quartz Composer 檔案的 plug-in。不過,plug-in 呼叫的是平台的 API,用途也該是解決平台上的需求,我們在做一個用到 WebKit 的應用程式的時候,如果需要用到 plug-in,就會毫不考慮使用 WebKit plug-in。
像 Mail.app 在顯示附件的時候,需要在網頁中以系統檔案圖示呈現郵件的附件,而 WebKit 中原本並沒有可以根據檔案類型就顯示相對圖示的物件,這時候,我們就可以寫個 WebKit plug-in,把檔名當做參數傳進去,然後呼叫 NSWorkSpace 的 iconForFileType:,拿到一個 NSImage,在 view 裡頭畫出來,收工。而 plug-in 除了可以放在系統指定的目錄讓 Safari 使用外,個別的應用程式,也可以在自己的 Application bundle 裡頭,放置專屬這個應用程式使用的 WebKit plug-in。
來簡單寫一個 WebKit plug-in:
1. 打開 Xcode,從選單中選擇 New Project,在 Project Template 裡頭選 WebKit plug-in。Xcode 就會根據範本,幫我們產生一個 WebKit plug-in 專案,隨便取個名字,叫做 TestPlugin 好了。
2. 編譯-這個範本就算什麼事都不做,就已經會動了。不過我們可能要從 Targets 那邊改一下編譯設定,在 Architectures 的地方,改成 32/64-bit Universal 之類。如果你用的是 Mac OS X 10.6,又在 64 位元環境下,64 位元的 Safari 可沒辦法載入只有 32 位元 binary 的 plug-in。
3. 我們來看一下這個 plug-in 有什麼東西。WebKit plug-in 和其他 Mac OS X 應用程式或系統 plug-in 一樣,是一個 bundle,設定一樣寫在 Info.plist 裡,NPAPI plug-in 也一樣可以包裝成 bundle。我們來看看 Info.plist 的設定,需要注意的包括:
<key>NSPrincipalClass</key> <string>TestPluginView</string> <key>WebPluginName</key> <string>Name of my plug-in</string> <key>WebPluginDescription</key> <string>Brief description of my plug-in</string> <key>WebPluginMIMETypes</key> <dict> <key>application/x-my-mime-type</key> <dict> <key>WebPluginExtensions</key> <array> <string>myextension</string> </array> <key>WebPluginTypeDescription</key> <string>Name of content type</string> </dict> </dict>
NSPrincipalClass:載入 plug-in 之後,程式的進入點是哪個 class。
WebPluginName 與 WebPluginDescription: plug-in 的基本資料。在 Safari 中,我們可以從 Help、Installed Plug-ins 選單中,打開一份列出所有已安裝 plug-in 的網頁,我們的 plug-in 資料會顯示在這個地方。我們也可以試試看用 InfoPlist.strings,將這些資訊翻譯成不同語系的版本。
WebPluginMIMETypes:我們的 plug-in 可以開啟哪些 MIME type 的檔案,包括哪些附檔名的檔案…等。我們先不修改這些地方。
4. 把編好的檔案-TestPlugin.webplugin-丟到 ~/Library/Internet Plug-Ins/ 目錄下。關閉 Safari 重開,讓 Safari 重新載入 plug-in。
5. 寫個簡單的 HTML 檔案,裡頭寫個一行:
<embed id="test" type="application/x-my-mime-type" width="400" height="400"></embed>
用 Safari 打開 HTML 檔案,如果沒有出現 missing plug-in 的訊息,就代表成功了,至於接下來怎麼在 NSView 裡頭畫東西、疊 sub view,就不是這邊要討論的東西了。
※ WebKit Plug-in 與網頁的互動
一個 plug-in 放在網頁裡頭,總不可能只做自己的事情,外面網頁會想要呼叫這個 plug-in 做點事情,plug-in 裡頭也會想要叫外面的網頁做點事情。
我們希望 plug-in 做些不同的事情,首先就是 embed tag 裡頭的 attribute-我們除了用 type,讓 WebKit 知道應該要用我們的 TestPlugin,傳了 width 與 height 決定 view 的高度與寬度,我們還可以加入其他的 attribute,而 plug-in 這邊在收到了 attribute 之後,也要做對應的處理。
在從範本產生的專案中,我們可以看到,WebKit 會呼叫 plug-in 的 +plugInViewWithArguments:,在產生新的 instance 的時候,傳入一個包含各種參數的NSDictionary,變數名稱是 newArguments ,裡頭就有不少我們需要的東西。我們先把 [newArguments valueForKey:WebPlugInAttributesKey] 倒出來,裡頭又是一個 NSDictionary:
WebPlugInAttributesKey = { height = 400; type = "application/x-my-mime-type"; width = 400; };
於是我們便可以拿到 embed tag 的所有 attribute 了。然後,用 WebPlugInBaseURLKey 可以知道 plug-in 所在網頁的 URL,用 WebPlugInContainingElementKey 則可以知道代表 embed tag 的 DOM 物件,型別是DOMHTMLEmbedElement,有了這個 DOM 物件,就可以用來 evaluate JavaScript,也可以用 ownerDocument 找到 DOM 的頂層 document,於是我們可以操作網頁裡頭的 DOM 物件。
至於用 WebPlugInContainerKey,則可以拿到一個實作 WebPlugInContainer Protocol 的物件,用途就是讓 plug-in 呼叫外頭。WebPlugInContainer 有以下的 method 可以呼叫:
- webPlugInContainerLoadRequest:inFrame: -要求瀏覽器開啟某個 URL
- webPlugInContainerShowStatus: -要求瀏覽器在狀態欄(通常在瀏覽器視窗左下角)顯示一段文字資訊
- webPlugInContainerSelectionColor -取得在瀏覽器中,圖文被選取是什麼顏色,讓 plug-in 可以選擇在被選起來的時候該畫成什麼樣子
- webFrame -取得 plug-in 所在的 frame 的 WebFrame 物件。有了 WebFrame,我們就可以拿到 JSGlobalContextRef,也可拿到 WebFrame 的 WebView,於是可以拿到 WebView 的 windowScriptObject。於是,我們可以在 plug-in 裡頭,或是要用 WebScriptObject 的 Obj C API,或是用 JavaScriptCore,呼叫所有可以呼叫的 JS 程式。
※ DOM
一個用 embed tag 包進網頁中的 plug-in,也是一個 DOM 物件,在上面提到的 HTML 檔案中,我們將這個 tag 的 id 設為 tag,在網頁裡頭只要寫一段 document.getElementById(‘test’),就可以拿到我們的 plug-in。接著,我們就可以用 JS 呼叫 plug-in 的 method,而其實執行的是 plug-in 的 Obj C method。
之前在系列閒聊的第二篇中提到,如果我們在寫一個擁有 WebView 的 Obj C controller 物件,我們可以透過對 windowScriptObject 呼叫 setValue:forKey:,把這個物件註冊成 JS 裡頭 window 物件的屬性。在寫 plug-in 的時候,其實…也可以這麼做,只是會很奇怪-一個 plug-in,明明該是在網頁裡頭的 DOM 物件,怎麼會在 window 的屬性裡頭也有一份,而且,同一個網頁裡頭,也可能一次用上很多個 plug-in,所以不能這樣搞。
在從範本產生的專案中,可以看到這一行:
- (id)objectForWebScript { return self; }
意思就是,當我們在 JS 裡頭,對 document.getElementById(‘test’) 拿到的 DOM 物件,呼叫一些 JS function,到底應該由哪個 Obj C 物件負責處理,這邊我們回傳 self,就是如果我們呼叫 document.getElementById(‘test’).test(),就要送到 TestPlugin 的 -(void)test 裡頭,我們可以在這邊換由其他物件處理。JS 程式怎麼呼叫 plug-in 裡頭的 Obj C method,與之前提到的方法完全相同。
※ WebKit Plug-in 的生命週期
在我們從範本產生的專案中,有留一些空的 method 等你實作,像是 webPlugInInitialize、webPlugInStart、webPlugInStop 與 webPlugInDestroy,讓你可以在瀏覽器告訴你 plug-in 產生、釋放的時候,做一些事情。
我們需要特別瀏覽器什麼時候會把 plug-in 關掉。網頁關了自然就會釋放 plug-in,但是當我們在 Safari 中開啟了一個用到 WebKit plug-in 的網頁,用 command + T 在同一個視窗中開啟新的 tab 的時候,原本網頁的 plug-in 會被放掉,如果包含這個 plug-in 的 DOM HTML element 的 CSS 樣式被設為隱藏(display: none),plug-in 也會被放掉;當我們切回原本的分頁,或是改變上層 DOM 物件顯示狀態,從隱藏變為顯示,網頁中出現的不是原本的 plug-in 物件,WebKit 會重新產生一個新的 instance。雖然在 Web Inspector 中,可以看到 embed tag 的 DOM 物件還在那邊,DOM 物件包含的 plug-in 已經被釋放了。
我們會把釋放成員變數這些事情放在 dealloc 裡頭,但像是用 QuickTime 播放影片、音樂等,在呼叫到 webPlugInStop 或 webPlugInDestroy 時,就該停止播放。
而由於原本的 plug-in 整個被放掉,我們沒辦法在 plug-in 內部保留之前的狀態,用 NSUserDefaults 可能不是好主意,因為在不同網頁中呼叫 plug-in 就該有不同的行為,不太應該用應用程式偏好設定來儲存這些狀態。比較可能的作法是,把一些設定交給外面網頁的 JS 程式保留,在重新產生新的 plug-in instance 時,從 JS 裡頭還原設定。老實說我不太知道什麼是比較好的實作。
不過,如果我們的 plug-in 只需要 +plugInViewWithArguments: 所傳入的那些資料,或是在使用時不會一直切換使用狀態,或 plug-in 是用在我們自己的應用程式中,而我們的程式不像 Safari 那樣一直切換分頁,就不用太擔心這些事情。
※ 最後
我個人最喜歡 WebKit plug-in 的部份,還是對於右鍵選單(context menu)的處理。在 NSView 裡頭,只要實作 menu 或是 menuForEvent: 其中一個 method,回傳一個 NSMenu 物件,在這個 view 裡頭點右鍵,就會出現選單。我們在 plug-in 中增加 foo: 與 bar: 兩個 method,然後實作 menu:
- (void)foo:(id)sender {} - (void)bar:(id)sender {} - (NSMenu *)menu { NSMenu *menu = [[[NSMenu alloc] initWithTitle:@"Menu"] autorelease]; NSMenuItem *foo = [[[NSMenuItem alloc] initWithTitle:@"Foo" action:@selector(foo:) keyEquivalent:@""] autorelease]; [foo setTarget:self]; NSMenuItem *bar = [[[NSMenuItem alloc] initWithTitle:@"Bar" action:@selector(bar:) keyEquivalent:@""] autorelease]; [bar setTarget:self]; [menu addItem:foo]; [menu addItem:bar]; return menu; }
我們在 plug-in 裡頭點右鍵,就會出現選單。基本上這沒什麼。
但,假如我們在網頁裡頭,用滑鼠把這個 plug-in 一起選取起來,然後再點選右鍵,這就值得一提了-鄉親啊,WebKit 會把跟網頁文字有關的選項,與 plug-in 裡頭所提供的選單,組合成同一份右鍵選單。
嗯嗯嗯,绝对追看的实用体验!
不论什么顺序说,其实大家最关心的还是如何在 iPad/Phone 中用 WebKit 组件通过标准的DHTML 来构建自个儿的应用,而不是通过可怕的 Cocoa …
Cocoa 哪有什麼好可怕的。在 iPhone 上只是把很多東西隱藏起來而已,WebKit 就是有這麼多東西。
奇文共欣賞! 大大對mac的深入沒話說. 工作需要, 最近開始接觸cocoa, 第一個topic就是webkits plug-in. 請教大大3個問題, 1)像adobe flash是用NPAPI還是webkits plug-in開發的? 2)webkits plug-in也可透過於網路自動下載安裝? 3)webkits plug-in版夲如何能像adobe flash自動由HTML偵測版本, 並下載新版本? 是否也能像windows於IE中指定版本atributes?
Thanks much.
1. Flash 是用 NPAPI
2. 通常是用 Package Maker 包成 package,然後讓使用者再載後,手動用 PackageInstaller 裝,可以參考一下像是 GMail Video Chat 等 plug-in 的作法。
3. 你可以在瀏覽器中,用 JavaScript 呼叫 navigator.plugins,可以取得所有 plug-in 的資訊,然後或許可以透過 plug-in 名稱來比對 plug-in 版本。
話說在 Mac 上面第一個專案就寫跟 webkit 有關,想來似乎有點苦,因為 WebKit 裡頭一堆東西是 Xcode 與 ADC 沒有文件,而是要去 WebKit.org 查,甚至要去查 Mozilla 的線上文件還比較快。 :p
请教一下在NPAPI Plug-in里面可以使用NSOpenGLView吗?我想实现一个media player的plugin。谢谢!
我記得沒辦法。
在 NPAPI 中,plug-in 只能夠在 Mac OS X 上面拿到 QuickDraw 或是 Quartz 2D 的 drawing context,而拿不到 view 物件,如果沒有 view,自然沒有辦法把別的 view 疊上去,包括 NSOpenGLView。
但是說不定後來有提供 OpenGL 的方法,不太確定,有空再去查查看 Mozilla 有什麼新聞。
Hi 你好!
我正在做一个webkit的plugin在Safari, 我想知道在用javascript call webkit的方法时,如何传入参数,现在不传参数是可以调的,加了参数,到不了方法的函数的里头,能给个sample吗? 谢谢!
大哥可以請叫一下嗎?
小弟要在WebView中讀取Flash object
但是不知道要如何呼叫Flash Plugin來載入Flash object
能否請您指點一下
感謝:)
要用 Flash 的話,就是在 webview 裡頭的 html 用 embed 語法,把 swf 檔案 embed 進來。
請問可以 plugin 可以取代原來瀏覽器對於 jpg/jpeg 預設的處理方式嗎? 例如 jpg/jpeg 有加密, 不知道能否透過改寫 plugin 來進行解密後再交由瀏覽器處理? 先謝謝您的回答!
很遗憾,到了10.7的年代,WebKit Plug-in 已经不再被支持了。
Dear前輩:
您好,可否請教一個問題,因我的電腦是 Mac Mini安裝的xCode 4.6版,不過我發現xCode新增專案時,已經沒有WebKit Plugins可以選擇… 想請問一下,您是否知道在xCode4.6上要如何開發 webKit plugin 麻煩您了 萬分感謝!! 搞好久了..><