install.rdf 文件
在上一节,我们看过了 Hello World 扩展的内容。现在我们从 install.rdf 文件开始,探究它的文件与代码。你可以使用任何文本编辑器打开它。
该文件为 RDF 格式,这是一种特殊格式的 XML 文件。RDF 曾是 Firefox 的核心存储机制,但现在正由一个更为简单的数据库系统替代。在未来的教程中,我们将同时介绍这两种存储系统。
现在我们来看下这个文件的重点部分。
<em:id>[email protected]</em:id>
这是扩展的唯一标示符。Firefox 需要它用来区分你的扩展与其它扩展,所以请确保你的 ID 唯一。
附加组件的 id 有两种为人们接受的标准。一种是 Hello World 样例中,类似 email 的格式,像 <project-name>@<yourdomain> 这样。另一种标准实践是使用一个生成的 UUID 字符串,通常它不可能重复。基于 Unix 的系统有一个命令行工具叫做 uuidgen 可以生成 UUIDs。也有些各平台通用的可下载工具能够生成。闭合括号只是标识,它们通常只、是惯例。只要你的 id 能唯一,使用任何一种方式均可。
<em:name>XUL School Hello World</em:name>
<em:description>Welcome to XUL School!</em:description>
<em:version>0.1</em:version>
<em:creator>Appcoast</em:creator>
<em:homepageURL>https://developer.mozilla.org/en/XUL_School</em:homepageURL>
这是扩展安装前显示的信息,安装后你可以在附加组件管理中查看。还有许多其它标签可以添加,比如贡献者和翻译人员。install.rdf 完整标准里有所有细节。
因为扩展可以被翻译成其他语言,所以通常有必要翻译扩展描述甚至名称。如下代码可以添加本地化的描述:
<em:localized> <Description> <em:locale>es-ES</em:locale> <em:name>XUL School Hola Mundo</em:name> <em:description>Bienvenido a XUL School!</em:description> </Description> </em:localized>
es-ES 区域字符串表示这是西班牙(ES)的西班牙语(es)。 你可以照自己需要添加更多<em:localized> 块。 至于 Firefox 2,本地化这个文件稍微复杂,我们在本节后面会讨论本地化。
<em:type>2</em:type>
这明确所安装的附加组件是扩展。你可以在 install.rdf 完整标准中了解到各种不同的可能类型。
<em:targetApplication> <Description> <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <em:minVersion>4.0</em:minVersion> <em:maxVersion>10.*</em:maxVersion> </Description> </em:targetApplication>
这个节点指定扩展的目标应用程序与目标版本,Firefox,4 -10 版本。UUID 是 Firefox 的唯一ID。其他 Mozilla 及基于 Mozilla 的程序如 Thunderbird 和 Seamonkey 也有他们自己的。你可以创建一个适用多个应用程序多个版本的扩展。比如,你创建了一个 Firefox 扩展,通常做些小调整就可以移植给 SeaMonkey,后者的特性和 UI 与 Firefox 很像。
最低和最高版本指定扩展允许安装的版本。这儿有更多的版本格式。如果应用或版本不匹配,你不会被允许安装扩展,又或者扩展安装了,但是被禁用。用户可以通过首选项禁用扩展的版本检测,又或者安装 Add-on Compatibility Reporter 扩展。 从 Firefox 11 开始,附加组件将默认兼容,而 Firefox 大部分时候会忽略版本范围。但是我建议你在每个 Firefox 版本上测试你的附加组件。
这些就是 Firefox 及其他 Mozilla 应用程序安装扩展需要的信息了。任何错误和信息缺失都会导致安装失败,或者扩展安装了但处于禁用状态。
chrome.manifest 文件
Chrome 是浏览器内容区域外的一组应用窗口用户界面元素。 工具栏、菜单栏、进度条和窗口标题栏是 Chrome 组成部分的典型例子。
摘自 Chrome 注册。
换句话说,chrome 是你在 Firefox 中看到的一切。所有能看到的 Firefox 窗口通常有两部分:(1) chrome,(2) 一个内容区域,比如这个在 Firefox 标签页中显示 web 页面的。下载窗口这种窗口是纯 chrome。大多数扩展代码是放在 chrome 的文件夹,就像 Hello World 的例子一样。
如我们在解压后的扩展文件结构中看到的,chrome 由 content、locale 和 skin 三个目录组成。大多数扩展都必须有这三个目录。如果我们打开 chrome.manifest 文件(同样的,任何文本编辑器都可以),我们可以看到同样的三个目录被提到:
content xulschoolhello content/ skin xulschoolhello classic/1.0 skin/ locale xulschoolhello en-US locale/en-US/
chrome.manifest 文件告诉 Firefox 哪儿找 chrome 文件。为了让文本看起来像表格,文本之间是分隔开的,但那不是必须的。分析器会忽略重复的空格。
每一行的第一个单词告诉 Firefox 它所声明的东西(content、skin、locale 或是后面会提到的其他)。第二个是包名,我们马上会解释。
Skin 和 locale 包都有第三个值,用于指定它们所继承的 skin 或 locale。针对不同的皮肤和区域,可以有多个 skin 和 locale 条目。最常见的是给全局皮肤定义一个皮肤条目,class/1.0,然后多个区域条目,每个翻译各一条。最后,指明位置。
chrome.manifest 文件里的条目还可以包含其他选项。它们都写在 Chrome 注册 页面。值得注意的是,我们可以写 OS 特定的条目。 这很重要,因为浏览器的外观在每个操作系统上区别很大。如果我们的扩展需要在不同系统下区别开,我们可以修改 manifest 文件,看起来像这样:
content xulschoolhello content/ skin xulschoolhello classic/1.0 skin/unix/ skin xulschoolhello classic/1.0 skin/mac/ os=Darwin skin xulschoolhello classic/1.0 skin/win/ os=WinNT locale xulschoolhello en-US locale/en-US/
这样,我们在 Windows、Mac OS X 和 Linux(及其他 unix 类系统)的皮肤就各不一样,它们分别定义在不同的目录下。因为大部分的其他系统都是基于 Unix,所以 "unix" 皮肤是默认的,没有标记。
Chrome
如前面提到的,chrome 由 content、locale 和 skin 三部分组成。content 是最重要的部分,包含用户界面(XUL)和脚本文件(JS)。skin 部分的文件定义 UI(使用 CSS 和图片,就像 web 网页一样)的外观与感受。最后,locale 部分包括扩展使用的所有文本,定义在 DTD 和属性文件里。这一块允许其他开发者创建主题,替换皮肤;允许译者做不同语言的本地化工作,所以这些都不需要修改你的扩展或代码。这给了 Firefox 扩展极大的灵活度。
Chrome 文件通过 chrome 协议访问,chrome URI 写法如下:
chrome://packagename/section/path/to/file
所以,举个例子,如果我想访问扩展中的browserOverlay.xul 文件,chrome URI 会是 chrome://xulschoolhello/content/browserOverlay.xul。
如果 content 中文件太多,然后你想将它们组织到子目录,你不需要在 chrome.manifest 里改什么,你只需要把正确的路径加入到 URI 中的 content 后。
Skin 和 locale 文件一样,另外你不需要指定 skin 或 locale 名称。所以,访问 Hello world 扩展中的 DTD 文件,chrome 路径是:chrome://xulschoolhello/locale/browserOverlay.dtd。Firefox 知道怎么找到 locale。
这儿有一个有趣的试验。打开一个 Firefox 的新标签页,在地址栏输入 chrome://mozapps/content/downloads/downloads.xul 并点击回车。 惊讶?你在 Firefox 标签页中打开了下载窗口!你可以通过地址栏输入 URI 访问任何 chrome 文件。如果你要检查 Firefox、其它扩展、或者你自有的一部分 js 文件,这就会很方便。除了 XUL 文件,大多数文件都以文本方式打开,XUL 文件则被执行然后显示,通常就是你在窗口中所看到的效果那样。
Content
content 目录中有两个文件。我们先来看 XUL 文件。
XUL 文件是 XML 文件,用于在 Firefox 和 Firefox 扩展中定义用户界面元素。XUL 受 HTML 启发,所以你会看到它们有许多相似之处。不过,XUL 相比 HTML 有诸多改进,从 HTML 进化过程中的失误决定中学到许多。相比 HTML 构建的界面,XUL 允许你创建更丰富也更有为互动的界面,或者说 XUL 至少让这一过程更轻松了。
XUL 文件通常定义两个东西:窗口或覆盖(overlay)。 你之前打开的文件,downloads.xul,就有定义“下载”窗口的代码。Hello World 扩展中所含的 XUL 文件是一个覆盖。一个覆盖扩展已有的窗口,或添加新元素,或替换旧元素。我们在 chrome.manifest 文件中跳过的一行表示这个 XUL 文件是主浏览器窗口的一个覆盖:
overlay chrome://browser/content/browser.xul chrome://xulschoolhello/content/browserOverlay.xul
通过这行代码,Firefox 知道用 browserOverlay.xul 覆盖浏览器主窗口(browser.xul)。在Firefox 中,你可以声明任何窗口或对话框的覆盖,但目前为止,对浏览器主窗口的覆盖更为常见。
现在我们来看看 XUL 文件内容,我们跳过前面几行,因为它们与 skin 和 locale 相关,我们稍后再说。
<overlay id="xulschoolhello-browser-overlay" xmlns="https://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
文件的根元素是一个 overlay。其它 XUL 文档使用 window 或 dialog 标签。根元素有一个唯一 id,大多数 XUL 中的元素都需要 id。第二个属性是命名空间,通常你应该始终定义在你 XUL 根元素。它表示这个节点其所有子节点都是 XUL。如果你要在一个文档中混合不同类型的内容,比如 XUL 与 HTML 或 SVG,你只需要修改命令空间的声明。
<script type="application/x-javascript" src="chrome://xulschoolhello/content/browserOverlay.js" />
和 HTML 一样,这引入了 JavaScript 脚本文件。你可以根据需要在 XUL 文件中使用许多脚本元素。我们稍后深入这些代码。
我们且跳过 locale 部分会说到的代码,看看内容中最重要的部分:
<menubar id="main-menubar"> <menu id="xulschoolhello-hello-menu" label="&xulschoolhello.hello.label;" accesskey="&xulschoolhello.helloMenu.accesskey;" insertafter="helpMenu"> <menupopup> <menuitem id="xulschoolhello-hello-menu-item" label="&xulschoolhello.hello.label;" accesskey="&xulschoolhello.helloItem.accesskey;" oncommand="XULSchoolChrome.BrowserOverlay.sayHello(event);" /> </menupopup> </menu> </menubar> <vbox id="appmenuSecondaryPane"> <menu id="xulschoolhello-hello-menu-2" label="&xulschoolhello.hello.label;" accesskey="&xulschoolhello.helloMenu.accesskey;" insertafter="appmenu_addons"> <menupopup> <menuitem id="xulschoolhello-hello-menu-item-2" label="&xulschoolhello.hello.label;" accesskey="&xulschoolhello.helloItem.accesskey;" oncommand="XULSchoolChrome.BrowserOverlay.sayHello(event);" /> </menupopup> </menu> </vbox>
这就是将 Hello World 菜单加入浏览器窗口的代码。
鉴于现有Firefox处理按菜单的方式,我们可以有两种看似相同的方法来实现。以前每个平台的Firefox都有一个菜单栏并且有若干菜单项,而最近的Firefox版本中,这些已经发生了变化,尤其是在Windows平台上,只有统一的并且简单的菜单操作才可见。所以在windows平台上使用Alt键来在菜单按钮和传统菜单间来回切换,第二个代码块就是描述菜单按钮的例子,第一个代码块描述的是其他例子。由于菜单按钮是最普遍的方法,所以,我们关注它。
为了写这些代码,我们需要了解关于browser.xul内中的XUL代码。我们需要知道菜单栏最右边窗格的id是appmenuSecondaryPane。我们将把自己定义的菜单加入到菜单栏,并且告诉Firefox,把我们定义的菜单按钮加到那个菜单栏Add-ons的右侧。这就是这个属性的目的。
insertafter="appmenu_addons"
appmenu_addons 就是主菜单中与 Add-ons 菜单项对应的id。我们随后会看到怎样来查看这些浏览器元素的id,但是现在我们看看构成Hello world菜单项的元素。
对于传统菜单,我们直接把 Hello world 菜单添加到菜单栏,这样你很容易看到它,但这不是推荐的做法。设想如果所有的扩展都向菜单栏添加菜单;安装几个扩展就会让它看起来像满是旋钮开关的飞机仪表盘。对于统一的菜单按钮,事情变得有点更复杂,因为你的选择更少了。如果你的菜单项放在“Web 开发者”中很合适,那么建议你就把它放在那。否则,放在主菜单可能是你唯一的办法。
对于传统菜单,一个推荐的放置菜单的地点是“工具”菜单里,这时代码应该是这样的:
<menupopup id="menu_ToolsPopup"> <menu id="xulschoolhello-hello-menu" label="&xulschoolhello.hello.label;" accesskey="&xulschoolhello.helloMenu.accesskey;" insertbefore="devToolsEndSeparator"> <menupopup> <menuitem id="xulschoolhello-hello-menu-item" label="&xulschoolhello.hello.label;" accesskey="&xulschoolhello.helloItem.accesskey;" oncommand="XULSchoolChrome.BrowserOverlay.sayHello(event);" /> </menupopup> </menu> </menupopup>
我们在覆盖 XUL 树更深层的菜单,但这并不影响,因为我们需要的只是我们想覆盖的元素ID。现在被覆盖元素是“工具”菜单里的弹出菜单元素。insertbefore属性告诉Firefox把这个菜单添加到开发工具菜单的后面,在下方分隔符的上面。在这个教程里,我们以后会谈到更多关于菜单的事情。
现在来看代码:
oncommand="XULSchoolChrome.BrowserOverlay.sayHello(event);"
这个代码定义了一个事件响应器,由于command事件是对许多UI进行的事件响应,所以它会在Firefox中经常使用,这个属性值就是要调用的JavaScript函数。这些函数定义在以script标记包括的JS文件中,一旦用户点击了Hello World菜单按钮,这个JS函数就会被立刻调用。所有的事件处理器都定义了一个特殊的对象叫event,通常能够很好的用来给这个函数传递参数,将会在后续章节深入讨论事件处理器。
现在,我们来看这个JavaScript文件,看看事件发生时会发生什么。
/** * XULSchoolChrome namespace. */ if ("undefined" == typeof(XULSchoolChrome)) { var XULSchoolChrome = {}; };
XULSchoolChrome 已经被定义了.所以我们在这个JavaScript文件中定义的所有对象和变量都都是全局的,意味着Firefox和其他扩展中的脚本都可以直接看到它们并与它们交互。如果我们定义了一个名为MenuHandler 对象(或者其它通用的名字,它看起来会和一个已经存在的对象产生冲突)。我们在这里仅仅是定义了一个单独的全局对象: XULSchoolChrome。现在我们知道所有的对象都属于这个全局对象,并且不会造成重复或者被其他扩展重定义覆盖。
你可以阅读更多关于 typeof operator的知识。如果你对JavaScript或者这种特殊的语法不熟悉,你会发现通过{}初始化一个对象与用new Object()初始化一个对象是等价的。
/**
* Controls the browser overlay for the Hello World extension.
*/
XULSchoolChrome.BrowserOverlay = {
最终, BrowserOverlay 就是我们使用的对象。用这么冗长的名字来命名看起来非常复杂,但是却很值得我们这样做。
sayHello : function(aEvent) { let stringBundle = document.getElementById("xulschoolhello-string-bundle"); let message = stringBundle.getString("xulschoolhello.greeting.label"); window.alert(message); }
那么,最后,这是我们的函数声明(定义?)。我们只需3行代码来让它工作。函数体的第一行声明了一个变量,它会储存覆盖层中定义的 stringbundle 元素。这个变量是用 let 声明的,let 与 var 类似但作用域更小。你可以阅读关于 let 声明的更多信息。
就像在普通的 JS 中那样,我们可以使用DOM (Document Object Model,文档对象模型)来控制 XUL 文档。首先我们获得文档中 stringbundle 元素 的引用。这是一个特殊的元素,它允许我们动态地获得本地化的字符串,只需要一个标识字符串的”键“。这是我们在第二行代码中做的事情。我们调用 stringbundle元素的 getString 方法 来获取要显示的本地化消息。然后我们用message参数调用window.alert,就像在HTML文档中所做的那样。
Locale
有两种文件可以用来定义locale文本:DTD文件和properties文件,在这个demo里面我们都用到了。DTD是效率最高的方式,所以你可以随便使用这种DTD文件定义locale文本。但是DTD文件有其自身的局限之处,它里面定义的文本不随加载动态生成,所以当你需要获取DTD中的文本时需要一定的手法。
我们继续来看菜单的代码,注意这几个属性(attributes):
label="&xulschoolhello.hello.label;" accesskey="&xulschoolhello.helloItem.accesskey;"
这几个属性定义了你在菜单里面能看到哪些文本,这些都是在DTD文件(browserOverlay.dtd)中定义的,注意这两个string keys(label和accesskey)的定义格式。然后DTD文件是这样被include到XUL文件中的:
<!DOCTYPE overlay SYSTEM "chrome://xulschoolhello/locale/browserOverlay.dtd" >
在DTD文件中你可以看到这些string keys是如何对应到具体语言的字符串的:
<!ENTITY xulschoolhello.hello.label "Hello World!"> <!ENTITY xulschoolhello.helloMenu.accesskey "l"> <!ENTITY xulschoolhello.helloItem.accesskey "H">
注意,在XUL文件中string key要用 & 和 ; 括起来,而在DTD文件中则是直接写的。如果格式不对你可能会遇到解析错误(parsing errors)或者本地化错误(incorrect localization)。
Access keys are the shortcuts that allow you to quickly navigate a menu using only the keyboard. They are also the only way to navigate a menu for people with accessibility problems, such as partial or total blindness, or physical disabilities that make using a mouse very difficult or impossible. You can easily recognize the access keys on Windows because the letter that corresponds to the access key is underlined, as in the following image:
Most user interface controls have the accesskey attribute, and you should use it. The value of the access key is localized because it should match a letter in the label text. You should also be careful to avoid access key repetition. For example, within a menu or submenu, access keys should not be repeated. In a window you have to be more careful picking access keys because there are usually more controls there. You have to be specially careful when picking access keys on an overlay. In our case, we can't use the letter "H" as an accesskey in the Main menu item, because it would be the same as the access key in the Help menu. Same goes with "W" and the Window menu on Mac OS. So we settled on the letter "l".
DTD strings are resolved and set when the document is being loaded. If you request the label attribute value for the Hello World menu using DOM, you get the localized string, not the string key. You cannot dynamically change an attribute value with a new DTD key, you have to set the new value directly:
let helloItem = document.getElementById("xulschoolhello-hello-menu-item"); // The alert will say "Hello World!" alert(helloItem.getAttribute("label")); // Wrong helloItem.setAttribute("label", "&xulschoolhello.hello2.label;"); // Better helloItem.setAttribute("label", "Alternate message"); // Right! helloItem.setAttribute("label", someStringBundle.getString("xulschoolhello.hello2.label"));
This is the reason DTD strings are not a solution for all localization cases, and the reason we often need to include string bundles in XUL files:
<stringbundleset id="stringbundleset"> <stringbundle id="xulschoolhello-string-bundle" src="chrome://xulschoolhello/locale/browserOverlay.properties" /> </stringbundleset>
The stringbundleset element is just a container for stringbundle elements. There should only be one per document, which is the reason why we overlay the stringbundleset that is in browser.xul, hence the very generic id. We don't include the insertbefore or insertafter attributes because the ordering of string bundles doesn't make a difference. The element is completely invisible. If you don't include any of those ordering attributes in an overlay element, Firefox will just append your element as the last child of the parent element.
All you need for the string bundle is an id (to be able to fetch the element later) and the chrome path to the properties file. And, of course, you need the properties file:
xulshoolhello.greeting.label = Hi! How are you?
The whitespace around the equals sign is ignored. Just like in install.rdf, comments can be added using the # character at the beginning of the line. Empty lines are ignored as well.
You will often want to include dynamic content as part of localized strings, like when you want to inform the user about some stat related to the extension. For example: "Found 5 words matching the search query". Your first idea would probably be to simply concatenate strings, and have one "Found" property and another "words matching..." property. This is not a good idea. It greatly complicates the work of localizers, and grammar rules on different languages may change the ordering of the sentence entirely. For this reason it's better to use parameters in the properties:
xulshoolhello.search.label = Found %S words matching the search query!
Then you use getFormattedString instead of getString in order to get the localized string. Thanks to this we don't need to have multiple properties, and life is easier for translators. You can read more about it on the Text Formatting section of the XUL Tutorial. Also have a look at the Plurals and Localization article, that covers a localization feature in Firefox that allows you to further refine this last example to handle different types of plural forms that are also language-dependent.
Skin
Styling XUL is very similar to styling HTML. We'll look into some of the differences when we cover the XUL Box Model, and other more advanced topics. There isn't much styling you can do to a minimal menu and a very simple alert message, so the Hello World extension only includes an empty CSS file and the compulsory global skin file:
<?xml-stylesheet type="text/css" href="chrome://global/skin/" ?> <?xml-stylesheet type="text/css" href="chrome://xulschoolhello/skin/browserOverlay.css" ?>
The global skin CSS file holds the default styles for all XUL elements and windows. Forgetting to include this file in a XUL window usually leads to interesting and often unwanted results. In our case we don't really need to include it, since we're overlaying the main browser XUL file, and that file already includes this global CSS. At any rate it's better to always include it. This way it's harder to make the mistake of not including it. You can enter the chrome path in the location bar and inspect the file if you're curious.
This covers all of the files in the Hello World extension. Now you should have an idea of the basics involved in extension development, so now we'll jump right in and set up a development environment. But first, a little exercise.
Exercise
Change the welcome message that is displayed in the alert window and move the Hello World menu to the Tools Menu, where it belongs. Repackage the XPI and re-install it. You can just drag the XPI file to the browser and it will be installed locally. Test it and verify your changes worked. If you run into problems at installation, it's likely that you didn't reproduce the XPI structure correctly, maybe adding unnecessary folders.
.XPI
. Do not zip the containing folder, just its contents. The content
folder, chrome.manifest
, install.rdf
, and other files and directories should be at the root level of your archive. If you zip the containing folder, your extension will not load.Keep in mind that on Firefox 4 and above, on Windows and some Linux distributions, the Tools menu is hidden by default. It can be enabled using the ALT key.
Once you're done, you can look at this reference solution: Hello World 2.
This tutorial was kindly donated to Mozilla by Appcoast.