构建 Firefox 扩展
与许多 Linux Journal 的读者一样,Firefox 是我首选的浏览器。其核心优势之一是可用的扩展数量。早期的扩展主要集中在改变浏览器的外观上;然而,在过去的几年里,扩展已被用来提供非常丰富的用户体验,同时跨越了桌面和 Web 应用程序之间的界限。
在本文中,我将解释构建一个与 Picnik 提供的照片编辑 API 集成的扩展,是多么容易扩展 Firefox。
新的扩展开发者应该做的第一件事是设置一个开发配置文件。虽然您可以使用正常的 Firefox 配置文件进行扩展开发,但创建一个专用于开发的新配置文件通常更容易。首先,启动 Firefox 的配置文件管理器
$ firefox -ProfileManager
接下来,单击“创建配置文件”按钮。一旦向导加载,单击“下一步”开始。此时,您应该看到一个类似于图 1 所示的窗口。输入新配置文件的名称(我使用了 dev)。在单击“完成”之前,请务必记下您的配置文件将存储的文件夹路径。您稍后将使用该路径。
现在您有了一个专用的配置文件,您应该安装一些使开发更容易的扩展。您应该安装的第一个是 Extension Developer。这是几个方便的扩展的汇编——所有这些扩展都旨在使开发人员的生活更轻松。有关其他几个方便的扩展,请参阅“资源”。我强烈建议您安装所有这些扩展。
此时,您已准备好开始您的第一个扩展。几乎所有扩展都以相同的基本样板代码开始,因此制作 Extension Developer 的同一个人将 Firefox Extension Wizard 放在一起,以自动化此过程的这一部分。您可以在本文的“资源”中找到它的 URL。
大多数必填字段应该很容易理解。主要值得注意的是扩展 ID。它用于唯一标识扩展以进行更新和其他目的。过去,标准做法是使用 GUID(全局唯一标识符)。最近,大多数开发人员已切换到类似于电子邮件地址的格式。对于本示例,我使用了 extension@linuxjournal.com。我还选择了创建上下文(右键单击)菜单的选项。图 2 显示了我如何填写其余字段。
一旦您对您的选择感到满意,请单击“创建扩展”按钮。几秒钟后,您的浏览器应提示您下载一个 zip 文件。继续并解压缩它
$ unzip linuxjournal.zip Archive: linuxjournal.zip inflating: linuxjournal/install.rdf inflating: linuxjournal/chrome.manifest inflating: linuxjournal/readme.txt inflating: linuxjournal/content/firefoxOverlay.xul inflating: linuxjournal/content/overlay.js inflating: linuxjournal/skin/overlay.css inflating: linuxjournal/locale/en-US/linuxjournal.dtd inflating: linuxjournal/locale/en-US/linuxjournal.properties inflating: linuxjournal/config_build.sh inflating: linuxjournal/build.sh
在深入了解所有这些文件的用途之前,您应该安装它以查看自动生成的扩展的实际外观。Firefox 可以使用两种方式安装的扩展。正常的安装方法涉及在 Firefox 中打开扩展的 .xpi 文件。这是大多数扩展分发和安装的方式。另一种方法是创建一个指针文件,告诉 Firefox 在哪里可以找到您的扩展文件。使用此方法,您不必每次要测试更改时都重新安装扩展;您只需创建指针文件即可
$ cd linuxjournal $ pwd > ~jjhuff/.mozilla/firefox/lhn85ppm.dev/extensions/ ↪extension\@linuxjournal.com
当然,您需要将 ~jjhuff/.mozilla/firefox/lhn85ppm.dev 替换为您的 Firefox 开发配置文件目录。
现在,继续并使用您的开发配置文件启动 Firefox
$ firefox -P dev
首先,检查以查看扩展是否已安装。选择“工具”→“附加组件”,并验证是否列出了 LinuxJournal 1.0。您应该看到一个类似于图 3 所示的窗口。当您打开“工具”菜单时,您可能注意到新的(红色)菜单项(图 4)。继续并在浏览器窗口中右键单击。您应该看到一个类似于图 5 所示的菜单。如果一切看起来都正确,则您的扩展已正确安装。

