MDN’s new design is in Beta! A sneak peek: https://blog.mozilla.org/opendesign/mdns-new-design-beta/

编写适合多进程 Firefox 的扩展

这篇翻译不完整。请帮忙从英语翻译这篇文章

致 Firefox 开发者:本文描述如何使你开发的扩展能在多进程 Firefox 中运行。

目前,在桌面版 Firefox 中,Chrome 代码(Chrome Code)和内容(Content)运行在同一个进程里。因此扩展可以直接访问网页内容:

gBrowser.selectedBrowser.contentDocument.body.innerHTML = "replaced by chrome code";

不过呢,在多进程 Firefox(又称 Electrolysis 或 E10S)中,扩展代码(Add-on Code)和内容则在不同的进程中运行,所以上述直接访问就不一定可行了。

在多进程 Firefox 中,扩展需要将触及内容的代码分解成单独分离的脚本,也就是所谓的框架脚本(Frame Scripts)。框架脚本在内容进程中运行,可以直接访问内容。框架脚本通过消息传递接口(Message-passing API)与扩展的剩余部分进行通讯。

运行在 Chrome 进程中的扩展代码必须向框架脚本发送异步消息。这样做可以确保 Firefox 的用户界面不会被内容进程卡死。

内容进程则可以向 Chrome 进程发送同步或异步消息,不过使用异步消息是再好不过了。

关于使用消息管理器的更多细节,请参阅:消息管理器指南。接下来,本文将告诉你如何判断你开发的扩展是否会受到这方面的影响,同时简要说明需要如何进行修改。最后,通过对一些简单的样板扩展进行修改,让它们能在多进程 Firefox 中运行。

检查你的扩展是否受到影响

总体规则如下:

  • 如果你只是使用附加组件开发工具包的高级接口,你的扩展不会受到影响(附加组件开发工具包尚未完全兼容多进程Firefox,但很快就会完全兼容)。
  • 如果你的扩展完全不访问网络内容,它们也不会受到影响。
  • 如果你通过直接使用表层扩展启动扩展底层开发工具包接口(例如窗口/工具标签页/工具)来访问网络内容,那么你的扩展有可能受到影响。
  • 如果你在标签页中加载可扩展用户界面语言(xul),你的扩展会受到影响。

为了证实是否受到影响,你还需要进一步测试,这个测试过程由两步过程构成:

  • 开启 Firefox 的多进程支持Firefox 每夜版(Nightly Build)支持多进程,但是默认是关闭的,而且并没有出现在选项窗口中。启用多进程的方法是访问 about:config 页面,找到名为 browser.tabs.remote.autostart 的项目,将其值设置为true,重启火狐浏览器。启用多进程功能后,标签页的标题会出现下划线以提示用户。
  • 声明你的扩展兼容多进程模式:为了便于迁移至多进程 Firefox,我们提供兼容性管理工具帮助不兼容多进程的扩展工作。故此,你需要禁用兼容性管理工具以检测你的扩展是否真正兼容。禁用兼容性管理工具的方法是向你的扩展的 install.rdf 文件添加一个名为 multiprocessCompatible 的属性,值为 true。

现在,你可以在多进程 Firefox 中禁用兼容性管理工具来测试你的扩展的兼容性了。可惜你还不能在开启多进程功能的时候安装新扩展,因此你必须先安装好扩展再开启多进程功能。关于这个问题,详见 bug 1055808

更新你的代码

更新代码的一般方法是:

  • 将你的扩展中需要访问网络内容的部分分解为一个或数个单独的脚本。在多进程Firefox中,这被称为框架脚本。
  • 为你的框架脚本注册 chrome:// 地址。
  • 消息管理器来将脚本加载进 browser 对象。
  • 如果你的扩展需要在主扩展代码和框架脚本之间通信,可以使用消息管理器接口来实现。
  • 如果你需要在标签页中加载可扩展用户界面语言,可以将它们注册为 about: 地址,然后通过 about: 地址加载它们。

更多细节,请参见 message manager 文档。

新应用程序接口的向前兼容能力

