本文档介绍了 Cross Process Object Wrappers (CPOWs),这使 chrome 代码能够同步访问多进程 Firefox 中的内容。
在多进程 Firefox 中,chrome 代码运行在与 Web 内容不同的另一个进程中。因此 chrome 代码不能直接与 Web 内容交互;相反,它必须考虑将与 Web 内容交互的脚本放在单独的脚本中,这被称为框架脚本(frame scripts),也称帧脚本。
Chrome 代码可以使用消息管理器加载框架脚本到内容进程,然后可以使用消息传递 API 与它们通信。有关于此的更多信息,详见 消息管理器 的使用文档。
Chrome 到内容的通信必须是异步的。这是因为 chrome 进程运行着 Firefox UI,因此如果被内容进程所影响,缓慢的内容进程可能致使 Firefox 对用户无响应。
将同步代码转换成异步可能是困难并且耗时的。作为一个迁移的辅助,消息框架使框架脚本变成了内容对象,通过一个被称为 Cross Process Object Wrapper(简称 CPOW)的包装器,使其在 chrome 中可用。但是,尽管 CPOWs 很方便,它们存在严重的局限性并且可能导致响应性问题,因此只应在必要时使用,并仅作为迁移的辅助。
从框架脚本传递 CPOWs
框架脚本可以发送消息到 chrome,使用两个全局函数之一:sendAsyncMessage()
或者 sendSyncMessage()
。这些函数的第三个可选参数是被包装的属性对象。举例来说,框架脚本在用户点击它时发送一个 DOM 节点到 chrome,并将 clicked
属性作为第三个参数:
// frame script addEventListener("click", function (event) { sendAsyncMessage("my-e10s-extension-message", {}, { clicked : event.target }); }, false);
在 chrome 脚本中,DOM 节点现在是通过 Cross Process Object Wrapper 访问,作为该消息的 objects
属性的个属性。chrome 脚本可以获得和设置包装的对象属性,以及调用它的函数:
// chrome script windowMM.addMessageListener("my-e10s-extension-message", handleMessage); function handleMessage(message) { let wrapper = message.objects.clicked; console.log(wrapper.innerHTML); wrapper.innerHTML = "<h2>已被 chrome 修改!</h2>" wrapper.setAttribute("align", "center"); }
自动生成的 CPOWs
没有自我声明多进程兼容的附加组件会加载一些兼容性垫片。其中一个垫片提供了以下行为:每当 chrome 代码尝试直接访问内容(例如通过 window.content
或者 browser.contentDocument
),提供一个包装了内容的 CPOW。这意味着下面这样的例子在多进程 Firefox 中也能正常工作。
gBrowser.selectedBrowser.contentDocument.body.innerHTML = "被 chrome 代码替换";
但仍然要记住,这是通过 CPOW 访问,并不是直接访问内容。
双向 CPOWs
一个常见的模式是 chrome 代码访问内容对象并添加事件监听器到那里。为了解决这个问题,CPOWs 是双向的。这意味着如果内容传递了一个 CPOW 到 chrome 进程,chrome 进程可以同步传递对象(如事件监听器函数)到 CPOW 中定义的函数。
这意味着你可以写这样的代码:
// frame script /* 在 mouseover,发送 button 到 chrome 脚本,以一个CPOW形式。 */ var button = content.document.getElementById("click-me"); button.addEventListener("mouseover", function (event) { sendAsyncMessage("my-addon-message", {}, { element : event.target }); }, false);
// chrome script /* 载入框架脚本,然后监听消息。 在我们得到消息时,提取 CPOW 并添加一个函数作为监听器到按钮的 "click" 事件。 */ browserMM.loadFrameScript("chrome://my-addon/content/frame-script.js", false); browserMM.addMessageListener("my-addon-message", function(message) { let wrapper = message.objects.element; wrapper.addEventListener("click", function() { console.log("被点击了"); }); });
映射内容文档到 XUL 浏览器
一个常见的模式是获取 XUL <browser>
,它对应一个内容文档。要做到这点, gBrowser.getBrowserForDocument
和 gBrowser.getBrowserForContentWindow
分别可以传递一个内容文档和内容窗口的 CPOW,并且返回这些文档 / 窗口所属的 XUL <browser>。
如果没有找到这样的浏览器,两者都是返回 null。
CPOWs 的限制
尽管 CPOWs 可以方便的使用,但它有几个主要的局限性,在下面列出。
CPOWs 与平台 API
你不能传递 CPOWs 到预期会收到 DOM 对象的平台 API。举例来说,你不能传递一个 CPOW 到
nsIFocusManager.setFocus()
。
Chrome 响应性
在 chrome 这边缺少同步 API 是有意的:因为 chrome 进程运行着 Firefox UI,任何响应性问题都将影响整个浏览器。在制成 chrome 进程与内容进程的过程中,CPOWs 打破了这个原则,并且致使内容进程可能使整个浏览器陷入无响应状态。
性能
尽管包装器看起来像是一个完全在 chrome 脚本范围下管控的对象,但它实际上只是一个到内容进程中一个对象的引用。在你访问一个包装器的属性时,它发送一个同步消息到内容进程及返回结果。这意味着它比使用一个对象慢很多倍。
消息顺序
CPOWs 可能违反你做出的有关消息排序的假设。考虑以下代码:
mm.addMessageListener("GotLoadEvent", function (msg) { mm.sendAsyncMessage("ChangeDocumentURI", {newURI: "hello.com"}); let uri = msg.objects.document.documentURI; dump("收到加载事件: " + uri + "\n"); });
这发送了一个消息,要求框架脚本更改当前文档的 URI,然后通过一个 CPOW 访问当前的文档 URI。你可能预期 uri
的值得到设置的 "hello.com"。但这不一定:为了避免死锁,CPOW 消息可以绕过正常的消息并且被优先处理。对 documentURI 属性的请求有可能在 "ChangeDocumentURI" 的消息之前被处理,并因而 uri
持有它在更改之前的值。
出于这个原因,最好不要混用 CPOWs 和正常的消息管理器消息。还有一个坏主意是将 CPOWs 用于任何安全相关,因为你可能获得不一致的结果,与使用消息管理器的相关代码。