图 4. 该扩展可以在“工具”下创建一个自定义菜单项。

图 5. 您还可以创建一个自定义右键单击菜单项。
在修改生成的代码之前,您应该了解所有部分是如何交互的。扩展的主要文件是 install.rdf。它指定了扩展的名称、ID 和版本。install.rdf 文件还包含所有兼容应用程序及其版本的列表。在本示例中,我们指定了一个应用程序,其 ID 为 {ec8030f7-c20a-464f-9b0e-13a3a9e97384},这是 Firefox 的 ID。我们还指定我们与 Firefox 1.5 到 2.0 版本兼容。
第二个感兴趣的文件是 chrome.manifest,它告诉 Firefox 扩展内部的内容。清单还包括覆盖列表。(我将在本文后面解释覆盖。)
大多数扩展都组织成几个目录。“content”目录通常包含扩展 UI 和逻辑的大部分。“skin”目录是 CSS 和任何图形所在的位置。最后,“locale”用于特定于语言环境的文件,例如翻译。(我将在本文后面讨论本地化。)
Firefox(以及其他一些 Mozilla 项目)的用户界面以称为 XML 用户界面语言 (XUL) 的文件格式实现,并结合了 JavaScript。总的来说,这被称为 Chrome。如果您安装了 Chrome List 扩展,您可以轻松查看构成您的浏览器及其扩展的文件。例如,文件 chrome://browser/content/browser.xul 包含主 Firefox 窗口的 UI。
通过将额外的 XUL 元素覆盖到现有的 Chrome 上来创建对用户界面的添加和修改。扩展的覆盖在 chrome.manifest 中指定,行类似于以下内容
overlay chrome://browser/content/browser.xul ↪chrome://linuxjournal/content/firefoxOverlay.xul
此行指定 firefoxOverlay.xul 应覆盖在 browser.xul 之上。如果您添加其他覆盖文件,或者您想修改应用程序的其他部分,则需要在 chrome.manifest 中添加更多行。
让我们看一下扩展如何向上下文菜单添加新项。首先,在 Chrome 浏览器中打开 chrome://browser/content/browser.xul,然后搜索 contentAreaContextMenu。第二个命中应类似于此
<popup id="contentAreaContextMenu" ... > ... <menuitem id="context-stop" label="&stopCmd.label;" accesskey="&stopCmd.accesskey;" command="Browser:Stop"/> <menuseparator id="context-sep-stop"/> ... </popup>
现在,从您的扩展中打开 firefoxOverlay.xul。您应该看到一个看起来像这样的块
<popup id="contentAreaContextMenu"> <menuitem id="context-linuxjournal" label="&linuxjournalContext.label;" accesskey="&linuxjournalContext.accesskey;" insertafter="context-stop" oncommand="linuxjournal.onMenuItemCommand(event)"/> </popup>
当覆盖加载时,浏览器会在其现有的 Chrome 中搜索 ID 为 contentAreaContextMenu 的元素,并将来自覆盖的 XUL 合并到其中。它最终会得到类似这样的东西
<popup id="contentAreaContextMenu" ... > ... <menuitem id="context-stop" label="&stopCmd.label;" accesskey="&stopCmd.accesskey;" command="Browser:Stop"/> <menuitem id="context-linuxjournal" label="&linuxjournalContext.label;" accesskey="&linuxjournalContext.accesskey;" insertafter="context-stop" oncommand="linuxjournal.onMenuItemCommand(event)"/> <menuseparator id="context-sep-stop"/> ... </popup>
当渲染此菜单时,我们的菜单项与正常的上下文菜单项一起出现。此外,我们指定了 insertafter 属性,以告知浏览器我们希望我们的菜单项出现在 context-stop 菜单项之后。
Chrome 系统建立在现有技术之上,以轻松支持本地化 UI。其中一个主要部分是将字符串与 UI 本身分开存储在 DTD(文档类型定义)文件中的能力。在我们的代码中,firefoxOverlay.xul 使用以下行引用该 DTD
<!DOCTYPE overlay SYSTEM "chrome://linuxjournal/locale/linuxjournal.dtd">
locale 的 Chrome URL 很特殊,因为浏览器会自动扩展它们以引用扩展中的正确位置。例如,对于说美国英语的人,Firefox 会自动将 chrome://linuxjournal/locale/linuxjournal.dtd 扩展为 chrome://linuxjournal/locale/en-US/linuxjournal.dtd。
DTD 用于定义新的 XML 实体,可以将其视为宏。我们的 DTD 包含
<!ENTITY linuxjournal.label "Your localized menuitem"> <!ENTITY linuxjournalContext.label "Your Menuitem"> <!ENTITY linuxjournalContext.accesskey "Y">
这些在 XUL 中通过在它们前面加上 & 来引用,如
<menuitem id="context-linuxjournal" label="&linuxjournalContext.label;" accesskey="&linuxjournalContext.accesskey;"
这种将字符串与 UI 分离的方式起初可能很笨拙,但它除了本地化之外还有其他优点。例如,如果您在出现在多个位置的内容中拼写错误,则只需修复一次。
扩展背后的实际代码是用 JavaScript 编写的,这使得许多经验丰富的 Web 开发人员都可以编写它们。JavaScript 还使扩展可以跨平台,只需很少的工作。为了实际加载代码,必须在覆盖 XUL 文件中引用它。在我们的例子中,以下内容可以完成这项工作
<script src="overlay.js"/>
在编写扩展时,要记住的最重要的一点是,JavaScript 的全局命名空间在所有扩展以及核心浏览器代码之间共享。这意味着开发人员需要使用技术来防止名称冲突。一种简单的方法是将唯一前缀添加到您的所有变量和函数。首选方法是创建一个未命名的对象,其中包含您的所有变量和函数。查看自动生成的扩展,我们看到
var linuxjournal = { onLoad: function() { ... }, showContextMenu: function(event) { ... }, onMenuItemCommand: function(e) { ... }, }; window.addEventListener("load", function(e) ↪{ linuxjournal.onLoad(e); }, false);
使用此技术,我们的扩展在全局命名空间中只有一个条目 (linuxjournal)。这使得名称冲突更容易避免。
请注意对 window.addEventListener 的调用。这确保了在加载覆盖时调用我们的 onLoad 函数。在生成的代码的情况下,它创建一个变量,我们可以使用该变量来访问字符串包。
现在您已经基本了解了如何创建扩展,让我们继续看一个真实的例子。我的雇主 Bitnik 最近发布了其基于 Flash 的照片编辑器 Picnik 的 API (www.picnik.com)。Bitnik 最初的计划是使我们的服务易于集成到第三方网站中。但是,API 也为使用扩展实现我们无法仅通过 Flash 实现的集成级别打开了大门。
我的第一个目标是添加一个简单的上下文菜单项,以允许用户轻松编辑现有照片。幸运的是,大多数代码已经包含在扩展向导生成的代码中。请参阅列表 1 和 2,了解两个最重要的文件(picnik.xul 和 contextmenu.js)的完整文本。我还创建了 common.js 来存储一些将在添加功能时在文件之间共享的变量。
列表 1. picnik.xul
<?xml version="1.0"?> <!DOCTYPE picnik SYSTEM "chrome://picnik/locale/picnik.dtd"> <overlay id="picnik-overlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" > <script type="application/x-javascript" src="chrome://picnik/content/common.js"/> <script type="application/x-javascript" src="chrome://picnik/content/contextmenu.js"/> <popup id="contentAreaContextMenu"> <menuitem class="menuitem-iconic" id="picnik-ctx-edit" insertafter='context-viewimage' label="&picnik.edit_picture;" oncommand="picnikContextMenu.editImage();" image="chrome://picnik/content/picnik_16x16.png"/> </popup> </overlay>
列表 2. contextmenu.js
var picnikContextMenu = { onLoad:function() { // Attach the showContextMenu function // to the context menu var e = document.getElementById("contentAreaContextMenu") if( e ) e.addEventListener("popupshowing", function(ev){ picnikContextMenu.showContextMenu(ev); }, false); }, // Called right before the context menu // popup is shown showContextMenu: function(event) { if( gContextMenu ) { var edit_picture = document.getElementById("picnik-ctx-edit"); if( edit_picture ) edit_picture.hidden = ! (gContextMenu.onImage || gContextMenu.hasBGImage); if( gContextMenu.onImage ) this.imageURL = gContextMenu.imageURL; else if( gContextMenu.hasBGImage ) this.imageURL = gContextMenu.bgImageURL; else this.imageURL = ''; } }, // Called if the user clicks the 'edit' // menu item editImage: function() { var url = picnikCommon.baseURL + "?import=" + escape(this.imageURL); gBrowser.selectedTab = gBrowser.addTab(url); }, }; window.addEventListener("load", picnikContextMenu.onLoad, false);
大多数修改发生在函数 showContextMenu 中。此函数在实际向用户显示上下文菜单之前立即调用。这使我们的扩展有机会动态修改菜单项。在我们的例子中,我只想在用户实际右键单击图像时显示“在 Picnik 中编辑”选项。
Firefox 为我们的函数提供了一个全局变量 (gContextMenu),其中包含有关用户单击内容的丰富信息。例如,当用户在图像上激活菜单时,gContextMenu.onImage 为 true。首先,showContextMenu 通过其 ID picnik-ctx-edit 获取对实际菜单项的引用。然后,如果用户没有单击图像,则隐藏该项。最后,该函数保存图像 URL,以便扩展知道如果用户实际选择菜单项,要将哪个图像加载到 Picnik 中。
当用户选择菜单项时,浏览器会调用 picnik.editImage。此函数构造一个 URL 以传递给 Picnik,然后使用该 URL 创建一个新选项卡。picnik.com 上的服务器首先下载图像,然后响应一个包含实际 Flash 应用程序和图像的页面,准备进行编辑。
在 Mozilla Developer Center 的网站上浏览时,我遇到了一些我认为是对 Picnik 的 Firefox 扩展的自然改进——拍摄完整网页屏幕截图的功能。这个难题的第一部分是新的 Canvas HTML 元素,它为 JavaScript 提供了灵活的 2D 绘图画布。Canvas 最初被设想为一种在客户端生成动态图形的方式。
两个额外的 Canvas 函数使屏幕截图成为可能。第一个是 drawWindow 函数。顾名思义,drawWindow 将 XUL 窗口渲染到画布上。在我们的例子中,我们将使用它来渲染网页。第二个重要函数是 toDataURL,它允许脚本获取画布上内容的图像。
通常,URL 引用远程服务器或本地文件系统上的对象。数据 URL 将实际对象存储为 URL 的一部分。这对于直接在 CSS 或 HTML 中嵌入小图形非常方便。此技术允许浏览器避免向 Web 服务器发出另一个请求。在我们的例子中,我们将使用它来获取画布的 PNG 文件。
正如您在列表 3 中看到的,picnik.xul 已被修改为向上下文菜单和“工具”菜单添加弹出菜单。一个额外的文件 screengrab.js(列表 4,可在 Linux Journal FTP 站点上找到——请参阅“资源”)包含实际抓取屏幕截图的代码。与上下文菜单代码一样,此文件也有一个 onLoad。然而,在这种情况下,该函数的工作是检测是否缺少 Canvas 或 toDataURL。如果是这样,它将禁用屏幕抓取功能。这允许扩展在 Firefox 1.5 上运行,而不会出现令人困惑的错误消息。
列表 3. picnik.xul 文件的扩展版本
<?xml version="1.0"?> <!DOCTYPE picnik SYSTEM "chrome://picnik/locale/picnik.dtd"> <?xml-stylesheet href="chrome://picnik/content/toolbar.css" type="text/css"?> <overlay id="picnik-overlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" > <script type="application/x-javascript" src="chrome://picnik/content/common.js"/> <script type="application/x-javascript" src="chrome://picnik/content/contextmenu.js"/> <script type="application/x-javascript" src="chrome://picnik/content/screengrab.js"/> <popup id="contentAreaContextMenu"> <menuitem class="menuitem-iconic" id="picnik-ctx-edit" insertafter='context-viewimage' label="&picnik.edit_picture;" oncommand="picnikContextMenu.editImage();" image="chrome://picnik/content/picnik_16x16.png"/> <menu class="menu-iconic" id="picnik-ctx-grab" insertafter='context-sendpage' label="&picnik.menu;" image="chrome://picnik/content/picnik_16x16.png"> <menupopup> <menuitem label="&picnik.grab_visible;" oncommand="picnikScreenGrab.grabVisible();"/> <menuitem label="&picnik.grab_full;" oncommand="picnikScreenGrab.grabFull();"/> </menupopup> </menu> </popup> <menu id="tools-menu"> <menupopup id="menu_ToolsPopup"> <menu class="menu-iconic" id="picnik-tool-menu" label="&picnik.menu;" image="chrome://picnik/content/picnik_16x16.png"> <menupopup> <menuitem label="&picnik.grab_visible;" oncommand="picnikScreenGrab.grabVisible();"/> <menuitem label="&picnik.grab_full;" oncommand="picnikScreenGrab.grabFull();"/> </menupopup> </menu> </menupopup> </menu> <toolbarpalette id="BrowserToolbarPalette"> <toolbarbutton id="picnik-button" type="menu-button" class="toolbarbutton-1" label="&picnik.label;" tooltiptext="&picnik.menu;" oncommand="picnikScreenGrab.grabVisible();"> <menupopup> <menuitem label="&picnik.grab_visible;" oncommand="picnikScreenGrab.grabVisible(); event.stopPropagation();"/> <menuitem label="&picnik.grab_full;" oncommand="picnikScreenGrab.grabFull(); event.stopPropagation();"/> </menupopup> </toolbarbutton> </toolbarpalette> </overlay>
grabFull 和 grabVisible 这两个函数分别设置参数以抓取完整页面或可见区域。它们将大部分工作留给了恰如其分的 grab 函数。为了限制需要上传的数据量,grab 会缩放画布,使其在实际渲染窗口之前小于 2800x2800。接下来,grab 创建一个画布并渲染窗口,然后在通过 toDataURL 检索数据 URL。
saveDataUrl 的工作是实际将图像发送到 Picnik。它构造一个 multipart/form-data 请求,其中包含几个 API 参数以及图像数据。Picnik 的服务器响应一个浏览器要加载的 URL。当这种情况发生时,requestState 会以 req.readyState 为 4 调用。最后,requestState 使用该 URL 创建一个新选项卡。
大多数 Firefox 扩展都作为跨平台安装包 (.xpi) 在 Mozilla 的附加组件网站上分发。该站点为用户提供了一个扩展的中央可信来源。由于该站点已预先被浏览器信任,因此使用户可以更轻松地安装。附加组件站点还使用户可以轻松地为用户提供自动更新。您只需上传一个新版本,就会自动提示用户升级。
扩展向导创建了一个 shell 脚本 (build.sh) 来自动化创建 XPI 的过程。创建完成后,您可以将其发送给朋友,将其发布在您的博客上,将其上传到 addons.mozilla.org 或以您认为合适的任何方式分发它。
我希望本文使您了解了编写 Firefox 扩展是多么容易以及它们的功能有多强大。不要忘记,您可以通过使用 Chrome List 扩展来探索浏览器和其他扩展的内部工作原理来学习。此外,Mozilla Developer Center 拥有丰富的操作指南和参考资源等待您挖掘。祝您编码愉快!
资源
本文的代码,包括 screengrab.js(列表 4):ftp.linuxjournal.com/pub/lj/listings/issue160/9730.tgz
Extension Developers Extension: ted.mielczarek.org/code/mozilla/extensiondev
Console2: https://addons.mozilla.org/en-US/firefox/addon/1815
Chrome List: https://addons.mozilla.org/en-US/firefox/addon/4453
Venkman: https://addons.mozilla.org/en-US/firefox/addon/216
Firefox Extension Wizard: ted.mielczarek.org/code/mozilla/extensionwiz
Mozilla Developer Center: developer.mozilla.org
Mozilla Add-ons: addons.mozilla.org
Picnik API: www.picnik.com/info/api
Justin Huff 是一位具有嵌入式系统背景的软件工程师,在一家名为 Bitnik 的小型 Web 2.x 公司工作。他居住在华盛顿州西雅图。