新的多进程 Firefox 的消息接口在多进程模式没有开启的情况下仍然可用。实际上它们从 Firefox 4开始就在某种程度上可用了。不过呢,最初的应用程序接口和现在的并不相同。一些已知的差异如下:

  • Firefox 17 的界面发生了变化
  • 在Firefox 19 之前的版本中,类似new content.document.defaultView.XMLHttpRequest()的代码会得到NS_ERROR_FAILURE: Failure的错误值。

你不仅应该在开启了多进程支持的每夜版Firefox上测试你的扩展的变化,而且应该在你打算支持的没有开启多进程支持的发行版(Release Build)中测试。

举几个例子

这部分将演示修改几种不同的扩展的过程。这些扩展都是很简易的,意在展示基础的扩展模式在多进程Firefox中需要的不同处理方式。

你可以在 e10s-example-addons GitHub repository 中找到这些例子的所有源代码。

在所有页面运行一个脚本

第一个扩展在每一个页面加载的时候运行一些代码。这些代码不和扩展的其他部分交互,它们只是对页面进行一些预设的修改。在这个例子中,扩展向文档的主体(body)添加了一个边界。

这个扩展通过将一种“页面加载中”代码碎片("On page load" code snippet)附加于可扩展用户界面语言层来实现此修改。

var myExtension = {  
    init: function() {  
        // The event can be DOMContentLoaded, pageshow, pagehide, load or unload.  
        if(gBrowser) gBrowser.addEventListener("DOMContentLoaded", this.onPageLoad, false);  
    },  
    onPageLoad: function(aEvent) {  
        var doc = aEvent.originalTarget; // doc is document that triggered the event  
        if (doc.nodeName != "#document") return; // only documents  
        // make whatever modifications you want to doc
        doc.body.style.border = "5px solid blue";
    }  
}  

window.addEventListener("load", function load(event){  
    window.removeEventListener("load", load, false); //remove listener, no longer needed  
    myExtension.init();    
},false);

因为这段代码直接访问网络内容,所以它不能在多进程 Firefox 中运行。

移植到消息管理器

为了使用消息管理器移植这个例子,我们可将这个扩展全部的主体部分放入一个框架脚本:

// frame-script.js
// will run in the content process

addEventListener("DOMContentLoaded", function(event) {
  var doc = event.originalTarget;
  if (doc.nodeName != "#document") return; // only documents
  doc.body.style.border = "5px solid red";
});

我们将为这个框架脚本注册一个 chrome:// URL :

// chrome.manifest

content    modify-all-pages    chrome/content/

我们附加到XUL overlay的主体脚本,只是一个使用全局消息管理器来在每个标签页中加载框架脚本的 stub。

// chrome script
// will run in the chrome process

var globalMM = Cc["@mozilla.org/globalmessagemanager;1"]
  .getService(Ci.nsIMessageListenerManager);

globalMM.loadFrameScript("chrome://modify-all-pages/content/frame-script.js", true);

移植到 Add-on SDK

一个好的替代这样的一个扩展的思路是将其移植到 Add-on SDK。Add-on SDK 包括一个名为 page-mod 的设计为在网页中加载脚本的模块。Add-on SDK 称这些脚本为内容脚本。

这种情况下扩展的主要代码创建一个 page-mod 来加载内容脚本到用户载入的每个页面:

// main.js

var pageMod = require("sdk/page-mod");
var self = require("sdk/self");

pageMod.PageMod({
  include: "*",
  contentScriptFile: self.data.url("modify-all-pages.js")
});

内容脚本可以直接修改页面:

// modify-all-pages.js - content script

document.body.style.border = "5px solid green";

在活动标签中运行一个脚本

这个例子说明了一个扩展如何:

这个例子是一个无需重启的扩展,它使用 CustomizableUI 模块添加了一个按钮。当用户点击这个按钮,这个扩展运行一些代码来改变当前标签。其基础构造取自 Jorge Villalobos 的 Australis "Hello World" 扩展 .

代码实际做的事是:找到任意 <img> 元素并将其 src 替换为从硬编码于扩展中的列表中随机抽取的无意义的 GIF 图像。 无意义的 gifs 从Whimsy extension 获取。

第一个版本直接访问页面,因此其不是多进程兼容的:

// bootstrap.js

