20120324

開發自己的 Firefox add-on 附加元件(六) Page-mod 模組

Page-mod 模組讓 add-on 開發者可以在特定的網頁中執行 scripts。最直接的運用是,我們可以使用 page-mod 來動態修改某個頁面的內容。

這個模組會產生一個建構函式 PageMod,此函式會建立一個新的 page modification (簡稱"mod")( MOD 數位電視 開玩笑的啦 哈哈:P <-某人餓瘋了)。

Page mod 只能在頁面已經被載入或重新載入時才可以修改頁面內容。換句話說,如果 add-on 在使用者瀏覽器開啟的情況下被載入,則使用者必須重新載入所有要讓 mod 修改的頁面。

若要停止某個 page mod 的作用,呼叫 page mod 的 destroy 方法。

跟所有其他模組相同,若要和網頁內容作互動,page-mod 使用在 content process 中執行並定義 messaging API的content scripts,以在 content script 和 main add-on code 之間做構通。

建立一個 PageMod ,add-on 開發者必須提供:
  • 基於網頁URL的一組規則(rule)來想要的要面。每個規則以 match-pattern syntax 來指定。
  • 要在想處理的頁面中執行的 content scripts
  • 為 onAttach 設定一個值:這個值是一個將在符合 ruleset(=a set of rules)的網頁被載入時被呼叫的函式。這麼做是為了在 add-on code和contentscript 之間建立溝通通道。
所有這些參數都是非必要的,除了 ruleset。ruleset 至少要包含一個規則(rule)。

以下的add-on中,當一個符合 ruleset 的頁面被載入時,會顯示一個提示視窗:
var pageMod = require("page-mod");
pageMod.PageMod({
  include: "*.org",
  contentScript: 'window.alert("Page matches ruleset");'
});
如果指定一個值"ready" 或 "end"(而不是"start")給 contentScriptWhen,則 content script 可以和 DOM 本身互動(因為"ready"的時機為載入所有 DOM 架構之後):
var pageMod = require("page-mod");
pageMod.PageMod({
  include: "*.org",
  contentScriptWhen: 'end',
  contentScript: 'document.body.innerHTML = ' +
                 ' "<h1>Page matches ruleset</h1>";'
});
上方例子中,當開啟或重新整理網址中包含"*.org"的網頁時,會將該網頁的網頁主體內容(<body>)改變為"<h1>Page matches ruleset</h1>"。

使用 contentScriptFile

此文大部分的例子都是以 string 來定義 content script,並使用 contentScript 選項(option)來指定給 page mods。

然而我們也可以在 data 目錄下建立獨立的 content script 檔案,然後使用 self 模組來取得指向該檔案的 URL,接著將其指定給 page-mod 的 contentScriptFile 屬性。

舉例來說,如果我們將 content script 命名為"myScript.js"並儲存在 data 目錄之下,我們可以使用以下的程式碼來指定到這個檔案:
var data = require("self").data;
 
var pageMod = require("page-mod");
pageMod.PageMod({
  include: "*.org",
  contentScriptWhen: 'end',
  contentScriptFile: data.url("myScript.js")
});

Communicating With Content Scripts

當符合規則的頁面被載入時,PageMod 會呼叫 add-on 程式碼中提供 給onAttach 的函式。PageMod 提供一個變數,worker object,給這個函式。

我們可以將 worker 視為 add-on 中的 add-on code 和附加在此 page 的 content script 之間的溝通通道結束。換句話說就是,當 content script 附加到符合規則的網頁中時,會呼叫定義在 onAttach 選項中的函式。

因此 add-on 可以藉由呼叫 worker.postMessage(),將訊息傳送給 content script;藉由註冊監聽 worker.on() 的函式,從 content script 接收訊息。

要注意的是,如果多個符合規則的頁面被同時載入,那每個頁面自己的 content script 副本會被載入其自己的 execution context。在這種情況下,onAttach 會為每個載入的頁面呼叫一次,然後 add-on 程式碼會有個別頁面的獨立 worker:



由以下例子證明:
var pageMod = require("page-mod");
var tabs = require("tabs");
 
var workers = [];
 
pageMod.PageMod({
  include: ["http://www.mozilla*"],
  contentScriptWhen: 'end',
  contentScript: "onMessage = function onMessage(message) {" +
                 "  window.alert(message);};",
  onAttach: function onAttach(worker) {
    if (workers.push(worker) == 3) {
      workers[0].postMessage("The first worker!");
      workers[1].postMessage("The second worker!");
      workers[2].postMessage("The third worker!");
    }
  }
});
 