let Gifinate = {
  init : function() {
    let io =
      Cc["@mozilla.org/network/io-service;1"].
        getService(Ci.nsIIOService);

    // the 'style' directive isn't supported in chrome.manifest for bootstrapped
    // extensions, so this is the manual way of doing the same.
    this._ss =
      Cc["@mozilla.org/content/style-sheet-service;1"].
        getService(Ci.nsIStyleSheetService);
    this._uri = io.newURI("chrome://gifinate/skin/toolbar.css", null, null);
    this._ss.loadAndRegisterSheet(this._uri, this._ss.USER_SHEET);

    // create widget and add it to the main toolbar.
    CustomizableUI.createWidget(
      { id : "gifinate-button",
        defaultArea : CustomizableUI.AREA_NAVBAR,
        label : "Gifinate",
        tooltiptext : "Gifinate!",
        onCommand : function(aEvent) {
          Gifinate.replaceImages(aEvent.target.ownerDocument.defaultView.content.document);
        }
      });
  },

  replaceImages : function(contentDocument) {
      let images = contentDocument.getElementsByTagName("img");
      for (var i = 0; i < images.length; ++i) {
        let gif = this.gifs[Math.floor(Math.random() * this.gifs.length)];
        images[i].src = gif;
      }
    },

移植到消息管理器

为了移植这个例子到消息管理器,我们将使 onCommand 加载一个框架脚本到当前的 <browser>,然后监听来自框架脚本的 "request-gifs" 信息。这些 "request-gifs" 信息应当包含我们在此页面上需要的 GIFs :信息监听器取回并返回这个数量的 GIFs。

// bootstrap.js
// will run in the chrome process 

let Gifinate = {
  init : function() {
    let io =
      Cc["@mozilla.org/network/io-service;1"].
        getService(Ci.nsIIOService);

    // the 'style' directive isn't supported in chrome.manifest for bootstrapped
    // extensions, so this is the manual way of doing the same.
    this._ss =
      Cc["@mozilla.org/content/style-sheet-service;1"].
        getService(Ci.nsIStyleSheetService);
    this._uri = io.newURI("chrome://gifinate/skin/toolbar.css", null, null);
    this._ss.loadAndRegisterSheet(this._uri, this._ss.USER_SHEET);

    // create widget and add it to the main toolbar.
    CustomizableUI.createWidget(
      { id : "gifinate-button",
        defaultArea : CustomizableUI.AREA_NAVBAR,
        label : "Gifinate Button",
        tooltiptext : "Gifinate!",
        onCommand : function(aEvent) {
          Gifinate.replaceImages(aEvent.target.ownerDocument);
        }
      });
  },

  replaceImages : function(xulDocument) {
    var browserMM = xulDocument.defaultView.gBrowser.selectedBrowser.messageManager;
    browserMM.loadFrameScript("chrome://gifinate/content/frame-script.js", false);
    browserMM.addMessageListener("request-gifs", Gifinate.getGifs);
  },

  getGifs : function(message) {
    var gifsToReturn = new Array(message.data);
    for (var i = 0; i < gifsToReturn.length; i++) {
      let gif = this.gifs[Math.floor(Math.random() * this.gifs.length)];
      gifsToReturn[i] = gif;
    }
    return gifsToReturn;
  },

再次地,我们需要为这个框架脚本注册一个 chrome:// URL:

// chrome.manifest

content gifinate frame-script.js

在框架脚本中,我们获取所有的 <img> 元素并发送 "request-gifs" 信息给扩展主体代码。 由于这是框架脚本我们可以将其变为一个同步信息,并使用其返回值更新 src 属性:

// frame-script.js
// will run in the content process

var images = content.document.getElementsByTagName("img");
var response = sendSyncMessage("request-gifs", images.length);
var gifs = response[0];

for (var i = 0; i < images.length; ++i) {
  images[i].src = gifs[i];
}

整个扩展的流程现在像这样:

已知问题

这里是可能影响扩展开发者移植到多进程firefox的开放的bug列表:

  • Bug 1051238 - 框架脚本被永久缓存,因此扩展不能在不重启浏览器的情况下正确更新
  • Bug 1017320 - 实施兼容shims的跟踪 bug

文档标签和贡献者

 此页面的贡献者: yfdyh000, Atester, Jack_No1, Clementine
 最后编辑者: yfdyh000,