tabs.open("http://www.mozilla.com");
tabs.open("http://www.mozilla.org");
tabs.open("http://www.mozilla-europe.org");
P.S.上例中用到 Tabs 模組,不了解 Tabs 模組請看這篇
在這裡我們指定了一個 ruleset 來配對任何以 "http://www.mozilla" 開頭的URL。當某頁面符合條件時,我們加入一個 supplied worker 到 array 中,然後當 array 中有三個 workers 時,我們輪流送出一個訊息給每個 worker,告訴他們要被附加的順序。最後worker在提示框( alert box )中顯示訊息。

這顯示了,不同的頁面在不同的 context 中執行,且每個 context 有自己的和 add-on script 溝通的通道( communication channel )。

值得注意的是,雖然每個 execution context 有不同的 worker,但 worker 是可以在單一 execution context 相關的所有 content scripts 之中分享的。以下的例子中,我們傳送兩個 content scripts 給 PageMod:這兩個 content scripts 會分享一個worker實體。

在範例中,每個 content scripts 在 add-on script 中以使用全域函式 postMessage 送出訊息,以辨識身分。在 onAttach 函式中,add-on 程式碼註冊監聽函式,讓頁面被附加 content script 時在 console 中紀錄 log:
var pageMod = require("page-mod");
var data = require("self").data;
var tabs = require("tabs");
 
pageMod.PageMod({
  include: ["http://www.mozilla*"],
  contentScriptWhen: 'end',
  contentScript: ["self.postMessage('Content script 1 is attached to '+ " +
                  "document.URL);",
                  "self.postMessage('Content script 2 is attached to '+ " +
                  "document.URL);"],
  onAttach: function onAttach(worker) {
    console.log("Attaching content scripts")
    worker.on('message', function(data) {
      console.log(data);
    });
  }
});
 
tabs.open("http://www.mozilla.com");
程式碼中第 15 行的 Console 用法請見這裡

這個add-on的主控台輸出(console output)為:
info: Attaching content scripts
info: Content script 1 is attached to http://www.mozilla.com/en-US/
info: Content script 2 is attached to http://www.mozilla.com/en-US/

Mapping workers to tabs

worker 物件有一個 tab 屬性,這個屬性會回傳與該 worker 關聯的分頁( tab )。我們可以以此來存取與某特定網頁關聯的 tab API:
var pageMod = require("page-mod");
var tabs = require("tabs");
 
pageMod.PageMod({
  include: ["*"],
  onAttach: function onAttach(worker) {
    console.log(worker.tab.title);
  }
});

Attaching content scripts to tabs

前面我們已經看過 page mod API 如何透過 URL 來附加 content script 給 page。然而有時候我們並不在乎URL:我們只想要在某特定分頁( tab )中執行需要的 script (而不是本文前面的例子,是對符合 URL 條件的網頁做處理)。

舉例來說,我們可能想要當使用者點擊 widget 時,在目前的分頁中執行一段 script:來阻擋特定內容、改變字體樣式、或顯示網頁的 DOM 結構。

使用 tab 物件的 attach 方法,我們可以附加一組 content scripts 給特定分頁,這些 scripts 會立刻執行。

下面的 add-on 建立一個 widget,其被點擊時,所有在當前分頁中載入的頁面中的 div 元素會被標示出來( highlight ):
var widgets = require("widget");
var tabs = require("tabs");
 
var widget = widgets.Widget({
  id: "div-show",
  label: "Show divs",
  contentURL: "http://www.mozilla.org/favicon.ico",
  onClick: function() {
    tabs.activeTab.attach({
      contentScript:
        'var divs = document.getElementsByTagName("div");' +
        'for (var i = 0; i < divs.length; ++i) {' +
          'divs[i].setAttribute("style", "border: solid red 1px;");' +
        '}'
    });
  }
});
Destroying Workers 當 workers 的關聯頁面被關閉時,其會產生一個 detach 事件:也就是說,當分頁會關閉或分頁的位置被改變時。如果我們保存包含屬於某 page mod 的 worker 的清單,我們可以使用 detach 事件來移除無效的 workers。 例如,我們保存附加在某 page mod 的 workers 清單:
var workers = [];
 
var pageMod = require("page-mod").PageMod({
  include: ['*'],
  contentScriptWhen: 'ready',
  contentScriptFile: data.url('pagemod.js'),
  onAttach: function(worker) {
    workers.push(worker);
  }
});
我們可以藉由監聽 detach 事件來移除已經無效的 workers:
var workers = [];
 
function detachWorker(worker, workerArray) {
  var index = workerArray.indexOf(worker);
  if(index != -1) {
    workerArray.splice(index, 1);
  }
}
 
var pageMod = require("page-mod").PageMod({
  include: ['*'],
  contentScriptWhen: 'ready',
  contentScriptFile: data.url('pagemod.js'),
  onAttach: function(worker) {
    workers.push(worker);
    worker.on('detach', function () {
      detachWorker(this, workers);
    });
  }
});
了解更多 Page-mod 類別的詳細建構子、方法、屬性、事件,請參考這裡

沒有留言:

張貼留言