Mozilla的架构(收集)

Mozilla的架构

原文发表于《程序员》2007第5期,BLOG首发。

Firefox的横空出世在很多人的意料之外,其体积小巧功能强大,在安全性、扩展性和可移植性上都有惊人的表现。这样优秀的软件,实在想不出是在 mozilla这棵老树上生出的新花。我们都认为mozilla老了,已经过时了,不是吗?如果不是firefox,我们还记得mozilla吗?

最近因为工作需要,我花了不少时间去研究Mozilla。才知道我的想法错了,mozilla的架构设计非常优秀,不但没有老,而且在持续的焕发出生机。不是别的,正是它优秀的架构孕育了firefox的诞生。

浏览器的复杂度可以和操作系统相提并论,mozilla有数百万行的C++代码。这样复杂的软件,其中到底使用了那些秘技呢?其实,好的设计总是遵循一些众所周知的设计准则,套句俗语说,差的设计千差万别,好的设计都一个样。Mozilla当然也是遵循了这些设计准则,不过,在实现方法上却有很多创新之处,让人惊叹不已。

本文就以这些设计准则为主线,来分析一下Mozilla的架构设计:

1. 分离界面和实现。
我们知道,用户界面是最容易变化的,也是最难于自动测试的,分离用户界面和内部逻辑是设计的主要目标之一。在这一方面,mozilla算是非常前卫了:用标记语言(XUL)开发界面,用编程语言来实现(C++)内部逻辑,再用脚本语言(javascript)把两者胶合起来。XUL的界面描述能力,javascript的简洁性和C++的性能完美的结合在一起了,mozilla把三者的长处发挥到了极致。它们的关系如下图所示:

o XUL 这是一种用XML来描述用户界面的语言。用XML描述用户界面已经不是什么新鲜事了,像Qt designer和Glade都是用XML文件格式来存放用户界面描述的,但它们都只是纯粹的界面描述。而XUL同时描述了事件处理、风格(style) 和字符串国际化等信息,可以直接被mozilla layout引擎解析执行。

o XBL 这种称为扩展绑定语言(Extensible Binding Language)的东东也是mozilla的一大特色,现在已经被W3C作为标准了。作为程序员,我们都知道公共函数库的重要性,公共函数库可以反复重用,从而提高开发效率。在开发用户界面时,也会遇到同样的问题,很多界面都比较类似,拷贝/粘贴当然很容易,但以后维护起来就麻烦了。而XUL并没有提供重用机制,XBL刚好弥补了它的不足。在XUL中可以只描述具有共性的部分,而由XBL对它进行扩展。XBL的功能强大,自身也有组合和继承机制,这大大提高了可重用性。

o CSS 我们知道Cascading Style Sheets在网页中已经应用多年了,而在浏览器本身实现中使用倒是很少听说。这也没有什么奇怪的,像GTK+中的RC和CSS功能都差不多, 也就是说GNOME应用程序一直都在使用类似于CSS的东西。有了CSS,把应用程序的界面视感(look and feel)与功能独立开来,让两者可以独立变化,这是非常自然的事了。不过CSS在这里,除了可以修改界面风格外,还可以把XBL和XUL关连起来,以完成对XUL的扩展。

o DTD DTD (Document Type Definition)常用来定义标记(Markup Langugae)语言的语法,功能上与BNF是等价的。不过它在这里,不是为了定义某种语言的语法,而是完成字符串的本地化,只是借了DTD中的实体 (Entity)展开机制罢了。这看起来有些大材小用,不过在XML中使用DTD实体来替换要翻译的字符串,没有比这更好的办法了。

o property 在XUL中用DTD来做字符串本地化,虽然是妙着一招,可是在javascript里它就没有用武之地了。这回该轮到property上场了,在 nsIStringBundle接口的帮助下,javascript可以方便的从property文件中取到所要的字符串。

o Javascript 在XPConnect的支持下,Javascript也可以用来开发COM组件,可以实现任何功能。不过胶合用户界面(XUL)和内部逻辑才是它最拿手的好戏。当然,其中文档对象模型(DOM)起了非常关键的作用,Javascript通过文档对象模型(DOM)来操作XUL中的元素。

我们以minimo为例,来看看它的实现方法:

<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
   <?xml-stylesheet href="chrome://minimo/skin/minimo.css" type="text/css"?>

上面的代码指明了CSS的文件名和路径。global.css是控制应用程序整体风格的,minimo.css是控制minimo主界面风格的。 CSS的语法很简单,不需要任何编程知识就可以修改它。只要一个文本编辑器,你就可以根据你的爱好,去修改它的字体、颜色和背景图片等风格元素,而且无需重新编译就可以看到调整后的效果。

<!DOCTYPE window [
          <!ENTITY % minimoDTD SYSTEM "chrome://minimo/locale/minimo.dtd" >
          %minimoDTD;
          <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
          %brandDTD;
          ]>

上面的代码指明了DTD的文件名和路径。正如前面所说的,DTD是用来做字符串本地化的,而不是用来描述当前文件语法的。

  <stringbundleset id="stringbundleset">
         <stringbundle id="minimo_properties" src="chrome://minimo/locale/minimo.properties"/>
     </stringbundleset>

上面的代码指明了properties的文件名和路径。正如前面所说的,properties文件是用来处理javascript里字符串的本地化问题的。

要本地化吗?好办,把这两类文件交给翻译人员就行了。如果你还在把字符串放在可执行文件中,甚至直接嵌到代码里,还在为不同的语言各编译一个版本,那就太老土了,向mozilla学学吧。

<tabbrowser id="content"
                    contentcontextmenu="contentAreaContextMenu"
                    disablehistory="false"
                    accessrule="access_content"
                    src="about:blank"
                    flex="1"
                    autocompletepopup="PopupAutoComplete"
   onnewtab="BrowserOpenTab()"/>

上面的代码就是真正的界面描述了。我们知道mozilla里面,它也像IE一样,把浏览器核心做成了一个控件,可以嵌入到应用程序自己的窗口中,minimo就是利用这种机制实现的。这段代码完成了浏览器控件的创建、属性设置和事件关联。简单吧,更重要的是,无须重新编译就看到效果。

  <script type="application/x-javascript" src="chrome://minimo/content/minimo.js"/>
     <script type="application/x-javascript" src="chrome://minimo/content/common.js"/>
     <script type="application/x-javascript" src="chrome://minimo/content/rssview/rssload.js"/>
     <script type="application/x-javascript" src="chrome://minimo/content/bookmarks/bmview.js"/>

上面的代码指明了javascript的文件名和路径。按照分层原则,上层可以调用下层的函数,用户界面(XUL)在上层,它可以调用javascript函数的,所以这里需要指明javascript文件的位置。

<menuitem id="command_back" command="cmd_BrowserBack" label="&backCmd.label;"  />
   <menuitem id="command_forward" command="cmd_BrowserForward" label="&forwardCmd.label;"  />

   <menuitem id="item-back"    hidden="true"  label="&backCmd.label;"      command="cmd_BrowserBack" />
   <menuitem id="item-forward" hidden="true"  label="&forwardCmd.label;"   command="cmd_BrowserForward"/>

function BrowserBack()
{
  gBrowser.webNavigation.goBack();
}
 function BrowserForward()
{
  gBrowser.webNavigation.goForward();
}

上面几行代码是Back和Forward菜单的界面描述和事件处理。这是一个典型的例子:用XUL描述用户界面,用XPCOM组件完成内部逻辑 (gBrowser是通过DOM接口取得的:gBrowser = document.getElementById(“content”)),用javascript完成对用户界面和内部逻辑的胶合。

2. 针对接口编程。
Mozilla整个设计是基于组件对象模型(COM)的,而组件对象模型(COM)的主要特点就是针对接口编程。在《设计模式》中,作者把针对接口编程作为设计的首要准则。针对接口编程,把模块的使用者和实现者之间的耦合降到最小,以达到信息隐藏和隔离变化的目的。而且在XPCOM的帮助下,组件可以动态替换或者增加,具有更强的灵活性。其中各个角色的关系如下图所示:

工厂(Factory) 我们知道,可以通常接口指针调用接口的实现,这样调用者就不必了解实现者的细节了。然而创建接口的实例时,只有知道接口的实现才能创建接口的实例。这是一个矛盾,如何才能把使用者和实现者完全隔离开来呢?那就要靠工厂(Factory),它把接口的实现和创建独立开来了。可以说工厂(Factory)是针对接口编程的基本条件之一,这就是为什么《设计模式》把工厂(Factory)模式放在前面介绍的原因了。

在Mozilla中,每个组件都要实现工厂(Factory)接口,才能被使用者调用。考虑到实现Factory的代码极为类似,为了避免重复,Mozilla实现了一个称为GenericFactory的通用Factory。在GenericFactory的帮助下,各个组件只要实现一个简单的结构ComponentInfo就行了。

组件管理器 工厂(Factory)虽然把组件的创建分离了,但使用者与实现者的工厂却又耦合起来了。这样看来,我们似乎又回到了起点,不过这次不一样了,各个组件的 Factory都实现了IFactory接口,可以由一个专门的管理器来管理。通过组件名称或ID可以找到组件的Factory,通过接口ID又可以让 Factory创建接口的实例。

IDL IDL是接口定义语言 (Interface Definition Language) 的简称,它在组件对象模型(COM)中也起着重要的作用。在C语言中,我们可以用结构(Struct)来描述接口,一个只包含函数指针的结构 (Struct)可以看作一个接口。在C++中,我们可以用类来描述接口,一个只包含纯虚函数的类可以看作一个接口。既然每种语言都有自己描述接口的方法,为什么还要IDL呢?答案正是因为每种语言描述接口的方式不一致,所以才要需要一个独立于任何编程语言接口定义语言(IDL)。

IDL是一个中间语言,它不能直接被编程语言使用,但在IDL的编译器的支持下,它可以被转换成不同的形式,在多种语言中使用。比如转换成头文件,可以在C/C++中使用,可以转换成类型库,可以在javascript等解释语言中使用。IDL在XPCOM实现语言无关性中,起了不可或缺的作用。

3. 分层设计
分层设计是常用的架构设计手法之一,它基于分而和治之的思想,可以有效的降低系统复杂度。分层设计同时还可以隔离变化,让上层模块不受到底层模块变化的影响,是开发跨平台软件的最强有力武器。在mozilla中,其分层架构视图如下所示:
NSPR是一套跨平台的运行库,实现了诸如线程、事件、信号量和文件等依赖于具体操作系统API的抽象,让上层模块不受具体操作系统的影响。其下有基于Linux、Windows和Mac等多种操作系统的实现。

o Gfx 是一套图形处理程序库,实现了诸如画线、多边形、填充、颜色、字体和打印等功能,让上层模块不受具体GUI的影响。其下有基于GTK、QT和Win32等多种GUI的实现。

o Widget 是一套窗口/控件程序库,实现了诸如窗口,事件、拖放、剪切板等功能,让上层模块不受具体GUI的影响。其下有基于GTK、QT和Win32等多种GUI的实现。

o XPCOM 实现了跨平台的组件对象模型(COM),这是mozilla的核心模块。

o JSEngine实现了javascript的解释器,以及与XPCOM组件之间的互相调用机制。

o ML Parser实现了各种标记语言的解析器,比如HTML、XUL、XBL、SVG和其它XML文件解析器。

o DOM 实现了文档对象模型接口,javascript可以通过这套接口操作文档。DOM是W3C定义的标准,支持DOM接口才能保证和其它浏览器的兼容性,让同样的javascript可以在不同的浏览器中运行。

o Network 实现了各种本地/远程协议,比如HTTP、FTP、about和file等协议。Mozilla还实现了SSL协议,保证数据传输的安全性。

o Layout Engine实现了界面的排版布局,支持HTML、XUL和SVG等文档的排版布局。它不但支持网页浏览,而且支持网页编辑。

o 最上层是用XUL和JS等界面元素开发出来的应用程序,像firefox和Thunderbird等。

4. 可扩展性。

对于浏览器来说,可扩展性也是非常重要的。由于它涉及的东西太多,专业的功能应该由专业的厂商去做,而不是全部由浏览器来实现。比如像flash播放器、视频播放器和pdf阅读器等等都应该从浏览器中分离出来。Mozilla提供了两种扩展机制,一种称为plugin,另外一种称为 extension。这可能有点让人混淆,我是这样理解的:
o plugin用来增强现有功能,比如PDF plugin可以阅读PDF文件,而media player plugin可以播放音/视频文件。Plugin都要求实现特定的接口。
o extension用来扩展新功能,这些功能可能与浏览器有关,也可能无关,像help extension就是用来实现帮助功能的。Extension不要求实现特定的接口。

5. 可移植性。

Mozilla是一套跨平台的系统,它可以运行在linux、windows和mac等操作系统上,可以运行在QT、GTK和Win32等GUI系统上。可移植性是mozilla是主要目标之一,开发人员在为此可谓费尽苦心:
o 实现了一套跨平台的组件对象模型(XPCOM),这使得mozilla可以利用组件对象模型(COM)的好处,又不局限于windows平台。
o 实现了一套可移植运行库,对各种操作系统的接口作了抽象,隔离上层模块对操作系统的依赖。
o 实现了widget和gfx两个库,前者负责窗口/控件的处理,后者负责图形/图像的绘制,对各种GUI系统作了抽象,隔离了上层模块对GUI的依赖。
o 实现了自己的国际化机制,包括编码转换、编码检测和字符串翻译等等。

6. 稳定性(内存问题检测)。

用C/C++开发应用程序,最麻烦的就是内存泄露和内存越界,即使有多年开发经验的老手也可能在此栽了跟头。有人说,有很多工具可以帮助检测内存问题啊。没错,但有两种情况可能让这些工具失效,一是GUI系统,它们通常使用了共享内存,大多数工具对此都无能为力。不信的话,你可以用valgrind 检查一下GTK应用程序试试。二是自己管理了内存,大型系统中,为了高效的使用内存,往往实现了自己的内存管理器,工具对此一无所知,自然帮不上忙。

Mozilla实现了自己的内存管理器,同时还实现多种内存问题检查机制,比如用boehm垃圾回收机制来检查内存泄露问题。当然,对于C/C++这个毛病,也是迫不得已,大家都在重复实现这些东西,却没有好的重用办法,这不怪mozilla。

Mozilla可以说是架构设计的典范,值得我们反复观摩和学习。但它规模庞大,深入研究需要大量的精力,笔者花了近两个月时间,所了解的内容仍然很有限,本文算是抛砖引玉吧,欢迎大家指正。

 

 

Mozilla系列——Mozilla技术架构简述

    Mozilla的技术体系主要分成三至四个层次,见下图的方框部分: 

    我们从最下面一层开始讲讲吧。

 
    Mozilla Platform Layer:Mozilla是一个跨平台的项目,因此,在最底下,是
对各种操作系统的平台适配层,让Mozilla在各个系统平台上都能运行起来。除了对
各种OS的适配层,这一层还包括Gecko、Necko等重要的引擎和一些基本功能包。(
Gecko是图形渲染引擎,Necko是网络引擎)。这一层的开发主要用C,以及C++。

    XPCOM Layer:跨平台组件层,对底层的功能进行封装,以组件的形式供其他XPCOM
组件和上层的javascript调用。XPCOM与COM是非常类似的技术,所不同的是COM注册
的环境是windows,XPCOM注册的环境是Mozilla,另外,XPCOM还有一套自己的书写
规范。XPCOM的开发语言主要是C++;用JavaScript也可以开发XPCOM,但是用的比较少。
Mozilla已经封装了很多实用的XPCOM组件,<XPCOM Reference>这本书里面列举了几乎
所有的组件和他们的使用方法,http://xulplanet.com/references/xpcomref/,不过
这本书似乎很久没有更新了。

 
    Chrome Layer:chrome的英文是金属“铬”,在这里是一种协议名称,也是对Mozilla
GUI部分的总称。所有的界面应用都是在这一层,也是最丰富多彩的一层。我们平时直接
感受和接触到的都是chrome层实现出来的冬冬。界面层用XBL技术定义了很多的界面组件,
我们也可以定义自己的界面组件,这些组件供XUL使用,用来定义界面的框架结构,再
配合JavaScript、CSS、RDF等就可以创建丰富多彩的界面效果,chrome层的文件最后会
被按照模块打成jar包,放在chrome目录下。chrome层主要的开发技术是XUL/XBL、
JavaScript、CSS、RDF、DTD等。这一层的东西我在后面专门作为一个topic来说明,
这里就不多说了。

 
    OK,Mozilla大致就分成这样几层,每一层都使用下一层提供的功能,每一层都
可以进行扩展和开发。这是对Mozilla进行横向切割划分的层次。
    如果进行纵向切割的话,Mozilla就可以被分成一个个的模块(module),module由
一部分XPCOM加一部分chrome构成。举例来说,browser就是一个module,他由browser.jar
和底层连接网络的XPCOM组件及其他一些组件;email模块,也是由界面部分加上一些pop3
和smtp的XPCOM组件构成。这些module并不是固定死的,它们是可以进行第三方开发的,
比如现在的很多Firefox插件,就是以不同的module加入到Firefox中来的,这些Module
也体现在访问时的时候的路径上,chrome://xxxx/content/....xul,这里的xxxx就是
module的名字。
    这里顺便说一下为什么说Mozilla是一个开发平台吧。
    我的理解,所谓平台,像windows、linux这样,它们是用驱动连接硬件设备,然后
向上提供一组API或者说服务,第三方可以对这API进行重新开发形成新的API,也可以利
用这些API和和服务进行应用开发,生产出运行在这些平台上的应用软件。
    同理,将这种模式运用到mozilla上,第三方完全可以基于mozilla开发出运行在
Mozilla环境中的应用程序。这里说的mozilla环境是一个广义的概念,包括从上面说的
最低层到最上层。Mozilla是一个开源的项目,我们可以得到它的所有代码,自然我们可
以对Mozilla平台的任何一个部分进行修改(当然,必须遵循MPL<Mozilla Public License),
比如底层的平台接口、Gecko、Necko等等;我们也可以开发自己的XPCOM,注册到Mozilla
环境中去,这个类似前面说的开发自己的API的概念;我们也可以利用这些XPCOM和底层的
功能开发自己的应用程序,类似前面说的开发应用软件。
    目前,基于Mozilla项目平台开发出的比较出名的产品有:mozilla、firefox、
Macintosh平台上的 AOL、Linux 上的 Galeon,以及 Netscape。另外,Amazon的图书
搜索器也可以看成是基于mozilla平台开发出的产品。将尺度再放宽一点说,mozilla和
firefox的很多插件都可以看成是基于mozilla平台开发出的产品。
    在www.mozdev.org上有很多基于mozilla平台进行的开源项目。

 

mozilla的分层IO架构

mozilla的IO实现是分层的,本质上和BIO是一样的,只是写法不同罢了,最上层,mozilla封装了一个结构体:
struct PRFileDesc {
    const PRIOMethods *methods;         //本层的IO函数的实现
    PRFilePrivate *secret;             
    PRFileDesc *lower, *higher;         //上下两层,如此所有的层次可以三个三个连成链表
    void (PR_CALLBACK *dtor)(PRFileDesc *fd);
    PRDescIdentity identity;            //标识
};
关于mozilla对分层io描述符的实现请参考nsprpub/pr/src/io/prio.c和nsprpub/pr/src/io/prlayer.c,特别值得一提的是最具有代表性的push操作:
PR_IMPLEMENT(PRStatus) PR_PushIOLayer(PRFileDesc *stack, PRDescIdentity id, PRFileDesc *fd)
{
    PRFileDesc *insert = PR_GetIdentitiesLayer(stack, id);
    if (stack == insert) {  //插入在当前IO栈的顶端
        PRFileDesc copy = *stack;
        *stack = *fd;
        *fd = copy;
        fd->higher = stack;
        stack->lower = fd;
        stack->higher = NULL;
    } else {  //插入在当前IO栈的任意位置
        fd->lower = insert;
        fd->higher = insert->higher;
        insert->higher->lower = fd;
        insert->higher = fd;
    }
    return PR_SUCCESS;
}
和OpenSSL的BIO_push是十分类似的,只是BIO_push只能在顶端push,是真正的push,而mozilla的实现命名为insert倒是更好。类似的,PR_CreateIOLayerStub和OpenSSL的BIO_new很类似,都是初始化一个新的“要插入”的io描述符,BIO_next在mozilla中直接通过结构体引用,即fd->lower,BIO中的BIO_METHOD实现了本层的io策略,而mozilla的prio实现中的PRIOMethods起着同样的作用。
     通过使用PRFileDesc以及prio所提供的众接口,你可以将一系列的io例程堆积在一起,形成一个io栈,这就是分层的思想。分层的IO可以很方便的加入任何自定义的过滤策略和数据加工策略。突然想到,windows操作系统的drivers目录下有一个叫做tcpip.sys的文件,而unix在/dev目录下亦有一个ip,tcp文件。

 

Mozilla研究—mozilla的目录结构

转载时请注明出处和作者联系方式:http://blog.csdn.net/absurd

作者联系方式:李先静

 

mozilla是一个以浏览器为中心的软件平台,它在我们平台中占有重要地位。我们用它来实现WEB浏览器、WAP浏览器、邮件系统、电子书和帮助阅读器等应用程序。为此,我最近花了不少时间去阅读mozilla的代码和文档,我将写一系列的BLOG作为笔记,供有需要的朋友参考。本文介绍mozilla的目录结构。

Accessible 对辅助功能的支持。这些功能主要为残障人士提供的,比如放大镜和屏幕阅读器等。Mozilla是基于GNOME的ATK实现的,要注意的是,它只是提供对辅助功能的支持,并没有实现这些功能,这些功能是由专门的应用程序实现的。

Caps 提供了一些根据安全设置等信息决定系统能力的接口,比如是否允许执行脚本,是否接受cookie,是否记住密码等等。

Chrome 一种本地资源访问协议,它提供了抽象的URL到物理文件之间的映射。这样,应用程序可按访问普通URL的方式去访问资源,而不必关心资源的物理位置。不过mozilla似乎没有用它,而用的是RDF下面那个实现。

Config Makefile的编译规则、configure生成的配置和其它一些用于编译的脚本。其中autoconf.mk是configure生成的配置,如果发现编译选项不对,可以看看这个文件。

Content 文档对象模型(DOM)的主要代码,各种负责标记语言的语法树的构建和表示。

xxxContentSinkxxx负责语法树的构建

xxxDocumentxxx和xxxElementxxx负责语法树的表示。

Db/dbm 数据库实现,估计主要是给邮件系统用的,minimo好像没有用到。

Docshell/Webshell 浏览器的总控模块,负责把各个模块协调起来。

Dom 文档对象模型的界面部分,负责比如窗口、焦点和事件处理等等。

Editor 编辑器的实现,支持text/textmail/html/htmlmail四种编辑类型。

Embedding 浏览器控件的实现,有基于不同GUI的封装。在Win32下实现成了activex控件,接口与IE Control类似吧。应用程序可以把浏览器控件嵌入到自己的窗口中,minimo就是按这种方式实现的。

Extensions 各种扩展的实现,像帮助系统和javascript调试器等等。

Gfx 对图形/图像处理的封装库,有基于GTK、QT和Win32等GUI的实现。

Widget 对窗口系统的抽象,有基于GTK、QT和Win32等的实现。

Intl 对国际化的支持,包括编码转换和字符串处理等等。

Ipc 一套进程间通信机制,在minimo里没有用到,所以没有仔细看。

Jpeg JPG图像编解码库。

Js javascript解释器和XPConnect的实现。

Minimo minimo应用程序。

Modules 压缩/解压、图像、注册表和plugin等等。其中libpr0n是对各种图像文件格式的包装,提供了mozilla框架需要的接口,如果图像处理不正常,不防从这里入手。

Netwerk 网络协议、mime、cookie、cache和格式转换。

Nsprpub 可移植的运行库。

Parser解析器,负责各种XML文件和HTML文件的解析,基于builder模式实现,调用content中的xxxContentSinkxxx去构建语法树。

Plugin 插件。

Profile 主要功能是管理各种配置文件的路径,它实现了nsIDirectoryServiceProvider,如果发现获取配置文件位置失败,可以看看nsProfileDirServiceProvider::GetFile。

Rdf 资源描述框架(RDF)的解析器等。这里面也实现了Chrome协议。

Security 安全相关的库如SSL等实现。

Storage 数据存储,可能是基于前面的数据库实现了mozilla需要的接口。

Toolkit 一些工具,如安装程序(installer)、扩展管理器和下载管理器等,主要是界面部分。

Xpfe 据说是一套应用程序框架,称为FrontEnd。里面有很多功能和toolkit提供的类似,不知是不是用来取代toolkit的。

Xpinstall 安装程序的实现。

Mail 邮件系统,各种邮件协议和邮件内容的解析器等。

Layout 界面排版布局,相当于mozilla实现的一套GUI系统。每个控件称为一个Frame,要实现Paint和HandleEvent等接口函数。如果显示或者事件处理不正常,可以看看对应Frame的这些函数。

View 视图和视图管理器。

Xpcom XPCOM的实现。

Uriloader 对协议的包装,并提供了一些附加功能:定义nsIWebProgressListener2接口用显示下载状态,调用StreamConv转换文件格式,起动外部分应用程序打开mozilla无法处理的文件等。

~~end~~

Mozilla研究—mozilla中的设计亮点

近几年我看了不少大型开源系统,它们每个设计得都很经典,而mozilla无疑是其中的佼佼者之一。好的设计总是遵循一些众所周知的设计准则,套句俗语说,差的设计千差万别,好的设计都差不多。Mozilla也一样,它也无非是遵循了一些好的准则,只是实现手段有些差异罢了。这里简单的说一说,就算是温故知新吧。

1. 针对接口编程。

Mozilla整个设计都是基于组件对象模型(COM)的,而组件对象模型(COM)的主要特点就是针对接口编程。在《设计模式》中,作者把针对接口编程作为设计的首要准则。针对接口编程的主要目的就是信息隐藏,隔离变化,把模块的使用者和实现者之间的耦合减到最小。而且在XPCOM的帮助下,组件可以动态替换或者增加,具有更强的灵活性。

2. 分离界面和实现。

在这一方面,mozilla算是非常前卫了:用标记语言(XML)开发界面,用编程语言来实现(C++)逻辑,而用脚本语言(javascript)把两者胶合起来,这可以说是把三者的长处发挥到了极致。以前我也有过类似的想法,当时刚进入手机行业,我发现各种GUI的不统一,导致手机程序移植非常困难。当时就想,如果应用程序的界面用HTML来写,应用程序的逻辑用C/C++写,界面在浏览器中运行,而应用程序的逻辑作为WEB服务器运行。再扩展一下HTTP协议,支持异步事件,完全可以实现传统的应用程序。后来发现mozilla早就实现了类似的东东,让我大跌眼镜。

另外mozilla也大量应用了观察者模式,这对分离界面和实现很有帮助。比如,网络操作通常都要花很长时间,在此期间要报告操作的当前状态。通过观察者模式,可以在长时间操作中更新状态,而又避免了操作与用户界面的耦合。

3. 可移植性。

Mozilla是一个跨平台的软件,它的基本运行平台有linux、windows和mac,实际上它还可以运行在其它unix平台上,所以可移植性是mozilla是主要目标之一。如果开发过跨平台软件,你就会知道开发可移植性软件是多么困难,特别是大型GUI应用程序。Mozilla在可移植性上可谓费尽苦心:

1)实现了一套跨平台的组件对象模型(XPCOM),这使得mozilla可以利用组件对象模型(COM)的好处,而又不限于windows平台。

实现了一套可移植运行库,对各种操作系统的接口作了抽象,隔离上层模块对操作系统的依赖。

2)实现了自己国际化机制,包括编码转换、编码检测和字符串翻译等等。

3) 对GUI作了抽象,实现了widget和gfx两个库,前者负责窗口/控件的处理,后者负责图形/图像的绘制。可以用不同的GUI作为实现后端。

4. 可扩展性。

对于浏览器来说,可扩展性也是非常重要的。由于它涉及的东西太多,专业的功能应该由专业的厂商去做,而不是全部由浏览器来实现。比如像flash播放、视频播放和pdf阅读等等都应该从浏览器中分离出来。Mozilla提供了两种扩展机制,一种称为plugin,另外一种称为extension。这可能有点让人混淆,我是这样理解的:

plugin用来增强现有功能,比如wml browser plugin可以把wml转换成html,而media player plugin可以播放音/视频文件。所有的plugin都要实现指定的接口。

extension用来扩展新功能,这些功能可能与浏览器有关,也可能无关,像help extension就是用来实现帮助功能的。Extension不必实现特定的接口。

5. 稳定性(内存问题检测)。

用C/C++开发的应用程序,最大的毛病就是容易出现内存泄露和越界,即使有多年开发经验的老手也可能在此栽了跟头。有人说,有很多工具可以帮助检测内存问题啊。没错,但有两种情况可能让这些工具失效,一是GUI系统,它们通常使用了共享内存,大多数工具对此都无能为力。不信的话,你可以用valgrind检查一下GTK应用程序试试。二是自己管理了内存,大型系统中,为了高效的利用内存,往往实现了自己的内存管理器,工具对此一无所知,自然帮不上忙。

Mozilla实现了自己的内存管理器,同时还实现多种内存问题检查机制,比如用boehm垃圾回收机制来检查内存泄露问题。当然,对于C/C++这个毛病,也是迫不得已,大家都在重复实现这些东西,却没有好的重用办法,这不怪mozilla。

Mozilla研究—mozilla能为我们做什么

Mozilla是一个庞大的系统,仅管在PC上来说,它对硬件的要求并不高,但对于手机平台而言,它占用的磁盘/内存空间,以及对CPU能力的要求就相当可观了。同时由于其复杂度太高,要灵活的运用它,将要花费不少的精力去研究它。很难说清这些开销是否值得,因为要视具体情况而定。不过,如果我们了解mozilla能为我们做些什么,对此看法可能会有些改变。

1. 作为WEB浏览器。

Mozilla本身就是以WEB浏览器为中心的,选择它作为WEB浏览器是很自然的事情了。Mozilla在浏览器方面的经验丰富,对标准兼容性非常好。它不但本身功能强大,支持CSS和Javascript等高级特性,而且架构设计精良,可扩展性极强,以后增加对flash的支持,对PDF的支持,对mediaplayer的支持等等,都非常方便。像dillo (http://www.dillo.org/),viewML(http://www.pixil.org/products.php) 等浏览器,虽然体积更小巧,但无论是从功能、兼容性,还是扩展性来看,都与mozillo没得比了。

2. 作为WAP浏览器。

WAP浏览器是smartphone必备的应用程序,尽管WML最终是要被XHMTL取代的,但目前仍然普遍存在,所以还是要支持的。专门为WML开发一款浏览器,一方面是工作量太大,我们目前没有这么多人力去做。另外由于WML的表现力是HTML的子集,如果有了HTML浏览器还要去开发一个WML浏览器,其工作完全是重复的,没有必要。最理想的办法,就是先把WML网页转换成HTML网页,然后在HTML浏览器上浏览WML网页。以前在做WML模拟器时,我做过类似的工作,所以最初我就打算这样做了,最近又发现mozilla已经有一个WML browser的项目了,它思路和我们的想法如出一辙,这下为我们省了不少事儿。

3. 利用其邮件编辑器。邮件编辑器也不是个小玩意儿,从头实现的工作量非常庞大,而且邮件在我们项目中也只是一个普通应用程序,我们不太可能为此花费太多精力。我们知道Mozilla有一个邮件客户端应用(Thunderbird),不过我们不打算移植Thunderbird,原因是:一方面它不适合在手机上使用,另外也不便于集成到我们的系统中来(它会破坏我们系统风格的一致性)。所以我们只是重用mozilla的编辑器和相关网络协议,这仍然为我们带来了很大的便利。mozilla的编辑器的可以编辑四种类型的文件:text、textmail、html和htmlmail,对我们来说已经足够了。

4. 实现电子书阅读器。

Mozilla提供了一种输入流转换的功能,只要文件能用HTML表现出来的,都可以转换成HTML格式,然后用mozilla来浏览。像text文件、rtf文件、chm和html,甚至word和pdf都可以用mozilla来浏览。利用mozilla的浏览器控件,我们可以方便的实现自己的电子书阅读器,对于不同的文件格式,只要写一个转换器就行了。

5. 实现功能强大的记事本。

利用Mozilla的HTML编辑器,我们可以实现功能强大的记事本,它可以插入图片、音频和视频文件。通过其插件系统,我们还可以实现现场图片编辑,录制音频和视频文件。其功能强大,甚至可以与ms word相媲美。

6. 实现帮助系统。Mozilla的帮助系统是作为一个扩展实现的,其功能强大,而代码规模小得令人惊讶。如果有必要,完全可以利用它实现自己的帮助系统。

当然,要充分利用mozilla的潜力,也不是那么简单的事,要花大量时间和精力去研究它才行。我会在后续的BLOG中介绍如何实现上述功能,请有兴趣的朋友关注。

~~end~~

 

 

Mozilla研究—XUL窗口创建和事件处理

转载时请注明出处和作者联系方式:http://blog.csdn.net/absurd

作者联系方式:李先静<xianjimli at hotmail dot com>

 

mozilla是一个以浏览器为中心的软件平台,它在我们平台中占有重要地位。我们用它来实现WEB浏览器、WAP浏览器、邮件系统、电子书和帮助阅读器等应用程序。为此,我最近花了不少时间去阅读mozilla的代码和文档,我将写一系列的BLOG作为笔记,供有需要的朋友参考。本文介绍窗口创建和事件处理。

窗口创建

1. 对于提示窗口,像javascript中的alert/confirm等函数所打开的。其过程如下:nsPromptService::DoDialogànsWindowWatcher::OpenWindowànsXULWindow::ShowModal。如果想要定制提示窗口的行为,比如在命令行下提示,可以重新实现nsIPromptService2/nsPIPromptService接口。

2. 对于正常窗口,其创建过程如下:nsWindowWatcher::OpenWindowà WindowCreator::CreateChromeWindow2ànsWindowConstructor。

窗口关闭

1. 在javascript中调用close函数关闭窗口。

2. 经XPConnect调用nsGlobalWindow::Close。

3. nsGlobalWindow::Close调用nsGlobalWindow::ReallyCloseWindow去关闭窗口。

事件注册

1. 在XUL文档解析完成之后,nsXULDocument::PrepareToWalk去创建所有的nsXULElement。

2. nsXULElement::Create调用nsXULElement::AddScriptEventListener去注册事件处理函数。

3. nsXULElement::AddScriptEventListener调用nsEventListenerManager:: AddScriptEventListener去注册事件处理函数。

4. nsEventListenerManager::AddScriptEventListener经nsJSContext,最终调用NS_NewJSEventListener去把javascript事件处理函数包装成nsIDOMEventListener,并注册到nsEventListenerManager中。

事件分发

1. nsWindow.cpp 中的xxx_event_cb之类的函数把GTK+的事件转发给nsWindow::OnXxxEvent函数。

2. nsCommonWidget::DispatchEvent把事件转发View注册的回调函数HandleEvent。

3. nsView.cpp: HandleEvent通过事件的Widget找到当前的View,再找到对应的ViewManager,然后通过nsViewManager::DispatchEvent分发消息。

4. nsViewManager通过PresShell把消息转发到对应的nsXULElement。

5. nsXULElement通过nsEventListenerManager把消息转发到nsJSEventListener。

6. nsJSEventListener::HandleEvent再经javascript解释器调用javascript的事件处理函数。

~~end~~

Mozilla研究—组件的创建过程

在《设计模式》一书中,作者把创建模式放在了该书的最前面,我想这一定是经过深思熟虑之后才决定的。试想,如果没有创建模式,接口的使用者必须要知道接口的具体实现,才能创建并使用它,那么接口的实现可能就要遍布整个系统了。如果是这样,那还能算是分离了接口与实现吗,还能算是针对接口编程吗?答案是否定的,所以创建模式尽管很简单,但它却是针对接口编程必要的手段。

而在组件对象模型(COM)中大量使用了创建模式中的工厂模式。在XPCOM中,几乎所有组件都是通过工厂(Factory)来创建的。一个组件可以实现多个接口,一个Factory可以创建多个接口,所以通常一个组件只要实现一个Factory接口就够了,而不必为每个接口实现一个Factory。

MSCOM的Factory接口叫作IClassFactory。记得当时有人说,这个名字取得不好。原因是IClassFactory生产的是对象而不是类,所以正确的名字应该叫IObjectFactory。呵,这倒是很有道理的。

在介绍mozilla的组件创建过程之前,让我们先回忆一下MSCOM组件的创建过程,这对理解XPCOM组件的创建过程也是有帮助的。

对于以动态库形式提供的MSCOM组件,必须export出下面这些函数:

1. DllGetClassObject 用来查询组件内的接口。

2. DllRegisterServer 向系统注册组件。

3. DllUnregisterServer 向系统注销组件。

4. DllCanUnloadNow 判断是否可以安全卸载组件。

为了让客户可以使用自己,组件首先要把自己注册到系统中去。注册过程实际上就是向注册表中写入一些信息,让客户可以找到组件对应的动态库。然后客户通过系统API(如CoCreateInstance)创建来组件的实例,而CoCreateInstance等API先从注册表找到该组件的动态库,再调用动态库DllGetClassObject函数查询到IClassFactory接口,最后通过IClassFactory去创建组件的实例

在mozilla中,XPCOM组件的创建与MSCOM组件的创建类似。这里的模块(Module)是一个物理实体(通常是动态库或JS包),一个模块(Module)内可以封装多个组件,而每个组件内又可以实现多个接口。

模块(Module)必须对外export出NSGetModule函数,该函数的功能是用来得到nsIModule接口的。nsIModule接口的主要函数如下:

1. GetClassObject用来查询组件内的接口。对应MSCOM的DllGetClassObject函数。

2. RegisterSelf向系统注册组件。对应MSCOM的DllRegisterServer函数。

3. UnregisterSelf向系统注销组件。对应MSCOM的DllUnregisterServer函数。

4. CanUnload判断是否可以安全卸载组件。对应MSCOM的DllCanUnloadNow函数。

mozilla实现了一套宏,一个通用的Module(nsGenericModule)和一个通用的Factory(nsGenericFactory),在它们的帮助下,模块只要实现一个nsModuleComponentInfo结构就行了,这大大简化了开发的繁杂度。nsModuleComponentInfo的成员仍然比较多,幸好通常只要提供mDescription、mCID、mContractID和mConstructor几个成员就行了。

nsGenericFactory 实现了nsIFactory接口,它只是对nsModuleComponentInfo的一个包装。NsIFactory的功能和MSCOM中的IClassFactory差不多,它需要提供下列接口函数:

1. CreateInstance 创建指定接口的实例。

2. LockFactory 加锁/解锁工厂。

有了以上的背景知识,mozilla的组件创建过程就不难理解了:

1. mozilla调用NSGetModule获取IModule接口。(初始化时)

2. mozilla调用IModule接口的RegisterSelf函数,向组件管理器注册组件。(初始化时)

3. 通过组件管理器查找组件的nsIFactory接口,然后通过组件的nsIFactory接口创建组件。

这样一来,组件的使用者和实现者之间耦合就降到最低了,两者独立变化而互不影响。

值得注意的是,在XPCOM中,组件的创建有两种方式:

1. 创建实例(CreateInstance)。这是普通的创建方式,每次都调用Factory::CreateInstance来创建新的实例。

2. 获取服务(GetService)。服务(Service)一词容易让人误解(我开始以为是跨进程的调用呢),其实这里的服务就是一个单件,也就是说该组件只有一个实例存在获取服务时,组件管理器发现如果先前已经创建过该组件的实例,就直接返回先前的实例,否则调用Factory::CreateInstance创建组件的实例,并保存该实例的引用以备后面再使用。

无论是以创建实例还是以获取服务的方式创建,对组件本身的实现没有太大的影响。只是如果按获取服务的方式创建,而且该服务可能在多线程环境下使用,那么组件要自己实现加锁保护。

~~end~~

Mozilla研究—组件加载机制

转载时请注明出处和作者联系方式:http://blog.csdn.net/absurd

作者联系方式:Li XianJing <xianjimli at hotmail dot com>

更新时间:2007-3-5

 

在传统意义下,模块(Module)通常是设计时的范畴,而组件(Component)则是指运行时的范畴。它们两者的关系与类和对象的关系极为相似。有时为了简单了起见,往往并不严格区分它们,在本文中也是如此。

在mozilla中,组件一般都用nsModuleComponentInfo结构来描述,这些结构为组件的查找和创建提供了必要信息。按组件加载方式来区分,组件可以分为四种类型。

1. 内置组件。这些组件实现了mozilla的核心功能,其中大多数都是不可或缺的,比如像内存管理、调试系统和错误处理等等。这些组件是在nsXPComInit.cpp中的components变量里定义,可以用配置参数来选择一些可选组件。在系统起动时,由NS_InitXPCOM3负责将这些组件注册到组件管理器。

2. 动态库组件。动态库组件封装在独立的动态库中,这些组件可能是mozilla内置的,也可能是第三方开发的。只要在动态库中导出 NSGetModule函数,就可以被mozilla当作组件加载。

nsNativeComponentLoader负责加载这类组件,它通过nsDll的GetModule函数调用动态库导出的NSGetModule,从而加载组件,并注册到组件管理器中。

3. 静态库组件。在Mozilla里,除了前面所说内置组件外的扩展组件,在通常情况下都是以动态库组件形式提供的,但有时为了提高时间和空间上性能,往往把这些组件静态编译进来。

把动态库组件变成了静态库组件,此时的加载方式也要相应变化。为了兼容动态与静态两种加载方式。Mozilla又实现了nsStaticComponentLoader来负责加载静态组件。

由于NsStaticComponentLoader和nsNativeComponentLoader都实现了nsIComponentLoader接口,所以无论是静态加载还是动态加载,对框架都没有太大的影响,这也是针对接口编程的好处之一吧。

4. JS组件。我们知道,组件对象模型 (COM)的一个主要特点就是语言无关性。从理论上说,你可以用任何一种语言编写COM组件,然后用另外一种语言来调用它。但Mozilla目前只支持javascript和c/c++两种语言编写的COM组件的互操作性(以后我会讲解其实现原理),也就是说可以用javascript来实现COM组件。

加载javascript编写的组件有别于用C/C++编写的组件,所以它需要一个专门的加载器:mozJSComponentLoader。与nsNativeComponentLoader和nsStaticComponentLoader一样,它也实现了nsIComponentLoader接口。

nsComponentManager里的mLoaderData管理了所有的nsIComponentLoader,可以通过函数AddLoaderType增加新的nsIComponentLoader实现。可以通过GetLoaderForType来获取已注册的nsIComponentLoader。

在nsComponentManagerImpl::Init里,已经把nsNativeComponentLoader和nsStaticComponentLoader加入mLoaderData了。由于mozJSComponentLoader本身也是一个组件,它要通过nsNativeComponentLoader或者nsStaticComponentLoader加载,然后才能使用。

当前用户第一次运行时,在nsComponentManagerImpl::AutoRegisterImpl里用AddLoaderType把mozJSComponentLoader注册到nsComponentManager中。第二运行时,则是在nsComponentManagerImpl::ReadPersistentRegistry里注册的。

 

编译 Firefox 其实很简单

     最近编译了一把Firefox,感觉编译Firefox 其实真的很简单。在编译之前,我也参考了一些文章,但发现那些文章不是过时了,就是错误的。于是决定把我的编译过程跟大家共享一下。废话少说,我的编译过程如下:

1) 安装VS 2008 + SP1。Express 或者Team Suite都可以,最少要把Visual C++ 的组件安装上。如果您想编译Firefox 2.0 的版本,您需要安装VS 2003;如果编译Firefox 1.5 需要VC6.0 + SP5,这个环境现在不好找了。

2) 到ftp://ftp.mozilla.org/pub/firefox/releases/ 下载要编译的Firefox版本。可以下载最新版本的。
3) 到http://ftp.mozilla.org/pub/mozilla.org/mozilla/libraries/win32/ 下载MozillaBuild 工具。这个工具含有Nsis和MinGW,编译Firefox全靠MinGW调用cl.exe了,Nsis工具用来给程序打包。可以下载最新版本的。

4) 解压缩Firefox 源代码

5) 安装MozillaBuild工具,其实MozillaBuild安装包也是一个压缩包,用7-zip一样可以解压缩。

6) 到E:\mozilla\browser\config 目录下,把mozconfig文件拷贝到E:\Mozilla\ 目录下。将下列内容加入mozconfig文件中:
mk_add_options MOZ_CO_PROJECT=browser
mk_add_options MOZ_MAKE_FLAGS="-j5"
mk_add_options MOZ_CO_MODULE="mozilla/tools/update-packaging"
mk_add_options MOZ_PACKAGE_NSIS=1
ac_add_options --enable-application=browser
ac_add_options --enable-update-channel=nightly
ac_add_options --enable-optimize
ac_add_options --disable-debug
ac_add_options --disable-tests
ac_add_options --enable-update-packaging

这些编译选项是可以按需修改的,具体请参照:https://developer.mozilla.org/en/Configuring_Build_Options
7) 下面就是正式编译了。首先,转到MozillaBuild安装目录下,这个目录中有一系列的bat批处理文件。对应着不同的VS版本。因我们使用的是VS 2008,所以双击start-msvc9.bat 文件。最后,需要输入编译指令。很简单,第一要将当前路径转到E:\Mozilla 路径下;第二就是用make工具编译:
cd E:\mozilla
make -f client.mk build


     约20分钟左右,Firefox就编译完成了。因此时CPU和硬盘都比较忙,最好别同时做别的。有时候编译过程中会Hang住,关掉控制台重来一下就可以了。

8) 编译好的程序放在了 E:\mozilla\dist\bin 下面,趁热双击Firefox.exe 看一看新鲜出炉的成果。值得提醒注意的是,官网和其他网站上描述的设定环境变量等操作,是根本不需要的(至少编译Firefox时不需要)。完全可以不理会。

   源代码编译出来的Firefox都是英文版本的,而且人家也不叫Firefox 而是Gran Paradiso。下面介绍一下给Firefox打语言包的方法:

1) 到ftp://ftp.mozilla.org/pub/firefox/releases/版本号/win32/xpi/ 目录下下载简体中文语言包插件zh-CN.xpi 到E:\Mozilla\dist\bin 目录下(别忘了把"版本号"替换成真正的版本号)。

2) 使用命令控制台(cmd.exe)转到E:\Mozilla\dist\bin 目录下, 运行firefox -install-global-extension zh-CN.xpi 安装语言包插件。

3) 启动Firefox,在地址栏输入 about:config 回车,设置Firefox的默认语言。


4) 将general.useragent.locale 的值由en变为zh-CN。重启Firefox。


5) 至此Firefox已经变为中文版本。大功告成!

以上内容在Vista + VS 2008 + SP1 下编译Firefox3.0 以上版本,在XP + SP2 + VS 2003 下编译Firefox 2.0 版本都测试通过。

利用VC创建XPCOM组件

创建准备

1、下载SDK

http://ftp.mozilla.org/pub/mozilla.org/mozilla/releases/mozilla1.7/

从如上路径,选择最近的版本下载

2、创建GUID

使用VC下的guidgen 生成GUID,例如90758A97-A6F3-4ea4-8953-16BD2EE3A977

3、创建接口文件定义

#include "nsISupports.idl"

[scriptable, uuid(90758A97-A6F3-4ea4-8953-16BD2EE3A977)]

interface IMyComponent : nsISupports

{

long Add(in long a, in long b);

};

创建控件

1、使用SDK下的xpidl.exe

在CMD中输入命令

xpidl -m header -I_DIR_ IMyComponent.idl will create the IMyComponent.h header file

xpidl -m typelib -I_DIR_ IMyComponent.idl will create the IMyComponent.xpt typelib file

请注意如果是在其他目录下,请在CMD里把SDK下的目录加在环境变量里,

D:\Program Files\Microsoft Visual Studio8\VC>PATH

= D:\gecko-sdk-i586-pc-msvc-1.8b1\gecko-sdk\bin;D:\gecko-sdk-i586-pc-msvc-1.8b1\gecko-sdk\idl;D:\gecko-sdk-i586-pc-msvc-1.8b1\gecko-sdk\include;%PATH%

或者可以在“我的电脑-属性-高级-环境变量”里修改path值。

如果不成功,请把xpidl,IMyComponent.idl,全部写成完整路径。

在这一步可能会出现缺少DLL的现象。

如出现此类情况,请在这里下载

http://ftp.mozilla.org/pub/mozilla.org/mozilla/source/wintools.zip

解压缩后请按照同样方法把…\wintools\buildtools\windows\bin\x86加如环境变量中。

执行通过,得到IMyComponent.h,IMyComponent.xpt 2个文件。

2、创建新文件

根据IMyComponent.h创建文件MyComponent.h,MyComponent.cpp,MyComponentModule.cpp。

/* MyComponent.h*/

#ifndef _MY_COMPONENT_H_

#define _MY_COMPONENT_H_

#include "IMyComponent.h"

#define MY_COMPONENT_CONTRACTID "@mydomain.com/XPCOMSample/MyComponent;1"

#define MY_COMPONENT_CLASSNAME "A Simple XPCOM Sample"

#define MY_COMPONENT_CID { 0x597a60b0, 0x5272, 0x4284, { 0x90, 0xf6, 0xe9, 0x6c, 0x24, 0x2d, 0x74, 0x6 } }

/* Header file */

class MyComponent : public IMyComponent

{

public:

NS_DECL_ISUPPORTS

NS_DECL_IMYCOMPONENT

MyComponent();

virtual ~MyComponent();

/* additional members */

};

#endif //_MY_COMPONENT_H_

/* MyComponent.cpp*/

#include "MyComponent.h"

NS_IMPL_ISUPPORTS1(MyComponent, IMyComponent)

MyComponent::MyComponent()

{

/* member initializers and constructor code */

}

MyComponent::~MyComponent()

{

/* destructor code */

}

/* long Add (in long a, in long b); */

NS_IMETHODIMP MyComponent::Add(PRInt32 a, PRInt32 b, PRInt32 *_retval)

{

*_retval = a + b;

return NS_OK;

}

/* MyComponentModule.cpp*/

#include "nsIGenericFactory.h"

#include "MyComponent.h"

NS_GENERIC_FACTORY_CONSTRUCTOR(MyComponent)

static nsModuleComponentInfo components[] =

{

{

MY_COMPONENT_CLASSNAME,

MY_COMPONENT_CID,

MY_COMPONENT_CONTRACTID,

MyComponentConstructor,

}

};

NS_IMPL_NSGETMODULE("MyComponentsModule", components)

编译控件

1、创建工程

使用VC,创建新的DLL工程,将IMyComponent.h, MyComponent.h,MyComponent.cpp,MyComponentModule.cpp添加到工程中。

2、工程配置

在tools->options->Directories中的include files和library files中将SDK中的include文件夹与lib文件夹添加进去。

在Project ->setting->link->input下添加4个lib文件:nspr4.lib, plds4.lib ,plc4.lib ,xpcomglue.lib

在Project ->setting->C++ ->general->Preprocessor definitions添加MYCOMPONENT_EXPORTS,XPCOM_GLUE

3、编译

生成MyComponent.dll,将其与IMyComponent.xpt放入Mozilla Firefox\components\文件夹下。

运行控件

1、 创建HTML文件

/* MyComponentTest.html */

<HTML>

<SCRIPT>

var FLDR_COMPONENTS = getFolder("Components");

var FLDR_PLUGINS = getFolder("Plugins");

var FLDR_PREFS = getFolder("Program","defaults/pref");

var FLDR_WINSYS = getFolder("Win System");

function MyComponentTestGo() {

try {

netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");

const cid = "@mydomain.com/XPCOMSample/MyComponent;1";

obj = Components.classes[cid].createInstance();

obj = obj.QueryInterface(Components.interfaces.IMyComponent);

} catch (err) {

alert(err);

return;

}

var res = obj.Add(3, 4);

alert('Performing 3+4. Returned ' + res + '.');

}

</SCRIPT>

<BODY>

<BUTTON ONCLICK="MyComponentTestGo();">Go</BUTTON>

</BODY>

</HTML>

将如上页面放在服务器上。

2、 测试页面

打开火狐浏览器,访问该页面,弹出相应结果。

基于Mozilla平台的扩展开发(续)----XPCOM组件篇

源代码下载:HelloWorld示例.rar

在《浅谈基于Mozilla ThunderBird的扩展开发》这篇入门文章中简单介绍了基于Mozllia平台进行扩展开发的基础知识,但仍然欠缺最为重要的一种武器---没错,XPCOM!这篇文章就是为它准备的。

XPCOM是什么?

这个问题不多做解释了,相信XPCOM对于了解COM技术的人来说很快就可以上手开发了,下列是Mozilla官方给出的一些XPCOM知识的入门资源:

个人尤其推荐IBM developerworks上那5篇文章。

使用已有的XPCOM

XPCOM的使用十分简单,Mozilla平台已经为我们提供了许多功能强大的XPCOM组件了,如果你需要某方面功能的组件,请先看看Mozilla平台下是不是已经有对应的了,别再“自己造轮子了“。

关于这方面也不打算再多说了,有兴趣的朋友可以阅读IBM developerworks下面这篇文章,《实战 Firefox 扩展开发》,相信通过这样一个图片批量下载工具的开发,就会对于Mozilla平台下已有的XPCOM组件的使用有所了解的。

So,what's next? 
    没错,自己如何开发XPCOM组件并在扩展中使用。网上对于这方面的资料不是很多,而且没有特别完整的示例,这就是我写这篇文章的目的所在,通过一个简单的XPCOM组件的开发全过程,展示XPCOM组件的内部细节。

项目目标:

组件要实现的功能非常简单,就只提供一个做加法的接口供客户调用。

long Add(in long a, in long b);

然后在扩展中调用这个加法接口。

准备工作

0,按照《浅谈基于Mozilla ThunderBird的扩展开发》这篇文章建立起开发扩展的基本环境。

1、下载Gecko SDK

http://ftp.mozilla.org/pub/mozilla.org/mozilla/releases/mozilla1.8b1/gecko-sdk-i586-pc-msvc-1.8b1.zip ,我们需要使用它来对IDL定义进行解释。

2、创建GUID

使用微软的的guidgen 生成GUID,例如b7b04070-45fc -4635- b219-7a172f806bee

4,从C:\mozilla-build\moztools-180compat\bin下拷贝libIDL-0.6.dll,glib-1.2.dll到\gecko-sdk-i586-pc-msvc-1.8b1\gecko-sdk\bin下,否则运行xpidl会报错.

开发XPCOM组件

1,创建接口文件定义

#include "nsISupports.idl"
[scriptable, uuid(b7b04070-45fc -4635- b219-7a172f806bee)]
interface IMyComponent : nsISupports
{
long Add(in long a, in long b);
};

2、使用Gecko SDK 的xpidl.exe
进入xpidl所在目录,在CMD中输入命令

xpidl -m header -I ..\idl  IMyComponent.idl(这里应该是IDL定义文件的实际路径)
xpidl -m typelib -I ..\idl  IMyComponent.idl

如果上面执行有问题的话,可以将

\gecko-sdk-i586-pc-msvc-1.8b1\gecko-sdk\bin;\gecko-sdk-i586-pc-msvc-1.8b1\gecko-sdk\idl;\gecko-sdk-i586-pc-msvc-1.8b1\gecko-sdk\include;

加入到环境变量的PATH里面去。

如果上述命令执行通过,在\gecko-sdk-i586-pc-msvc-1.8b1\gecko-sdk\bin就会得到IMyComponent.h,IMyComponent.xpt 这2个文件。

2、创建新文件

根据IMyComponent.h创建文件MyComponent.h,MyComponent.cpp,MyComponentModule.cpp。

/* MyComponent.h*/

#pragma once

#ifndef _MY_COMPONENT_H_
#define _MY_COMPONENT_H_

#include "IMyComponent.h"
#define MY_COMPONENT_CONTRACTID "@mydomain.com/XPCOMSample/MyComponent;1"
#define MY_COMPONENT_CLASSNAME "A Simple XPCOM Sample"
#define MY_COMPONENT_CID  {0xb7b04070, 0x45fc, 0x4635,{ 0xb2, 0x19, 0x7a, 0x17, 0x2f, 0x80, 0x6b, 0xee } }


class MyComponent:public IMyComponent
{
public:
    NS_DECL_ISUPPORTS
    NS_DECL_IMYCOMPONENT
    MyComponent(void);
~MyComponent(void);
};
#endif


/* MyComponent.cpp*/
#include "StdAfx.h"
#include "MyComponent.h"

NS_IMPL_ISUPPORTS1(MyComponent, IMyComponent)

MyComponent::MyComponent(void)
{
}

MyComponent::~MyComponent(void)
{
}

NS_IMETHODIMP MyComponent::Add(PRInt32 a, PRInt32 b, PRInt32 *_retval)
{
*_retval = a + b;
return NS_OK;
}

/* MyComponentModule.cpp*/
#include "StdAfx.h"
#include "nsIGenericFactory.h"
#include "MyComponent.h"

NS_GENERIC_FACTORY_CONSTRUCTOR(MyComponent)

static nsModuleComponentInfo components[] =
{
{
        MY_COMPONENT_CLASSNAME, 
        MY_COMPONENT_CID,
        MY_COMPONENT_CONTRACTID,
        MyComponentConstructor,
    }
};

NS_IMPL_NSGETMODULE("MyComponentsModule", components) 

编译XPCOM组件

1、创建工程

使用VC2005,创建新的DLL工程,将IMyComponent.h, MyComponent.h,MyComponent.cpp,MyComponentModule.cpp添加到工程中。

2、工程配置

1)c/c++ GeneralAdditional Include Directories 中设置为\gecko-sdk-i586-pc-msvc-1.8b1\gecko-sdk\include
2) c/c++Preprocessor Preprocessor Definitions中加入MYCOMPONENT_EXPORTS,XPCOM_GLUE
3)c/c++Code GenerationRuntime Library中设置为Multi-threaded DLL (/MD)
,这里非常重要,否则编译会报错的!!。
4)LinkerAdditional Liberary Directoryse设置为\gecko-sdk-i586-pc-msvc-1.8b1\gecko-sdk\lib
5)Linker Additional Depenendies加入nspr4.lib plds4.lib plc4.lib xpcomglue.lib

3、编译生成MyComponent.dll。

在扩展中使用XPCOM组件

对《浅谈基于Mozilla ThunderBird的扩展开发》中的helloworld项目进行修改,加入一个文件夹components和一个安装文件install.js。
2008042501.jpg
   关于这两个东西具体的含义这里就不多做介绍了,简单点说,intall.js就是把XPCOM组件注册到Mozilla平台中去,就类似于Windows的注册表一样,从而可以使用组件。

1)Install.js的内容:

// Install script for helloworld

var err;
const APP_VERSION="0.0.0.1";//版本号

//初始化安装
err = initInstall("helloworld"+APP_VERSION,  // name for install UI
"/helloworld",               // registered name
                  APP_VERSION);              // package version
if(err!=0)
{//安装出错,取消安装
    cancelInstall(err);
}

//标准目录
var fProgram = getFolder("Program");//程序根目录 
var fChrome     = getFolder("Chrome");//chrome目录
var fComponents = getFolder("Components");//components目录

// workaround for Mozilla 1.8a3 and newer, failing to register enigmime correctly
var delComps = [ "compreg.dat" ]; // Components registry
for (var j=0; j<delComps.length; j++)
{
var delFile = getFolder(fComponents, delComps[j]);
if (File.exists(delFile))
        File.remove(delFile);
}

err = getLastError();
if (err == DOES_NOT_EXIST)
{
// error code: file does not exist
    resetError();
}
else if (err != SUCCESS) 
{
    cancelInstall(err);
}

// addDirectory: blank, archive_dir, install_dir, install_subdir
addDirectory("", "components",    fComponents, "");
addDirectory("", "chrome",        fChrome,     "");


err = getLastError();
if (err == ACCESS_DENIED)
{
    alert("Unable to write to components directory "+fComponents+".\n You will need to restart the browser with administrator/root privileges to install this software. After installing as root (or administrator), you will need to restart the browser one more time, as a privileged user, to register the installed software.\n After the second restart, you can go back to running the browser without privileges!");
    cancelInstall(ACCESS_DENIED);

}
else if (err != SUCCESS)
{
    cancelInstall(err);

}
else
{
// Register chrome
    registerChrome(PACKAGE | DELAYED_CHROME, getFolder("Chrome","helloworld.jar"), "content/helloworld/");
    err = getLastError();
if (err != SUCCESS)
{
      cancelInstall(err);
    }
else
{
      performInstall();
    }
}

2)在Componts文件夹中加入MyComponent.dll和IMyComponent.xpt

3)修改overlay.js如下:

// This is the main function
const ENIG_C = Components;
const ENIG_ENIGMAIL_CONTRACTID = "@mydomain.com/XPCOMSample/MyComponent;1"
var gEnigmailSvc = null;

function helloWorld()
{
try
{
        alert("准备创建组件");
        gEnigmailSvc = ENIG_C.classes[ENIG_ENIGMAIL_CONTRACTID].createInstance(ENIG_C.interfaces.IMyComponent);//创建实例
if(gEnigmailSvc!=null)
{
            alert("创建组件成功");
            gEnigmailSvc = gEnigmailSvc.QueryInterface(ENIG_C.interfaces.IMyComponent);
        }
else
{
            alert("创建组件失败");
return;
        }
var res = gEnigmailSvc.Add(3, 4);
        alert('Performing 3+4. Returned ' + res + '.');
        alert("创建结束");
    }
catch(ex)
{
        alert("error");
    }
}

好了,到此就完成了这个最简单的XPCOM组件的开发了,enjoy it!

Reference

1,利用VC创建XPCOM组件

2, http://enigmail.mozdev.org/home/index.php

需要完整代码的,请发email至phinecos@163.com,也欢迎有兴趣的朋友们一起来交流Mozilla的扩展开发技术

作者:洞庭散人

出处:http://phinecos.cnblogs.com/

本博客遵从Creative Commons Attribution 3.0 License,若用于非商业目的,您可以自由转载,但请保留原作者信息和文章链接URL。

基于 Mozilla 的扩展开发

目录

前言

我在今年才刚开始接触 Firefox 和“扩展(Extension)”这些概念,许多的内容也是在学习中探索。我感觉到,由于这些技术在国内很少有系统的介绍,引入的书籍性翻译资料几乎为零,只有在国内几个知名程序员的 Blog 上有些介绍,其余寥寥无几。苦于没有现成的中文资料可供参考,我不得不在英文很差的情况下,翻看了部分国外的资料,并且通过读源代码的方式来增加自己对扩展开发的理解。

出于开源开发的思想,我想把自己已有的开发经验做些总结,并在未来加入自己新的理解,维护好这篇文档。希望这篇文档能对你 Mozilla 下的开发有所帮助,我将尽我之力将复杂的技术阐述清楚。

在此,我要特别感谢我所在的亿邮公司和领导,没有他们的支持,我不可能利用工作时间对已知的技术做如此系统的整理;我还要感谢我的同事,没有他们的“锱珠必较”,我也不可能将那些复杂的技术描述得比较通俗易懂。同时,我还也要感谢你的阅读,如果你能对此文档的不足之处有所补充,或对错误提出指正,此文档将会被更好的维护,谢谢!

第一章 技术介绍

扩展开发(Extension development)是由于 Mozilla 技术的实现而被引入的。那段浏览器大战的是是非非,我不想做过多的介绍。Netscape 浏览器源代码在被送到开源组织之后,非盈利性的“Mozilla基金会(Mozilla Foundation)”成立了。也正是在重写了 Netscape 的源代码之后,才有的 Mozilla 浏览器,而所有的这些技术也正是基于这个“脱胎换骨”的 Mozilla Suit 而来的。直到近几年,Mozilla 基金会为了使 Mozilla 浏览器更加精简高效,它的开发者们决定将浏览器和邮件客户端程序独立出来,Mozilla Firefox 和 Mozilla Thunderbird 等就此产生。

(注:以下除非特殊声明,对基于 Mozilla 内核的应用程序一律用 Mozilla 来称呼;对 Firefox 或 Thunderbird 等程序只做特殊称呼,又由于它们也是基于 Mozilla 内核的应用程序,所以它们也可用 Mozilla 来称呼。)

扩展的开发也多是基于以上提到的几个应用程序的,扩展程序的目的是为了增强浏览器本身的功能。因为 Mozilla 本身只支持一些浏览器所具有的普遍的功能,显然不能满足一些用户的特殊需求。但由于 Mozilla 本身的框架非常好,它所提供的资源已经远远超出了一个浏览器的范畴,更多的情况下倒像是一个“平台”。因为它提供了大量的开发接口,高度的模块化和可扩展性。所以,这就为我们在其下进行各种开发提供了很强的基础。下面对 Mozilla 所支持的各种技术做一些简单的介绍。

1.1 扩展开发所涉的技术

XUL:它是“XML 化的用户界面语言(XML User Interface Language)”的缩写,这是一种以平台无关性为目标,用来描述用户界面的语言,现在被广泛地应用于 Mozilla 平台。再有,Mozilla 本身的界面就是用 XUL 进行描述的。

CSS:它是“层叠样式表(Cascading Style Sheets)”的缩写,这是一种可以通过规则来控制 HTML/XUL/XML 等显示外观的语言。

DOM:它是“文档对象模型(Document Object Model)”的缩写,这是一个允许通过脚本来动态访问和更新 HTML/XML 文档的内容,结构和样式的接口。

XPCOM:它是“跨平台组件对象模型(Cross -platform Component Object Model)”的缩写,它很像微软的提出的组件模型技术,但它是跨平台的,即其运行环境可以不依赖于某种特定的操作系统平台。

XPConnect:一种将 XPCOM 与 JavaScript 连接起来的技术。该技术允许组件被脚本化,而且能够用 JavaScript 来进行组件的开发。

XBL:它是“可扩展绑定语言 (Extensible Binding Language)”的缩写。

RDF:它是“资源定义框架(Resource Definition Framework)”的缩写。Mozilla 使用这种文件格式来保存扩展的注册信息和描述信息等。

JavaScript:由 Netsacpe 公司的 Brendan Eich 创造的一种解释型语言,它主要用来进行基于浏览器下的脚本应用开发。因为 Mozilla 内置了 JavaScript 解释器,所以使用 JavaScript 开发扩展,就成为编程语言的首选。虽然,你也可以使用 C++,Perl,Python 等进行扩展的开发,但这些语言的先天优势都明显不足。

对于扩展开发来说,必须掌握的技术有 XUL,CSS,DOM,XPCOM,JavaScipt,而对于 XBL,XPConnet,RDF 做简单了解即可。我们可以看到,做扩展开发涉及到了这么多的技术,那它们都分别负责什么功能呢?

1.2 各种技术所负责的功能

当我们在自己的机器上安装了某个扩展之后,我们可以看到,那些扩展工作起来就像是浏览器的一部分,与浏览器一起工作得很协调。虽然以上的许多技术都是由 W3C 这样的互联网组织提出的,但 Mozilla 却用它们来做桌面开发。因为,这些扩展确切来说全都是程序,是一种面向桌面应用的程序,而不是网页一类的东西。

有了以上的这个观点,就不难解释它们所负责的功能了,下面分别给予解释。

XUL,CSS 都是用来负责控制程序的界面,一个是用来描述界面,一个用来在被描述的界面上加入一些界面效果(如:字体颜色,是否透明,边框大小等)。可以看出,它们更多的功能是负责与用户打交道,所以用户在评价某个扩展程序的好坏时,多半会提及它的界面设计是否合理。如果想要使我们的扩展与用户交互得更合理,还需要你对应用于桌面开发的人机交互有所了解;

DOM 主要用来为 JavaScritp 提供一个 HTML/XML 的文档操作接口,并且,它也可以用来操作 CSS。由于扩展的界面是由 XUL 定义的,而 XUL 只是 XML 的一个特殊应用,所以我们也可以通过 DOM 来对扩展界面进行“动态”操作(如:按钮的禁止与否,动态装载数据等)。同时,又有许多的文件和数据会采用 XML 进行存储和传输,所以创建和分析 XML 文档又显得尤为重要。可以看出,通过 DOM 接口,我们可以将程序的逻辑处理部分与界面表现部分有机的结合起来。

JavaScipt 是扩展开发的核心要求,它主要用来实现程序的业务逻辑描述。在很长一段时间里,大家都对 JavaScript 有着不正确的认识。直到最近一段时间,才有所改观。确切的说,JavaScipt 在扩展开发中起着粘合剂一样的功能。利用 JavaScript 开发扩展有着先天的优势,因为 Mozilla 本身除了底层部分是用 C++ 编写以外,大量的代码全部采用 JavaScript 编写。再有,由于 JavaScript 是非编译性的,所以即使你下载的是安装包程序,你仍然可以查看并修改其中的 JavaScript 源代码,这一点符合 Mozilla 的开源特性。

虽然 JavaScript 看上去很简单,但用 JavaScript 开发大型程序,却对开发人员本身提出了很高的要求。因为 JavaScript 是一种比较灵活的语言,就当前版本的 JavaScript 来说,其本身并没有提供很好的进行大型开发的结构,所以就要求开发人员必须具备很强的能力,对那些复杂的结构进行模块化的处理。如果仅仅使用面向过程进行开发,就会在开发时“捉襟见肘”,这就要求开发人员必须采用面向对象的方式进行开发。可以说,从网页下的 JavaScript 开发到扩展下的 JavaScript 开发,是一种由轻量级到重量级的转变。

由于 JavaScript 语言只内置了几个与本地访问无关的对象,而对于桌面开发来说,显然不能满足要求。因为桌面开发需要访问大量的本地和网络资源,包括文件,剪贴板,Socket,浏览器本身等各种资源。而 XPCOM 为面向桌面的开发提供了这种可能,并且它使开发出的扩展程序可以跨平台的运行,而不用依赖于某个特殊的操作系统。只有使用 XPCOM,我们的扩展才可以做出实用的功能,没有 XPCOM,本地与远程的资源整合可以说是不可能。虽然扩展开发是用 JavaScript 来做的,但每个封装的对象或函数可能都要调用 XPCOM 对象来完成特定的功能。可以这么说,离开 XPCOM,我们寸步难行。

要想对以上的技术依次做很深入的解释简直是太难了,因为每种技术本身和它的应用都足以写上厚厚的一大本书。我现在还不打算做这么系统的工程,我打算从一个示例入手,将这个看上去很复杂的问题说得简单点儿。

第二章 开发平台的准备

“工欲君欲善其事,必先利其器”。Firefox 就是我们的开发环境,因为所做的开发都是基于 Firefox 的。那么首先,我们要配置好所在的开发环境。合理而有效地配置好整体的开发环境和指定的项目环境,可以令你的开发快速而高效。本章节将讲解如何配置整体的开发环境,后面的章节还将对指定项目的环境配置给予讲解。

2.1 安装开发工具

首先,我们需要下载一个开发平台。虽然 Mozilla Suit 在很长一段时间内还将被使用,但它将不再是扩展开发的重点对象,因此作者希望你的开发环境是 Firefox。那么首先,你需要在自己的机器上下载并安装 Firefox 浏览器。对于开发人员来说,我们有必要在第一时间了解到 Firefox 的更新状态,下面的链接地址可以让你快速地定位新版本的下载位置。

http://ftp.mozilla.org/pub/mozilla.org/firefox/releases/

由于作者在写这篇文档时,Firefox 正存在着高版本和低版本的区别,这是由于它分别采用了主分支和子分支的内核造成的。在这种情况下,你可能需要下载两个不同版本的 Firefox 进行开发。但是,如果你对扩展的开发还不熟悉的话,作者不建议你这么做,我希望你下载一个公认的比较稳定的版本,并且下文提到的那些扩展在上面工作得很好。

当你下载完成并进行安装时,你需要选择“自定义(Custom)”安装模式。因为在这种模式下,我们可以选择并安装“开发工具(Developer Tools)”,这一点对于开发人员尤其重要。接下来,我们要下载并安装辅助开发的“工具性扩展”。下面,先对这些工具性扩展进行简单的介绍。

JavaScript Debugger:又被称为 Venkman,它是 Mozilla 平台下的脚本调试工具。Firefox 下的网页及扩展开发,都要求使用 JavaScrit 的调试工具。其实不仅仅是开发扩展,对于步入某一未知开发领域的开发人员,掌握并使用语言调试工具都可以起到快速入门,有效开发的功效,这一点是无庸置疑的。

Console Filter:JavaScript 控制台(JavaScript Console)的信息辅助过滤工具。由于我们在开发过程中,难免会出现这样或那样的编写错误,而这些信息都被写入到 JavaScript 控制台中。通过这个工具,我们可以在 JavaScript 控制台中对那些错误信息进行有效的过滤和定位,这样就可以加快解决错误的速度。

Component Viewer:XPCOM 组件查看器。通过此工具,你可以了解到 Mozilla 系统下已经安装的所有 XPCOM 组件信息,适合高级别的开发。

Extension Developer:辅助开发扩展的扩展,用来帮助你建立扩展开发项目。它提供了一套工具用来满足 Mozilla 的扩展开发要求,虽然实际上它并不是很好使。在此,作者只建议你使用其中的部分功能,如:“Reload all Chrome”。因为此工具的“Extension Builder”功能还有一些问题,并且不利于你了解扩展的注册机制。

以上提到的这些扩展,你可以在下面的两上网站下载和获取到帮助信息:

安装完成之后,你需要熟悉一下这些扩展的使用方法。这些扩展中,最难掌握的可能就是 JavaScript Debugger 了。你可能需要很长的时间来摸索它的使用技巧,并且随着你对它的熟悉,你会发现不太好使的 Venkman,功能其实是很强大的。而 Console Filter 工具则被集成到 JavaScript 控制台里,并且它还提供了一些简单的过滤命令,你可以在它的发布页面里看到详细的使用方法。对于 Extension Developer 工具,作者经常使用它的“Reload all Chrome”功能。此功能可以让 Firefox 重新装载所有的功能代码,以此使 Firefox 重置为启动状态,这包括所有的扩展代码和组件代码;这样我们就不必在修改代码错误之后,重新启动 Firefox 了,因为只有在重新启动时,那些被修改过的代码才会生效。

2.2 创建 Profile 及配置环境变量

Firefox 是一个允许创建多个 Profile 的浏览器,不同的 Profile 被分别进行管理。其实,不同的 Profile 被对应到不同的配置目录,由配置目录的子目录和文件来维护这个 Profile 的设置。对于不同操作系统和 Mozilla 平台来说,这个目录是有所区别下的,下面列出 Firefox 的 Profile 目录在不同操作系统下的位置,其它类型的 Mozilla 平台,类推即可。

操作系统
Proile 对应的目录

Windows 9x/Me
C:\Windows\Application Data\Mozilla\Firefox\Profiles\<Profile name>\

Windows 9x/Me, alternate
C:\Windows\Profiles\<Windows login/user name>\Application Data\Mozilla\Firefox\Profiles\<Profile name>\

Windows NT 4.x
C:\Winnt\Profiles\<Windows login/user name>\Application Data\Mozilla\Firefox\Profiles\<Profile name>\

Windows 2000/XP
C:\Documents and Settings\<Windows login/user name>\Application Data\Mozilla\Firefox\Profiles\<Profile name>\ 或
%APPDATA%\Mozilla\Firefox\Profiles\<Profile name>\

Unix
~/.mozilla/firefox/<Profile name>/

Mac OS X
~/Library/Mozilla/Firefox/Profiles/<Profile name>/ 或
~/Library/Application Support/Firefox/Profiles/<Profile name>/

(注:文档的其它部分将使用 %profile% 来代表以上的 Profile 目录,使用 %app% 来代表 Mozilla 程序的安装目录。)

由于在开发扩展时,我们希望一个 Profile 用来做开发,一个 Profile 用来测试扩展的发布版本。所以,我们要配置不同的 Profile 来满足这种要求。

一般把默认的 Profile 做为开发的 Profile,所以,我们不用做什么特殊的设置。你可以把上面提到的那些扩展,全部安装到此 Profile 下。之后,关于开发的所有内容都将对此展开。

那么,我们还要创建一个新的 Profile 来完成扩展的安装,或者说是发布测试工作。通过下面的两个步骤,你可以实现这种需求。

首先,我们需要在命令行方式下,定位到 Firefox 的安装目录,通过运行

firefox -P

我们可以启动 Firefox 的 Profile 管理器窗口,利用此窗口,你可以建立一个负责测试安装的 Profile。当然,如果你还有别的要求,你也可以建立更多的 Profile。

然后,假设你通过上面的步骤建立了一个名为“test”的 Profile,如果你想启用这个 Profile,你需要通过

set MOZ_NO_REMOTE=1
firefox -P test

这两行命令来启用这个非默认启动状态的 Profile。很显然,这两条命令可以写成一个 Shell 脚本或做成一个批处理文件。那么,在你下次再启用这个 Profile 的时候就会轻松许多。

对于负责开发的 Profile,我们还要更改其中的某些设置项,以使其更利于扩展的开发。下面列出了几个需要更改的设置项,并且给出了解释。

  • javascript.options.showInConsole = true :将 chrome 类型的错误提示信息显示在 JavaScript 控制台中,这样可以方便扩展的调试;
  • nglayout.debug.disable_xul_cache = true :令 chrome 下的 XUL 修改不会被 cache,以便不重新启动 Mozilla 也可以查看到页面布局的更改;
  • browser.dom.window.dump.enabled = true :将 window.dump() 方法打开,这样 dump 信息就被打印在标准控制台上。此时,你还需要通过“-console”参数来启动 Firefox。只有这样,所有的 dump 信息才会被输出到标准控制台上;
  • javascript.options.strict = true :允许严格的 Javascript 警告出现在 JavaScript 控制台中。对于你自己的扩展,这样的设置可能会显示出更多的错误提示信息。

通过在地址栏中键入 about:config,你可以打开 Mozilla 内置的配置界面,这样你就可以更改那些设置项的值。在更改完这些设置项之后,我们会在以后的开发中更加得心应手。

第三章 扩展的结构及 Chrome 注册

上一章讲解了如何准备开发平台,但仅有开发平台还是不够的。本章就扩展程序的结构和 Chrome 注册机制,予以比较详细的讲解,只有当你对这部分的内容清楚以后,才能正确的对扩展项目进行配置。如果你对这方面已经非常清楚了,你可以跳过下面的内容。

3.1 扩展程序的结构

在 Mozilla 下安装的扩展程序是一种以 XPI 做为其扩展名的文件,实际上它只是一个 ZIP 格式的文件,扩展名不同而矣。所有的扩展程序,包括上一节提到的那些扩展都具有相似的内部结构。你可下载一个现成的扩展(比如:Venkman),将其扩展名改名成 ZIP,并对其解压缩,你就会看以其内部的组织结构。下面对其内部组织结构进行解释说明。

3.1.1 扩展的顶级结构

一个标准的扩展程序,解压缩后会生成以下几个目录:

  • chrome:Mozilla 规定扩展必须具备的目录。其下有一个 JAR 文件,此文件中保存着完成扩展主要功能的文件,后面将做更进一步的说明;
  • components:约定俗成的可选目录,用于存放自定义的 XPCOM 组件文件。由于大多数的扩展根本没必要自己定义 XPCOM 组件,因此,在没有自定义 XPCOM 组件的情况下,此目录是不用存在的;
  • defaults:负责存放一些默认的设置数据,其下还会包含子目录,以分别对默认数据进行存储;

另外,其下一般还会具备 3 个特殊的文件:

  • install.rdf:它是一个 RDF/XML 格式的文件,用于描述当前扩展的注册信息和附加信息等。扩展在安装时,负责安装扩展的程序会自动分析此文件的信息,然后将这些信息注册到 Mozilla 系统下。此文件必须被命名为 install.rdf,并置于扩展压缩包的顶级目录下;
  • install.js:负责安装扩展的脚本,此文件可选。一般情况下,install.rdf 完全可以胜任扩展的安装注册工作。但是,如果有些扩展要在安装时做一些额外的准备工作,则要通过一个称为 XPInstall 的机制来完成,那些负责额外工作的代码则要被固定地写到此文件中;
  • chrome.manifest:负责将扩展的各种包注册到 Mozilla 的 chrome 系统中。Gecko 1.8 内核新引入的机制,用来代替原有的 contents.rdf 文件;

如果你在别人编写的扩展中看到了除此之外的其它目录和文件,这应该是扩展开发者的一种个人行为,而不是必须的。

3.1.2 扩展的二级结构

在上提到的 chrome 目录下,会一个与当前扩展名称相近或相同的,扩展名为 JAR 的文件,此文件用来组织扩展的核心功能。第一章已经说过,编写一个扩展就像编写一个桌面程序一样,你需要构造扩展的外观并且编写完成逻辑功能的代码。那么,这些外观和代码就都被存储在此文件中。解压缩此 JAR 文件之后,一般会生成以下 3 个目录。

  • content:用于存储负责描述扩展界面的 XUL 文件和完成实际逻辑功能的 JS 文件;
  • locale:用于存储负责本地化处理的字符串数据文件,这些文件中的本地化字符串内容会被 content 目录中的文件所引用。如果某个扩展没有对本地化进行处理,那么它是可以省略的;
  • skin:用于存储负责美化界面外观的样式表文件和图片文件,这些文件中的样式和图片会被 content 目录中的文件所引用。如果扩展没有使用单独的样式表文件和图片,那么它也是可以被省略的;

其实,那些目录下的文件不一定是直接存储在它下面的,甚至它还有可能被存储在一个不相干的目录下。这是因为,Mozilla 下的扩展开发有一些固定的和约定俗成的东西。对于那些非固定的规定,你可以不遵守。就比如说,有的扩展可能将以上负责不同功能的文件混合放在 content 目录下。但是,作者不建议你这么做,这种方式只会给扩展的维护带来困难。

下面我们再来看一下这些目录下的所存储的内容:

  • content:目录下可能还会包含一个与扩展名称相同或相近的子目录,用这个子目录来存储以上提到的界面和代码文件。对于 locale 和 skin 目录,你可能也会看到再包含一个与扩展同名的子目录的规则;
  • locale:目录下还会有针对不同语言的子目录,这些子目录会被起成如“en-US”,“zh-CN”这种用来区分“语言-国家/地区”的名称。通过这种国际上标准的语言区分方式,Mozilla 会根据其自身的语言,选择一个最合适的语言目录让 content 中的文件进行引用。这样做的结果就是,同一个扩展,在编写了不同的语言包之后,它会根据 Mozilla 的语言来进行自适应。其实,这是 Mozilla 的功劳,我们仅仅是提供了不同的语言包文件,选择并适应的工作是由 Mozilla 来完成的;
  • skin:目录下还会有针对不同的 Mozilla 主题命名的目录,如“classic”,“modern”等。不过,一般情况下,我们只创建针对 classic 的“皮肤”。皮肤的适应方式与语言一样的,Mozilla 会根据当前的主题样式来选择一种最适合它的皮肤。

上面提到的这些目录让 Mozilla 的扩展组织结构显示得十分“冗余而复杂”。其实,这种看上去效率不高的组织方式却十分利于扩展程序的维护和降低耦合度。再有,在每一个直接存储文件的子目录下,你会见到一个固定的名为 contents.rdf 的文件,它是一个特殊的文件,格式同样是 RDF/XML 的,扩展的注册和工作都要靠此文件来完成。

3.2 contents.rdf 文件

我们先抛开那些形式上的目录结构,了解一下最重要的东西。content,locale,skin 这三个目录都被称为package),那么什么是包呢?在 Mozilla 下,package)就是一组文件集合,它的内容和功能就像上面对 content,locale,skin 等目录描述的那样。包可以被注册到 Mozilla 系统下,并且一旦被注册,它其中的文件就可以通过一种被称为 chrome 的地址协议进行访问。包可以包含任意类型的文件,这些文件可以被分别放置于不同的子目录下。包的表现形式既可以是目录也可以是 JAR 文件,但常以 JAR 做为表现形式,同时 contents.rdf 文件是必须的。那么,contents.rdf 的确切功能又是什么呢?

contents.rdf 文件就是用来分别描述这些包的,它描述了每种包的结构和负责完成的功能。确切的说,它其中的信息是为包的注册服务的。扩展在安装时,负责安装扩展的程序会分析它的内容,并将包注册到 Mozilla 系统中。只有在包被注册到 Mozilla 系统之后,它才可以进行正常的工作,才可以被通过 chrome 地址协议进行访问。本章的后面将对 chrome 地址协议和扩展的安装原理做更一步的解释,现在只说明 contents.rdf 文件的结构。

再有,由于基于 Gecko 1.8 内核的 Mozilla 程序采用一种新的方式来进行包的注册,所以 contents.rdf 其实已经被废弃掉了。新方式通过一个位于扩展顶级目录的 chrome.manifest 文件来完成同样的功能,它是一个格式十分简单的纯文本文件。但为了保证扩展能够兼容 Gecko 1.8 以前的版本,我们还要在扩展中使用 contents.rdf 文件格式,直到它真正的被废弃掉。

我们已经了解到,contents.rdf 的文件格式是 RDF/XML 格式的,它是一种通过 XML 描述的 RDF 格式。RDFResource Description Framework,译为“资源描述框架”)主要用来描述资源之间的关系,并且可以用许多格式来表示,但常用 XML 格式进行表示。Mozilla 也只是应用了这样一种格式来描述它的包资源。前面提到的 3 个包属于 3 种不同类型的资源,所以在进行描述时也会有所区别。并且这些包的描述格式是相对固定的,你完全可以通过修改下面的模板文件来生成你的 contents.rdf 文件。

3.2.1 适用于 content 包的 contents.rdf 文件

示例格式如下:

<?xml version="1.0"?>
<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
         xmlns:chrome="http://www.mozilla.org/rdf/chrome#">

  <!-- list all the packages being supplied by this directory -->
  <RDF:Seq about="urn:mozilla:package:root">
    <RDF:li resource="urn:mozilla:package:sampleext"/>
  </RDF:Seq>

  <!-- package information -->
  <RDF:Description RDF:about="urn:mozilla:package:sampleext" chrome:name="sampleext">
  <!--  additionally for Mozilla Suite and old Firefox/Thunderbird versions include:
        chrome:extension="true"
        chrome:displayName="Sample Extension"
        chrome:author="Your Name Here"
        chrome:authorURL="http://sampleextension.mozdev.org/"
        chrome:description="A sample extension with advanced features"
        chrome:settingsURL="chrome://sampleext/content/settings.xul" -->
  </RDF:Description>

  <!-- overlay information -->
  <RDF:Seq about="urn:mozilla:overlays">
    <RDF:li resource="chrome://browser/content/browser.xul"/>
  </RDF:Seq>

  <RDF:Seq about="chrome://browser/content/browser.xul">
    <RDF:li>chrome://sampleext/content/overlay.xul</RDF:li>
  </RDF:Seq>
</RDF:RDF>

下面对以上的一些格式做解释说明,先看一下它的文件头部。

<?xml version="1.0"?>
<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
         xmlns:chrome="http://www.mozilla.org/rdf/chrome#">

上面这一段格式相对固定,它主要是用来引入 RDF 和 chrome 命名空间。接下来

<!-- list all the packages being supplied by this directory -->
<RDF:Seq about="urn:mozilla:package:root">
  <RDF:li resource="urn:mozilla:package:sampleext"/>
</RDF:Seq>

这里的 packagesampleext 都需要注意。首先,package 用来说明它描述的是一个 content 类型的包。对于 locale 和 skin 类型的包,应把它替换成 localeskin,虽然这看上去与包目录的命名有些不一致。sampleext 是用来唯一标识扩展的名称,Mozilla 系统靠它来识别是哪个扩展的注册信息。后面还有好几处出现了同样的内容,你都需要注意。对于 locale 和 skin 则不能写能 locale:sampleext 或 skin:sampleext 这种“类推”出来的格式,它们的格式后面做说明。

文件的中间部分

<RDF:Description RDF:about="urn:mozilla:package:sampleext" chrome:name="sampleext">
<!--  additionally for Mozilla Suite and old Firefox/Thunderbird versions include:
      chrome:extension="true"
      chrome:displayName="Sample Extension"
      chrome:author="Your Name Here"
      chrome:authorURL="http://sampleextension.mozdev.org/"
      chrome:description="A sample extension with advanced features"
      chrome:settingsURL="chrome://sampleext/content/settings.xul" -->
</RDF:Description>

<!-- --> 标识中的内容已经说明,它是适用于 Mozilla Suite 和老版本 Firefox/Thunderbird 的附加信息,所以对于内核较老的 Mozilla 必须被写成:

<RDF:Description RDF:about="urn:mozilla:package:sampleext" chrome:name="sampleext">
      chrome:extension="true"
      chrome:displayName="Sample Extension"
      chrome:author="Your Name Here"
      chrome:authorURL="http://sampleextension.mozdev.org/"
      chrome:description="A sample extension with advanced features"
      chrome:settingsURL="chrome://sampleext/content/settings.xul">
</RDF:Description>

对于新版本的 Mozilla,<!-- --> 中的内容可以忽略。这段内容主要用来描述扩展的一些附加信息,如作者和扩展的显示名称等。其实在 install.rdf 文件中,存在同样的一段描述信息。因而,这里再做描述显得有些罗嗦,作者也不建议你在 contents.rdf 中加入那些附加的信息。

文件的结尾部分

<RDF:Seq about="urn:mozilla:overlays">
  <RDF:li resource="chrome://browser/content/browser.xul"/>
</RDF:Seq>

<RDF:Seq about="chrome://browser/content/browser.xul">
  <RDF:li>chrome://sampleext/content/overlay.xul</RDF:li>
</RDF:Seq>

这是 content 类型的 contents.rdf 文件所描述的关键部分。Mozilla 下的扩展之所以能够和浏览器整合在一起,像一个程序一样工作,是因为它采用了一种被称为“覆盖Overlays)”的技术。你的扩展可以在 Mozilla 的某个已有界面上,再组合上另外的 XUL 界面元素,而不会与之产生任何的冲突和不协调。这种技术与其说是“覆盖”,不如说是“合并”更合适。因为,原有的界面元素会根据加上去的界面元素自动调整自己的位置,以适应变化。同时,“覆盖”技术还为扩展程序的运行提供了“入口点”,这也正是我们编写的扩展能够运行的原因。

在上面的内容中,第一个 RDF:Seq 标记指明要对 Mozilla 的界面进行覆盖;而要对哪些界面进行覆盖,则通过它的子标记 RDF:li 标记进行描述,如:

<RDF:li resource="chrome://browser/content/browser.xul"/>

它意思是要覆盖负责描述浏览器界面的 browser.xul 文件,这个文件就是通过 chrome 地址协议进行指定的,后面你还将看到在许多情况下,我们都需要通过 chrome 地址协议来访问注册到 Mozilla 系统下的包资源。如果你还想覆盖其它的界面文件,你只需像上面一样指定多个 RDF:li 标识。

第二个 RDF:Seq 对那些被覆盖的文件做更进一步的描述,它描述了指定的目标文件会被当前包下的哪几个文件所覆盖。如:

<RDF:Seq about="chrome://browser/content/browser.xul">
  <RDF:li>chrome://sampleext/content/overlay.xul</RDF:li>
</RDF:Seq>

意思是用 overlay.xul 覆盖 browser.xul 文件,并且 overlay.xul 也是以 chrome 方式进行定位的,或者说是以已经注册后的地址进行定位的。如果还有更多的界面文件要覆盖到 browser.xul 上,照上面格式书写即可。

3.2.2 适用于 locale 包的 contents.rdf 文件

示例格式如下:

<?xml version="1.0"?>
<RDF:RDF xmlns:chrome="http://www.mozilla.org/rdf/chrome#"
         xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

  <RDF:Seq about="urn:mozilla:locale:root">
     <RDF:li resource="urn:mozilla:locale:en-US"/>
  </RDF:Seq>

  <RDF:Description about="urn:mozilla:locale:en-US"
          chrome:author="Me"
          chrome:displayName="English(US)"
          chrome:name="en-US">
    <chrome:packages>
      <RDF:Seq about="urn:mozilla:locale:en-US:packages">
        <RDF:li resource="urn:mozilla:locale:en-US:sampleext"/>
      </RDF:Seq>
    </chrome:packages>
  </RDF:Description>
</RDF:RDF>

我们可以看到,locale 包的 contents.rdf 文件的与 content 包的 contents.rdf 文件没有什么太大的区别,只有文件前面的 RDF:li,其 resource 中写的是 locale:en-US,注意区别前面 content 中的 package:sampleext。后面的 RDF:Description 格式相对固定,只是你要注意将 RDF:li 中的 sampleext 替换成与上面一致的扩展名称。另外,由于这一类型的 contents.rdf 是用来描述语言包的,所以,你必须要处理好相应的语言描述信息。

3.2.3 适用于 skin 包的 contents.rdf 文件

示例格式如下:

<?xml version="1.0"?>
<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
         xmlns:chrome="http://www.mozilla.org/rdf/chrome#">

  <RDF:Seq about="urn:mozilla:skin:root">
    <RDF:li resource="urn:mozilla:skin:classic/1.0" />
  </RDF:Seq>

  <RDF:Description about="urn:mozilla:skin:classic/1.0">
    <chrome:packages>
      <RDF:Seq about="urn:mozilla:skin:classic/1.0:packages">
        <RDF:li resource="urn:mozilla:skin:classic/1.0:sampleext" />
      </RDF:Seq>
    </chrome:packages>
  </RDF:Description>
</RDF:RDF>

我们可以看到,它与 locale 包的 contents.rdf 文件差不多,格式也很相近,只是 RDF:Description 标记中的属性少了一些。因为,这两种资源的描述几乎是采用一致的格式进行注册的,你只要注意 skin:classic/1.0 即可。

以上只是给出一些标准的格式,随着你对扩展开发的深入,你会发现一些特殊的格式及应用。另外,由于这个文件是 XML 的,所以作者提醒你保存成不带文件头标记(BOM)的 UTF-8 格式,并且建议你在创建完此文件之后,用浏览器再验证一下文件的格式和编码问题。

3.3 install.rdf 文件

install.rdf 文件位于扩展的顶级目录下,用于描述当前扩展的作者信息,升级地址,设置入口,版本兼容信息等。在安装扩展时,Mozilla 会分析其中的信息。它的标准格式如下:

<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
     xmlns:em="http://www.mozilla.org/2004/em-rdf#">

    <Description about="urn:mozilla:install-manifest">
        <em:id>{XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}</em:id>
        <em:name>Sample Extension</em:name>
        <em:version>1.0</em:version>
        <em:type>2</em:type>
        <!-- optional items -->
        <em:creator>Your Name Here</em:creator>
        <em:description>A sample extension with advanced features</em:description>
        <em:contributor>A person who helped you</em:contributor>
        <em:contributor>Another one</em:contributor>
        <em:homepageURL>http://sampleextension.mozdev.org/</em:homepageURL>
        <em:updateURL>http://sampleextension.mozdev.org/update.rdf</em:updateURL>
        <em:optionsURL>chrome://sampleext/content/settings.xul</em:optionsURL>
        <em:aboutURL>chrome://sampleext/content/about.xul</em:aboutURL>
        <em:iconURL>chrome://sampleext/skin/mainicon.png</em:iconURL>

        <!-- Firefox -->
        <em:targetApplication>
            <Description>
                <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
                <em:minVersion>0.9</em:minVersion>
                <em:maxVersion>1.5.0.*</em:maxVersion>
            </Description>
        </em:targetApplication>

         <!-- This is not needed for Firefox 1.1 and later. Only include this 
              if you want to make your extension compatible with older versions -->
        <em:file>
            <Description about="urn:mozilla:extension:file:sampleext.jar">
                <em:package>content/</em:package>
                <!-- optional items -->
                <em:skin>skin/classic/</em:skin>
                <em:locale>locale/en-US/</em:locale>
                <em:locale>locale/zh-CN/</em:locale>
            </Description>
        </em:file>
    </Description>
</RDF>

我们先看一下此文件的头部分:

<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
     xmlns:em="http://www.mozilla.org/2004/em-rdf#">

同 contents.rdf 文件的头部分一样,它也是负责引入命名空间。而位于头部分下面的 Description 标记,则是此文件的实质性描述。下面对 Description 下的子标功能给予说明。

3.3.1 必需的标记
3.3.1.1 em:id

它用来唯一的标识某个扩展,此标记内容的要求在 Firefox 1.5 版本的前后还发生了变化。在 1.5 以前的版本中,em:id 标记的内容被要求必须以 GUID 格式进行表示;而在 1.5 及后续版本中,你还可以使用一种格式为extension@domain 的 id。如下示例了两种不同的格式:

<em:id>myextension@mysite.com</em:id>
<em:id>{daf44bf7-a45e-4450-979c-91cf07434c3d}</em:id>

对于 GUID 格式的 id,我们可以通过一些工具来生成。在 Windows 下,我们可以用微软的 guidgen.exe 工具来生成。如果你在类 Unix 的系统下做开发,你可以用系统自带的 uuidgen 工具来生成。

3.3.1.2 em:name

此标记的内容会被显示在扩展管理器中。它被允许由多个单词组成,且中间可以包含空格。

3.3.1.3 em:version

此标记的内容是一个版本字符串,用来表示扩展的当前版本,它会被显示在扩展管理器中。示例如下:

<em:version>2.0</em:version>
<em:version>1.0.2</em:version>
<em:version>0.4.1.2005090112</em:version>

同时,Mozilla 还对版本字符串的格式做出了规定。如果这对这方面感兴趣,请你查阅 Mozilla 文档。

3.3.1.4 em:type

从 Firefox 1.5 版本以后,此标记被引入,它主要用来表示当前安装包的安装类型 (注意不要和包 [package] 弄混)。其实,在 Firefox 的 Theme 包结构里,同样要用 install.rdf 文件来描述其安装信息。而安装程序在处理安装时,要根据安装包的类型做不同的处理,此标记正是为区别安装包的类型设计的。

它的内容是一个整型值,并且只允许以下几个值出现:

2	Extensions
4	Themes
8	Locale
16	Plugin

对于扩展类型的安装包,type 标记的内容显然要被指定为 2。

3.3.1.5 em:targetApplication

此标记及其子标记用来指明所适用的 Mozilla 平台。由于你开发的扩展可能适用多个 Mozilla 平台,所以你需要确切地指定它所适用的平台。通过它的 em:id 子标记,可以指明所适用的平台;通过 em:minVersionem:maxVersion 子标记可以进一步所适用平台的版本范围。需要注意的是,如果你编写的扩展在安装时弹出了禁止安装的窗口,多半是由于 em:targetApplication 书写有问题造成的,特别是 em:maxVersion 设置过低的问题。

如果你编写的扩展适用于多个平台,你可以通过书写多个 em:targetApplication 标记来进行限定。下面将一些常用的 Mozilla 平台与 id 之前的对应关系列出:

Firefox			{ec8030f7-c20a-464f-9b0e-13a3a9e97384}
Thunderbird		{3550f703-e582-4d05-9a08-453d09bdfdc6}
Nvu			{136c295a-4a5a-41cf-bf24-5cee526720d5}
Mozilla Suite		{86c18b42-e466-45a9-ae7a-9b95ba6f5640}
SeaMonkey		{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}
Sunbird			{718e30fb-e89b-41dd-9da7-e25a45638b28}
Netscape Browser	{3db10fab-e461-4c80-8b97-957ad5f8ea47}
Flock Browser		{a463f10c-3994-11da-9945-000d60ca027b}
3.3.2 可选的标记
3.3.2.1 em:creator

此标记的内容用来指明扩展作者的名字。如果你没有提供下面的 em:aboutURL 标记,它将被显示在 Firefox 预定义的 about 窗口中。

3.3.2.2 em:description

此标记的内容用来描述扩展的功能或特性,它会被显示在扩展管理器中。但是,建议你在书写此内容时尽量简明扼要。

3.3.2.3 em:contributor

此标记的内容用来指明扩展贡献者的名字。如果你在扩展开发过程中,还得到了其他的人帮助,或者用到了其他人的源代码,请在此处写明贡献者的信息。并且,此标记允许存在多个,而 em:creator 标记只允许存在一个。如果你没有提供下面的 em:aboutURL 标记,它将被显示在 Firefox 预定义的 about 窗口中。

3.3.2.4 em:homepageURL

此标记的内容用来指明扩展或作者的主页地址。

3.3.2.5 em:updateURL

基于 Mozilla 扩展的最大好处就是可以自动升级,此标记的内容正是用来指明版本检测时的 URL 地址。Mozilla 给扩展提供这样一种升级机制,它通过强制方式或定期检测方式请求升级描述文件,从而获取到有关扩展自身的升级信息,进而对扩展进行升级操作。

为了配合扩展的升级检测操作,Mozilla 还提供了一些环境变量。虽然一般情况下,我们书写的升级检测地址都是固定的,但 Mozilla 提供了一些特殊的环境变量。通过这些环境变量,Mozilla 可以对不同的扩展生成不同的升级检测地址。下面只是一个示例:

<em:updateURL>http://www.foo.com/update.cgi?id=%ITEM_ID%&amp;version=%ITEM_VERSION%</em:updateURL>

有关这些环境变量的详细用法,请到 Mozilla 网址查阅。

3.3.2.6 em:optionsURL

此标记的内容用来指明当前扩展的选项设置入口,并且要以 chrome:// 地址方式进行指定。由于扩展的一些选项值要由用户来设置,所以扩展的作者有必要为扩展提供一个设置窗口。在扩展管理器中,通过“右键/设置”菜单项,我们可以对不同的扩展打开不同的设置窗口,这就是此标记完成的功能。但前提是,扩展作者必须要为所开发的扩展编写设置窗口,否则此标记不是必须的。

3.3.2.7 em:aboutURL

此标记的内容用来指明自定义“about(关于)”窗口的地址。同 em:optionsURL 标记一样,你必须编写并以 chrome:// 地址方式来提供此信息。如果你没有自定义 about 窗口,Firefox 会调用预定义的 about 窗口,并把以上的 em:name,em:version,em:creator,em:description,em:contributor 等信息显示在里面。

3.3.2.8 em:iconURL

此标记的内容用来指明扩展在扩展管理器中显示的图标,它同样要以 chrome:// 地址方式提供。虽然是图标,但它却经常用 PNG 格式的文件来保存。如果你没有指定自己的图标,Firefox 将采用预定义的图案来表示你的扩展。

3.3.2.9 em:targetPlatform

从 Firefox 1.5 版本以后,此标记被引入,它主要用来限制所运行的操作系统(OS)类型。由于某些扩展有二进制兼容的限制,所以就要限制所安装到的 OS 平台。关于此标记的详细用法,请查阅 Mozilla 网站的资料。

3.3.3 已经废弃的标记
3.3.3.1 em:file

从 Firefox 1.5 版本以后,此标记就被废弃了,这是由于引入了 chrome.manifest 文件。有关此标记的解释,作者将其放到了“扩展安装的实现原理”一节。

3.4 有关 Chrome

在 Mozilla 下做开发,你听到最多的词可能就是 Chrome,这个词在 Mozilla 下用的实在很滥。因为有许多地方都使用了这个词,并且每个地方的用法及其解释都不相同,我们要根据所使用的环境来确定 Chrome 这个词所代表的含义。那么,通常的情况下,Chrome 是什么意思呢?

3.4.1 Chrome 的定义

Chrome 是一组应用程序窗口的用户界面元素集合,它们位于窗口的内容区域之外。工具栏,菜单栏,进度条和窗口标题栏都是这些元素的例子,它们都是典型的 chrome 的一部分。

3.4.2 Chrome 提供者

chrome 提供者是一种为特定的窗口类型(如浏览器窗口)提供 chrome 的资源供给机制。从工具栏按钮上的图片到负责描述显示在窗口上的文本,内容和窗口本身的文件都是由 chrome 提供者提供的,这些提供者在一起工作,为特定的窗口提供了一组完整的 chrome。

chrome 提供者共有 3 种类型,分别是 Content,Locale 和 Skin,也就是以前提到的 3 种包资源。当安装包的 content,locale 和 skin 目录真正注册到 chrome 系统以后,它们下面的文件就成了 chrome 提供者。这些提供者就可以完成以上提到的那些功能,但前提是,必须通过一种机制来访问这些提供者,这就是 Chrome URL 要完成的功能。

3.4.3 Chrome URL

像已有的其它 URL 协议一样,我们可以通过 chrome:// 这种形式的 URL 协议来访问那些 chrome 提供者(换句话说,就是已经注册在 chrome 系统下的资源)。其实,那些资源是以物理文件或目录方式被存储,通过地址映射机制以 chrome 地址来访问的。但是我们根本不用管其物理地址是什么,Mozilla 下的扩展及其自身都是以这种映射过的地址来访问的。这样做的最大好处就是屏蔽了文件系统的多样性,为 Mozilla 的跨平台运行打下了基础。同 http 和 ftp 地址格式不一样,Mozilla 对 chrome 地址格式做了严格的规定,如下:

chrome://<package name>/<part>/<file.name>

<package name> 用来指明访问的扩展名称,这个名称在 content 类型的 contents.rdf 文件中被定义,或是被 chrome.manifest 文件所定义。比如,下面的 contents.rdf 文件内容

<RDF:Seq about="urn:mozilla:package:root">
  <RDF:li resource="urn:mozilla:package:sampleext"/>
</RDF:Seq>

它的 <package name> 是 sampleext。你同样会在 chrome.manifest 文件中看到类似的 <package name> 命名。

<part> 用来指明访问的 chrome 包类型,它分别允许 content,locale 和 skin 这 3 种固定的类型。

<file.name> 用来指明访问的文件名称,因为前面的 <package name> 和 <part> 已经对文件的路径做了限定,所以,这个文件相对来说是某个明确的文件。

当我们通过这种方式访问 content 类型的文件时,那些文件将被固定的访问;而当我们访问 locale 或 skin 类型的文件时,Mozilla 会根据自身的语言和皮肤,选择一个最合适的文件来动态访问,这一问题在前面已经提过了。但前提是,你必须提供足够多的语言包和皮肤包供其选择。

如果 <part> 下面还有子目录,你可以像访问文件一样,对子目录下的文件进行相对访问,就像下面这样:

chrome://browser/content/bookmarks/bookmarksManager.xul

而这些 <part> 下的子目录不用再定义 contents.rdf 文件,因为 <part> 所对应的目录已经定义了 contents.rdf 文件,重复定义是没有意义的。另外,chrome 地址还有一种缩略格式,它只被应用在一些特殊的情况下。如下:

chrome://navigator/content/

这看上去是在访问某个目录, 实际上它是

chrome://navigator/content/navigator.xul

的缩写。Mozilla 会根据你访问的 chrome 资源类型来自动补齐后面的文件名称,规则是

<file.name> = <package name> + (.xul|.dtd|.css)

这种形式的 chrome 地址多用于访问比较规则的包资源。下面再列出一些 Firefox 固有的 chrome 文件,你可以尝试用地址栏访问一下这些 chrome 地址:

主窗口
chrome://browser/content/browser.xul

选项窗口
chrome://browser/content/pref/pref.xul

私有选项窗口
chrome://browser/content/pref/pref-privacy.xul

书签管理器
chrome://browser/content/bookmarks/bookmarksManager.xul

书签面板
chrome://browser/content/bookmarks/bookmarksPanel.xul

历史记录面板
chrome://browser/content/history/history-panel.xul

Javascript 控制台
chrome://global/content/console.xul

管理员密码
chrome://pippki/content/pref-masterpass.xul

下载管理器
chrome://mozapps/content/downloads/downloads.xul

3.5 扩展安装的实现原理

本章的以上内容已经对扩展的结构做了详细的解释,但作者觉着还是有必要解释一下 Mozilla 在安装扩展时都做了些什么,这对你了解扩展是如何被注册和工作的都十分有利。在你了解了扩展的安装原理之后,手动注册扩展也不再是什么难以理解的事情了。

3.5.1 Gecko 1.8 以前版本

Gecko 1.8 以前内核的 Mozilla 在安装扩展时,它会去分析扩展安装包顶级结构下的 install.rdf 文件,将 em:id 标记的内容做为目标目录,解压缩到 %profile% 目录下。将扩展的 id,作者,升级地址,设置入口等信息写入到 %profile%/extensions/Extensions.rdf 文件中,因为此文件主要用来保存扩展管理器(Extension Manager)中的显示信息。

同时,它还会根据 install.rdf 中的 em:file 标记来确定有哪些包要注册到 chrome 系统中。如:

<em:file>
    <Description about="urn:mozilla:extension:file:sampleext.jar">
        <em:package>content/</em:package>
        <em:skin>skin/classic/</em:skin>
        <em:locale>locale/en-US/</em:locale>
        <em:locale>locale/zh-CN/</em:locale>
    </Description>
</em:file>

然后,Mozilla 会把如 sampleext.jar 这样的二级 JAR 文件做为分析的目标,分别读取其内部目录下的 contents.rdf 文件(如上为 sampleext.jar 下的 /content/contents.rdf,/skin/classic/contents.rdf,/locale/en-US/contents.rdf,/locale/zh-CN/contents.rdf)。首先,它会将 contents.rdf 中的那些唯一标识包资源的信息与包的物理地址映射起来,写到 %profile%/chrome/chrome.rdf 文件中,此文件为通过 chrome 地址访问包资源提供了地址映射机制。然后,它会将 content 类型包的 contents.rdf 文件所描述的“覆盖”规则写到 %profile%/overlayinfo/ 目录下的对应文件中(如:browser/content/overlays.rdf),这些文件为扩展程序“覆盖”到 Mozilla 系统提供了一种机制,并为扩展程序的运行提供了“入口点”。

3.5.2 Gecko 1.8 及后续版本

Gecko 1.8 及后续内核的 Mozilla 废弃了那种将所有的 chrome 映射信息都由 %profile%/chrome/chrome.rdf 维护,所有的覆盖信息都由如 %profile%/overlayinfo/browser/content/overlays.rdf 这样的文件进行维护的机制,转而将这两种信息都由扩展自己来维护。因而在扩展内部,不再需要提供 contents.rdf 文件,而只需提供一个即描述了“覆盖”信息,又描述了“地址”映射信息的 chrome.manifest 文件即可。

同时,负责保存扩展 id,作者,升级地址,设置入口等信息的 %profile%/extensions/Extensions.rdf 文件,也被换成了 %profile%/extensions.rdf 文件,且格式有所变化。

我们可以看到,新版本的 Mozilla 在安装扩展时,它不用再将那些 chrome 映射信息,覆盖信息分析索引了,而是由扩展开发者来给它预先生成。看上去是我们多做了一些工作,而实际上是方便了我们对扩展注册的控制。

3.6 chrome.manifest 文件

如上所述,在新版本的 Mozilla 中,使用 contents.rdf 注册包的机制被废弃了,但新版本还将兼容这种“古老”的注册方式。采用纯文本格式的 chrome.manifest 使 chrome 资源的注册更加方便,并且更利用扩展的开发。

下面先让我们看一个 chrome.manifest 的示例,如下:

content       necko                   jar:comm.jar!/content/necko/ xpcnativewrappers=yes
locale        necko       en-US       jar:en-US.jar!/locale/en-US/necko/
content       xbl-marquee             jar:comm.jar!/content/xbl-marquee/
content       pipnss                  jar:pipnss.jar!/content/pipnss/
locale        pipnss      en-US       jar:en-US.jar!/locale/en-US/pipnss/
# Firefox-only
overlay chrome://browser/content/pageInfo.xul           chrome://pippki/content/PageInfoOverlay.xul application={ec8030f7-c20a-464f-9b0e-13a3a9e97384}
overlay chrome://communicator/content/pref/preftree.xul chrome://pippki/content/PrefOverlay.xul
overlay chrome://navigator/content/pageInfo.xul         chrome://pippki/content/PageInfoOverlay.xul application=seamonkey@applications.mozilla.org
content       pippki                  jar:pippki.jar!/content/pippki/ xpcnativewrappers=yes
locale        pippki      en-US       jar:en-US.jar!/locale/en-US/pippki/
content       global-platform         jar:toolkit.jar!/content/global-platform/ platform
skin          global      classic/1.0 jar:classic.jar!/skin/classic/global/
override chrome://global/content/netError.xhtml jar:embedder.jar!/global/content/netError.xhtml
content       inspector               jar:inspector.jar!/content/inspector/ xpcnativewrappers=no

仔细查看上面的内容,我们不难发现 contents.rdf 的影子。由于 chrome.manifest 是对 contents.rdf 的改进,确切的说是结合了 install.rdf 文件的产物,所以会有许多已有的特性。同时,Mozilla 还为 chrome.manifest 加入了许多原来没有的新规则。所有的规则是由每个文本行头部的“指令”来标识的,这些指令的解释如下。

3.6.1 注册指令
3.6.1.1 content

它用来注册一个 content 类型的包,如下:

content packagename uri/to/files/ [flags]

packagename 是所注册的包名称,同 chrome 地址中的 <package name> 和原有 contents.rdf 中的包名称含义一样。

uri/to/files/ 用来指明这个包资源的操作系统路径,它的格式比较多。对于 %profile% 下的扩展,我们知道在安装之后,Mozilla 会将顶级结构解压缩生成二级 JAR 文件。对于这种相对路径下的 JAR 文件,我们要采用如:

jar:chrome/cview.jar!/content/cview/

这种格式的 uri 来指明。“jar:”用来指明此 uri 指向的是某个 JAR 文件的内部地址。还有一个特殊的“resource:/”用来代表 Mozilla 应用程序的根目录,例如:

jar:resource:/chrome/pipnss.jar!/content/pipnss/

用来指向位于 %app%/chrome/ 目录下 pipnss.jar 的内部压缩地址。而对于非压缩的目录形式的包,则要通过 file:// 地址来指明,如:

file:///D:/edit/projects/sample_extension/chrome/sample_extension/content/sample_extension/

这是一个位于 Windows 操作系统下的 file:// 地址,生成这种地址很简单,你可以通过浏览器地址栏生成符合要求的 file:// 地址。你或者还可以组成上面的 jar: 前缀来定位磁盘上的某个 JAR 包。

jar:file:///D:/edit/projects/sample_extension.jar!/content/sample_extension/
3.6.1.2 locale

它用来注册一个 locale 类型的包,格式如下:

locale packagename localename uri/to/files/ [flags]

localename 用来指明所注册的语言类型,如: en-US,zh-CN 等,其它的同上。

3.6.1.3 skin

它用来注册一个 skin 类型的包,格式如下:

skin packagename skinname uri/to/files/ [flags]

skinname 用来指明所注册的皮肤包类型,如: classic/1.0,其它的同上。

3.6.1.4 overlay

它用来指明 XUL 的“覆盖”规则,这种格式的规则比 contents.rdf 的更容易让人读懂。

overlay chrome://URI-to-overlay chrome://overlay-URI [flags]
3.6.1.5 style

它用来指明 CSS 的“覆盖”规则,这是新加入的特性。通过这个新规则你可以为已有的界面文件引入新的样式表效果。

style chrome://URI-to-style chrome://stylesheet-URI
3.6.1.6 override

在某些时候,你可能会有完全“重载”或者说是替换某个 chrome 文件的要求,这条指令就可以完成这种要求。

override chrome://package/type/original-uri.whatever new-resolved-URI
3.6.2 注册指令中的 Flags 标识

在上面的注册指令中,你会发现有个 [flags] 标识经常出现行尾 ,这个标识多用来标识特定的 chrome 包属性或限定当前行命令是否可用。下面对那些可以做为 [flags] 标识的内容一一说明。

3.6.2.1 application

它是一个条件类型的标识,用于限定注册包所应用的 Mozilla 平台。由于某些扩展可以被安装在多个 Mozilla 平台下,通过这个条件标识就可以将某个包限定性的应用于某个特定的 Mozilla 平台下。

application=app-ID

本章的“install.rdf 文件”一节已经列举了一些常见的 Mozilla 平台,参考上面列出那些 id,书写此标识即可。如果你有多个平台要限定,你可以将多个条件写在行尾。

3.6.2.2 appversion

它是一个条件类型的标识,用来限定注册包所应用 Mozilla 平台的版本范围,如下:

appversion=version
appversion<version
appversion<=version
appversion>version
appversion>=version

version 是 Mozilla 平台的版本,你可以通过 Mozilla 的 about 窗口,查看它的确切版本号。如果某个注册包的范围限定需要多个条件,你同样可以像 application 一样,将多个条件写在同一行。

3.6.2.3 platform

它是一个关键字标识,用来限定包资源所应用的操作系统类型。由于某些包的使用需要基于特定的操作系统,所以要做这方面的限定。但此标识只能应用在 content 类型的包上,locale 和 skin 类型的包将忽略这个标识。如果对某个 content 包应用了这种标识,你还需要在那个 content 包目录下再建 3 个子目录,将它们分别命名为 win,mac 和 unix,以对应 3 种不同的操作系统 Windows/OS2,OS9/OSX 和 Unix-Like 。在这 3 个目录下,你肯定还要建立一些适用于指定操作系统的文件,如: .dll 或 .sh 文件,所有在这 3 个子目录之外的文件都将被 Mozilla 忽略。如下便是一个应了 platform 的示例:

content global-platform jar:toolkit.jar!/toolkit/content/global-platform/ platform
3.6.2.4 xpcnativewrappers

它是一个条件类型的标识,对于那些要访问所浏览网页内容的 chrome 代码来说,XPCNativeWrapper 是一种代码保护机制。在有些恶意网页中,网页的编写者可以通过重载网页中的某个 DOM 对象的属性或方法来达到代码注入的效果。当有些 chrome 代码要访问当前网页的某个 DOM 元素时,它肯定要获取此元素的属性或调用其下的某个方法,而如果那个 DOM 元素的属性或方法被使用 JavaScript 重写成了某段恶意代码,chrome 代码的执行肯定脱离了本意,并且变成了恶意代码的执行者,这样恶意网页就达到 chrome 注入攻击的效果。

Mozilla 通过一个叫 XPCNativeWrapper 的机制来取那些真实的属性和方法,来避免 chrome 代码的恶意注入。在以往的程序里,你需要通过手动调用 XPCNativeWrapper 对象来取 DOM 元素的属性,这比较麻烦。在 Firefox 1.5 以后,这个保护特性被默认打开,你不再需要使用 XPCNativeWrapper 对象来读取 DOM 属性了。但是,如果你想关闭这个特性,你必须通过指明 xpcnativewrappers=no 条件来实现,我想很少会有人这么做的。

此条件标识只被应用在 content 类型的包上,locale 和 skin 类型的包将忽略此标识。

3.7 Chrome 新机制的应用: Contents.rdf + Install.rdf = Chrome.manifest

下面对以上章节中出现的 contents.rdfinstall.rdf 结合起来做一个 chrome.manifest 转换,看看这时的 chrome.manifest 是什么格式。

overlay	chrome://browser/content/browser.xul	chrome://sampleext/content/overlay.xul
content	sampleext	jar:chrome/sampleext.jar!/content/
skin	sampleext	classic/1.0	jar:chrome/sampleext.jar!/skin/classic/
locale	sampleext	en-US	jar:chrome/sampleext.jar!/locale/en-US/
locale	sampleext	zh-CN	jar:chrome/sampleext.jar!/locale/zh-CN/

第四章 配置项目环境

通过上一章我们已经对扩展的结构和关键的 Chrome 有所了解,在此基础之上,本章将讲解扩展项目的配置方法。许多的扩展开发者可能都有过这样的经历:XUL 元素掌握得差不多了,可是想做一个小项目的时候却不知如何入手。作者希望通过本章节,你能了解到如何合理地配置扩展项目的开发环境。

Mozilla 平台下的项目配置和其它平台下的项目配置有些不同,其下的项目配置需要一个“螺旋式”渐进的过程。首先,我们需要先建立一个框架式扩展;然后,将其以安装方式注册到 Mozilla 下,同时对此扩展涉及到的 chrome 映射做修改;最后,我们才能真正地对项目进行开发。如果你在开发过程又中增加或修改了有关 chrome 注册的信息,你还要重复以上的过程,但那时的扩展已经不能再称为框架式扩展了,这种配置方法非常像软件开发中的“螺旋式开发模型”。由于,Firefox 的新版本与老版本在 chrome 注册方式上不同,所以相应的项目配置方法也会不相同。

4.1 建立框架式扩展

每一个做 Mozilla 扩展开发的人,他的第一个扩展几乎都是根据已有的“模板式”扩展修改而来的。而对于刚开发扩展的人来说,不依赖已有的扩展,从头实现一个自己的扩展几乎是不可能的。那么,就让我们从经典的“Hello World”扩展开始吧。

4.1.1 “Hello World” 的结构

对于一个扩展来说,如果你想让它能分别运行于高版本和低版本的 Mozilla 上,确实是一件让人很头痛的事情。我们必须将那些兼容性的文件都考虑到,特别是有关 chrome 注册的 contents.rdf 文件。下面给出它的目录结构:

helloworld/
  chrome.manifest
  install.rdf
  chrome/
    helloworld/
      content/
        contents.rdf
        overlay.js
        overlay.xul

你必须在磁盘上建立一个对应的目录结构,因为这就是你项目开发的基础,你在项目开发时的所有源代码的都将放在这里,后面将称其之为“项目目录”。

4.1.2 负责注册 Chrome 的文件

chrome.manifest 的内容如下:

overlay	chrome://browser/content/browser.xul	chrome://helloworld/content/overlay.xul
content	helloworld	jar:chrome/helloworld.jar!/content/

install.rdf 的内容如下:

<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 
     xmlns:em="http://www.mozilla.org/2004/em-rdf#">

  <Description about="urn:mozilla:install-manifest">
    <em:id>{241b5bc7-a8aa-44a6-a18d-3054dc6047cf}</em:id>
    <em:name>Hello World</em:name>
    <em:version>1.0</em:version>
    <em:type>2</em:type>
    <em:creator>Lewis Lv</em:creator>
    <em:description>The classical demo with "Hello, world!"</em:description>
    <em:homepageURL>http://kb.mozillazine.org/Getting_started_with_extension_development</em:homepageURL>

    <!-- Firefox -->
    <em:targetApplication>
      <Description>
        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
        <em:minVersion>1.0</em:minVersion>
        <em:maxVersion>1.5</em:maxVersion>
      </Description>
    </em:targetApplication>

    <!-- This is not needed for Firefox 1.1 and later. Only include this 
      if you want to make your extension compatible with older versions -->
    <em:file>
      <Description about="urn:mozilla:extension:file:helloworld.jar">
        <em:package>content/</em:package>
      </Description>
    </em:file>
  </Description>
</RDF>

contents.rdf 的内容如下:

<?xml version="1.0"?>
<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
         xmlns:chrome="http://www.mozilla.org/rdf/chrome#">

  <RDF:Seq about="urn:mozilla:package:root">
    <RDF:li resource="urn:mozilla:package:helloworld"/>
  </RDF:Seq>

  <!-- package information -->
  <RDF:Description RDF:about="urn:mozilla:package:helloworld"
        chrome:name="helloworld"
        chrome:extension="true"
        chrome:displayName="Hello World"
        chrome:author="Lewis Lv"
        chrome:authorURL="http://kb.mozillazine.org/Getting_started_with_extension_development"
        chrome:description="The Classical Demo With Hello World">
  </RDF:Description>

  <!-- overlay information -->
  <RDF:Seq about="urn:mozilla:overlays">
    <RDF:li resource="chrome://browser/content/browser.xul"/>
  </RDF:Seq>

  <RDF:Seq about="chrome://browser/content/browser.xul">
    <RDF:li>chrome://helloworld/content/overlay.xul</RDF:li>
  </RDF:Seq>
</RDF:RDF>

以上这 3 个文件是负责 chrome 注册的,所以提前给出。如果你在语法上还有什么不清楚的,请你参考上一章的内容。下面将结合着 browser.xul,着重介绍一下 overlay.xul 文件的内容。

4.1.3 overlay.xul

在上一章中,已经介绍过有关覆盖的内容,但是介绍得过于肤浅且没有示例。下面的一段内容摘自《Rapid Application Development with Mozilla》,原文如下:

"The overlay system is quite simple. One XUL document is the master document. This document provides a starting point for the final content. Any other XUL documents are overlays. Overlay content is merged into, or added to, the master document's content. This happens in memory when those documents are loaded and has no effect on the original files."

作者对上面的内容进行了意译,参考如下:

“覆盖系统十分简单,一个文档是 XUL 主文档,它提供了整个内容的入口点;另一个是文档是 XUL 覆盖文档,它描述了覆盖到主文档中的内容。当这些文档被装载时,覆盖内容会被合并或者加入到主文档的内容中。但以上的这一过程只发生在内存中,所以不会影响到原始的物理文件。”

从上面这段内容中,不难反应出一个问题。Mozilla 之所以扩展性很好,是因为它在设计之初就考虑到了如何应对扩展的问题。那些扩展程序在物理上,分别对自己的“覆盖”信息进行管理,互不影响;可一旦运行起来,却都被集成到 Mozilla 上,像一个程序一样协作运行。很显然,这就是 Mozilla 把自己做为一个平台,以此来对外提供接口的途径。下面,让我们看一下“Hello World”的 overlay.xul 实现。

<?xml version="1.0"?>
<overlay id="helloworld-overlay"
         xmlns=http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul>
  <script type="application/x-javascript" src="overlay.js"/>

  <menupopup id="menu_ToolsPopup">
    <menuitem id="helloworld-hello" label="Hello, world!" oncommand="helloWorld();"/>
  </menupopup>
</overlay>

这个文件所实现的功能,就是在 Firefox 的“工具(tools)”下拉菜单中再添加一个“Hello,world!”菜单项。通过点击那个菜单项,执行 helloWorld 函数。效果如下:

menu.jpg

我们可以看到,这个 XML 格式的文件就是 XUL 文件,这是由于它使用了 XUL 标记并引入了 XUL 命名空间。并且,这个专门负责描述“覆盖”的 XUL 文件与其它的 XUL 文件还有所不同,因为它使了一个特殊的 overlay 根标记。可以这么说,这个 XUL 文件就是整个“Hello World”扩展的入口。

此文件在 overlay 根标记中引入了 XUL 的命名空间,它所实现的功能就是: 在默认情况下,overlay 下面的所有子标记会自动继承它的命名空间。

xmlns=http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul

只有这样,此文档的所有标记才会被识别为 XUL 标记。在它的下面,还引用了 overlay.js 脚本文件,引用的方式与在 HTML 中没有什么太大的区别,helloWorld 函数于此文件内部被定义。

<menupopup id="menu_ToolsPopup">
  <menuitem id="helloworld-hello" label="Hello, world!" oncommand="helloWorld();"/>
</menupopup>

这段内容即为对“覆盖”的描述。因为在 chrome://browser/content/browser.xul 中描述了 Firefox 的界面信息,所以当前扩展的覆盖信息就应该参照这个文件来完成,它就是上文中提到的“XUL 主文档”。又由于在 browser.xul 中,“工具(tools)”下拉菜单的 id 被定义成了“menu_ToolsPopup”,所以 overlay.xul 就参照 browser.xul 中的定义,在自己内部定义了

<menupopup id="menu_ToolsPopup">

它的目的是要告诉 Firefox,让 Firefox 比照当前标记,将其下的所有子标记“合并”到对应的“menu_ToolsPopup”标记中。当然,这个“合并”操作不是真的在物理文件上做合并,而是在 Firefox 启动时,在内存中进行“合并”操作。

XUL 覆盖文档”的内容一般都很简单。因为在一般情况下,不需要将太多的界面元素“覆盖”到 Mozilla 原有的界面上。它只是充当一个入口的角色,有的“覆盖文件”甚至连一个描述“覆盖”的标记都没有,扩展中大量的功能都是在如 overlay.js 这样的脚本文件中完成的。因为 JavaScript 可以对 XUL DOM 进行操作,所以它同样可以通过脚本方式动态添加或修改界面元素。

4.1.4 overlay.js

这个文件负责定义 helloWorld 函数。helloWorld 函数实现打开“Hello,world!”窗口的功能,代码如下:

// This is the main function
function helloWorld()
{
    alert("Hello, world!");
}

以上的代码很简单,它只是通过调用 window 对象的 alert 方法来打开一个预定义窗口,内容就是“Hello,world!”。效果如下:

alert.jpg

但在实际的项目中,这种实现肯定是无法满足要求的。你可能要编写许多的函数或对象,甚至编写多个 JS 文件,使它们在一起协调工作,这些文件同样像 overlay.js 一样被 overlay.xul 引用。至于如何编写那些 JS 文件,就是你最大的课题了,但至少现在你知道如何着手做东西了。

4.2 安装并修改 Chrome 映射

我们已经以目录形式建立了扩展,但是如何让它在 Firefox 中运行呢?你可能会想到,将它照扩展的 XPI 格式打包,再安装到 Firefox 上不就可以了吗?这种想法肯定是没错的,但是你打算一直在“打包 -> 安装 -> 修改错误或添加功能 -> 打包 -> 卸载原有/安装 -> 修改错误或添加功能 -> ...” 的过程中循环进行吗?我可不打算这么干,开发效率太低了,我想你的鼠标和键盘也不会“答应”的。正确的方法是,你必须以目录形式注册扩展,以此来进行开发。即使是这样,你都免不了要在开发时重复的关闭和开启 Firefox 进程。

由于,Firefox 1.0 与 Firefox 1.5 版本,在扩展注册方式上有许多的不同。同样是手动注册扩展,后者非常简单,而前者则有些复杂,本章开始时提到的“螺旋式”配置,也只是针对后者来说的。为了能在 Gecko 1.8 以前版本(对应 Firefox 1.0)的 Mozilla 中配置项目,作者还是希望你能掌握这些技巧,以应对扩展的快速开发。需要注意的是,你每次修改完 chrome 注册信息,都需要重新启动 Firefox,因为 Firefox 只在启动时才会加载那些注册信息。

4.2.1 Firefox 1.0 及以前版本

为什么非要从安装扩展开始呢?手动注册扩展有什么困难吗?答案是,如果使用“公认”的方法,在 Firefox 1.0 下手动注册扩展其实很简单。但是它将影响并破坏每个 Profile 的独立性,并且有可能破坏 Firefox 程序本身。这种公认的方法就是修改 %app%/chrome/installed-chrome.txt 文件,在这个文件里,你只需要简单地添加几行扩展的注册信息即可。但在这里,作者不打算讲解这种方式的实现,只是怕你破坏掉 Profile 的独立性及 Firefox 本身。这种方法的缺点如下:

  • 注册的所有扩展信息会被写入到 %app% 目录下的文件中,所以在所有的 Profile 下都是“可见的”,即可以通过 chrome 地址进行访问。这不利于配置独立的 Profile 来分别进行开发和测试;
  • 另一个负面影响就是,“安上去容易,卸下去难”。如果你想卸载通过这种方法注册的扩展,你必须手动删除许多的注册信息,稍不留意,就会破坏 Firefox 本身;
  • 所注册的扩展在 Firefox 扩展管理器中不可见。上一章在讲“扩展安装的实现原理”时提到过,位于扩展管理器中的信息是被写入 %profile%/extensions/Extensions.rdf 文件中的。但是通过这种方式手动注册的扩展,其信息只被写入 %app% 下的相关文件中,所以在扩展管理器中是不可能见到的,并且你还无法“真实地”对设置窗口和关于窗口进行测试(原因是,虽然你可以通过 chrome 地址来访问设置窗口和关于窗口,但它显然不是被放到“真实的”环境进行测试的);

通过独立的 Profile 来注册扩展,显然可以将其影响范围降到最小,并且开发中的扩展与发布安装时的扩展,其运行状态没有区别。但对独立的 Profile 做手动的扩展注册简单太麻烦了,上一章讲扩展的安装原理时已经提到过 Mozilla 所做的工作。那些扩展的注册信息“东拆西拆”的,被放到许多的文件中,并且注册语法很复杂。相比之下,“Firefox 本身却认为这些注册扩展的操作没有什么”。我们正好利用扩展能自动安装的特性,让 Firefox 自己来安装这些扩展,然后,我们再对那些“至关重要”的 chrome 地址映射做修改,即可完成项目配置的要求。

4.2.1.1 打包扩展

第一步,我们先要将扩展从目录形式转换为 XPI 安装包形式,所使用的打包工具就是 ZIP 压缩软件。常用的 ZIP 压缩软件有 WinRAR,WinZIP;Unix-Like 操作系统下必须使用 zip 工具,gzip 的格式是不符合要求的。如果工具没问题了,我们还必须将扩展按下面的格式进行打包:

helloworld.xpi/
  chrome.manifest
  install.rdf
  chrome/
    helloworld.jar/
      content/
        contents.rdf
        overlay.js
        overlay.xul

helloworld.xpihelloworld.jar 使用的都是 ZIP 格式,只不过是扩展名不同而矣。如果你打包时的结构出现了问题,扩展是不能被 Firefox 正常安装的,所以你要特别小心。

4.2.1.2 扩展的安装

第二步,将扩展安装到 Firefox 上。提醒你一下,你别打算在扩展管理器中找到任何安装扩展的按钮。安装扩展很简单,你可以通过“文件(File)/ 打开文件(Open File)”来指定被安装的扩展,也可以将扩展拖拽到 Firefox 内容区域或扩展管理器窗口中,Firefox 会自动弹出安装提示窗口。

4.2.1.3 修改 Chrome 地址映射

我们知道安装后的扩展,其 chrome 地址映射信息是被写入到 %profile%/chrome/chrome.rdf 文件中的。因此,我们将那些映射的地址改成项目目录地址即可。如下是作者 chrome.rdf 中的扩展映射信息片段:

<RDF:Description RDF:about="urn:mozilla:package:helloworld"
                 c:baseURL="jar:file:///C:/Documents%20and%20Settings/lewislv/Application%20Data/Mozilla/Firefox/Profiles/gv8xfmu1.dev/extensions/%7B241b5bc7-a8aa-44a6-a18d-3054dc6047cf%7D/chrome/helloworld.jar!/content/"
                 c:locType="profile"
                 c:name="helloworld"
                 c:extension="true"
                 c:displayName="Hello World"
                 c:author="Lewis Lv"
                 c:authorURL="http://kb.mozillazine.org/Getting_started_with_extension_development"
                 c:description="The Classical Demo With Hello World" />

上面出现的 c:baseURL 属性,它所指向的是 content 类型包的物理地址。当然,这个地址是位于 %profile%/extensions/{241b5bc7-a8aa-44a6-a18d-3054dc6047cf}/helloworld.jar 中的压缩地址。由于我们要以目录形式开发扩展,所以要将它替换成项目目录中的 content 包所在地址。比如:

<RDF:Description RDF:about="urn:mozilla:package:helloworld"
                 c:baseURL="file:///D:/edit/projects/helloworld/chrome/helloworld/content/"
                 c:locType="profile"
                 c:name="helloworld"
                 c:extension="true"
                 c:displayName="Hello World"
                 c:author="Lewis Lv"
                 c:authorURL="http://kb.mozillazine.org/Getting_started_with_extension_development"
                 c:description="The Classical Demo With Hello World" />

照这样替换以后,Firefox 再进行地址映射时,它会到这个目录下去映射那些 chrome 文件,而不会再去 helloworld.jar 中映射对应的文件。如果你的扩展还注册了其它类型的包资源,你都需要一一替换。现在,你即使将 helloworld.jar 删除了,也不会有任何的问题了。

4.2.2 Firefox 1.5 及后续版本

由于作者在写这篇文档时,主要的开发工具“JavaScript Debugger”还没有 Firefox 1.5 的兼容版本,所以作者主要工作在 Firefox 1.0 下。实际上,作者在同一系统下安装了两个版本的 Firefox,它们共享同一套 Profile。如果你也像作者一样,我希望你联系着上面的 Firefox 1.0 配置来进行 Firefox 1.5 的配置,因为这时你仅需对安装后的 chrome.manifest 文件做修改,即可完成配置项目环境的要求。

原有的 chrome.manifest 中

overlay	chrome://browser/content/browser.xul	chrome://helloworld/content/overlay.xul
content	helloworld	jar:chrome/helloworld.jar!/content/

我们需要将其照下面的修改

overlay	chrome://browser/content/browser.xul	chrome://helloworld/content/overlay.xul
content	helloworld	file:///D:/edit/projects/helloworld/chrome/helloworld/content/

如果你发现 Firefox 好像有记忆性,它总是会先到 helloworld.jar 中进行映射。那么,你需要确认一下是否已经将 nglayout.debug.disable_xul_cache 选项置成 true 了。

如果你只使用 Firefox 1.5 做开发,你还可以使用一种非常简单的方法进行注册。你没必要像 Firefox 1.0 中那样,经历“打包 -> 安装 -> 修改 chrome”的过程,你只需建立一个“指向文件”,并对项目目录下的 chrome.manifest 做些修改即可。方法如下:

  1. 打开 %porfile%/extensions 目录;
  2. 建立一个与你所开发扩展 id 名称相同的纯文本文件,如: {241b5bc7-a8aa-44a6-a18d-3054dc6047cf}。需要注意的是,此文件没有任何的扩展名。你需要将项目目录的路径保存其中,如:“D:\edit\project\helloworld\”,这个路径的格式必须是操作系统路径,而不能是 file:// 地址格式;

除此之外,你还要更改项目目录下的 chrome.manifest 文件。因为,Firefox 在启动时会到项目目录中来查找描述了 chrome 映射信息的 chrome.manifest 文件。你只需将它的 chrome 映射像上面做修改即可,这时你给出的地址既可以是相对的,也可以是绝对的。

相对地址格式: content	helloworld	chrome/helloworld/content/
绝对地址格式: content	helloworld	file:///D:/edit/projects/helloworld/chrome/helloworld/content/

但是这样有一个不好的地方,项目目录中的 chrome.manifest 与发布时的 chrome.manifest 内容不再一样了。你必须在发布时,另外再建立一个适用于发布的 chrome.manifest 文件。为了避免这个问题,作者还有另外一种方法,它可能更适合处理开发项目的无关性。

  1. 同样,打开 %porfile%/extensions 目录;
  2. 建立一个与你所开发扩展 id 名称相同的目录,将你项目目录下的 install.rdf 和 chrome.manifest 文件复制到其中;
  3. 以绝对地址格式修改 chrome.manifest 中的 chrome 映射地址,将其指向开发项目的对应目录;

以这种方式注册的扩展,项目目录下的内容最大限度地与环境无关。因此,你不用再考虑建立一个适合于发布的 chrome.manifest 文件了。

通过以上的这些工作, 我们的项目环境终于被配置完成。可以这么说,现在真的可以着手开发项目了。

4.3 开始真正的开发

上面的“Hello World”实现得过于简单了,它只使了一个简单的 alert 窗口来显示“Hello,world!”文本。下面,我们打算用定义自己的窗口来显示那些信息,其中还会包含一些图片和 CSS。当然,我们要对已有的内容做修改。

4.3.1 加入 Skin 包资源

一个正常的扩展一般都会有 skin 资源,只有加入了 skin 资源,界面才会更美观。同时,也有利于 CSS 与 XUL 描述的分离。下面,我们要像打补丁一样,在原有文件的基础上做修改。

首先,我们要对 install.rdf 文件做修改。在此文件的 Description 标记下,加入“关于”和“图标”的 chrome 地址。

<em:description>The classical demo with "Hello, world!"</em:description>
<em:aboutURL>chrome://helloworld/content/helloworld.xul</em:aboutURL>
<em:iconURL>chrome://helloworld/skin/icon.png</em:iconURL>
<em:homepageURL>http://kb.mozillazine.org/Getting_started_with_extension_development</em:homepageURL>

通过 em:iconURL 标记,位于扩展管理器的扩展将具有一个用来“标识”自己的图标。同时,通过 em:aboutURL 标记,你可以在扩展管理器中打开自定义的“关于”窗口。效果如下:

extmange.jpg

当然,以上的增加的这些内容,你都要提前有所规划。我们的主要目的是加入 skin 类型的包资源,因此还要在 install.rdf 中加入下面的内容:

<Description about="urn:mozilla:extension:file:helloworld.jar">
  <em:package>content/</em:package>
  <em:skin>skin/classic/</em:skin>
</Description>

然后,为了应对新版的 Firefox,我们还在 chrome.manifest 加入 skin 包的注册信息。

skin	helloworld	classic/1.0	jar:chrome/helloworld.jar!/skin/classic/

最后,我们必须在项目目录下创建对应的 skin 包目录,并且加入必要的文件。当然,所加入的这些文件,与你的设计需要有关。在这里,作者打算加入两个图片文件,一个用来做图标(icon.png),一个用来做 Logo(helloworld.png),如下:

icon.png helloworld.png

还要加入一个 CSS 文件,用来保存分离的 Style 定义(helloworld.css);同时,还有必不可少的 contents.rdf 文件。下面是变化后的目录结构:

helloworld/
  chrome.manifest
  install.rdf
  chrome/
    helloworld/
      content/
        contents.rdf
        overlay.js
        overlay.xul
      skin/
        classic/
          contents.rdf
          helloworld.css
          icon.png
          helloworld.png

我们还要创建 skin 的 contents.rdf 文件,如下:

<?xml version="1.0"?>
<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
         xmlns:chrome="http://www.mozilla.org/rdf/chrome#">

  <RDF:Seq about="urn:mozilla:skin:root">
    <RDF:li resource="urn:mozilla:skin:classic/1.0" />
  </RDF:Seq>

  <RDF:Description about="urn:mozilla:skin:classic/1.0">
    <chrome:packages>
      <RDF:Seq about="urn:mozilla:skin:classic/1.0:packages">
        <RDF:li resource="urn:mozilla:skin:classic/1.0:helloworld" />
      </RDF:Seq>
    </chrome:packages>
  </RDF:Description>
</RDF:RDF>

在 Firefox 1.0 下,如果你仅有那些资源文件,而忽略了定义 contents.rdf 文件,那是资源文件也是不能被识别的。现在,所有关于 skin 的资源已经定义完毕了。需要说明的是,对于 helloworld.css 的内容,我们要结合 helloworld.xul 文件来进行定义。

4.3.2 新一轮的 Chrome 配置

本章的开始部分已经说过,“如果你在开发过程又中增加或修改了有关 chrome 注册的信息,你还要重复以上的过程”。那么,根据你当前的 Firefox 版本,你可能还要重复进行“打包 -> 安装 -> 修改 chrome”的过程,或者你只需简单的配置修改。

你应该明白,这些修改是必须的,除非你完全清楚低版本的 Extensions.rdf,chrome.rdf 或高版本的 extensions.rdf 的格式要求。

4.3.3 创建自定义的“Hello World”窗口

如果你的 XUL“理论”已经相当不错了,或者至少写过简单的 XUL 的话,我想你已经急不可耐了。因为,绕了这么一大圈子,我们终于可以写一个运行在 chrome 状态下的 XUL 文件了,并且它还可以调用 skin 类型的 chrome 资源。

4.3.3.1 helloworld.xul

它就是那个用来显示“Hello World”的窗口文件,与其配合完成功能的文件还有上面的 helloworld.css 和后面的 helloworld.js,helloworld.xul 文件的内容如下:

<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://helloworld/skin/" type="text/css"?>

<window id="helloWorld"
  xmlns=http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul
  title="Hello World Demo"
  orient="vertical"
  onload="onLoad();">

  <script type="application/x-javascript" src="helloworld.js" />

  <commandset>
    <command id="cmd_close" oncommand="window.close();"/>
  </commandset>

  <keyset>
    <key id="key_colse" keycode="VK_ESCAPE" command="cmd_close"/>
  </keyset>

  <groupbox align="center" orient="horizontal" id="infobox">
    <vbox>
      <hbox align="baseline" id="titlebox">
        <text value="Hello, world!" id="title"/>
        <spacer style="width:1em;"/>
        <text value="version 1.0" id="version"/>
      </hbox>
      <vbox align="end" id="creatorbox">
        <text value="Created By:" id="createby"/>
        <text class="url" id="creator" onclick="contactCreator();"/>
      </vbox>
      <vbox align="end" id="homebox">
        <text value="Home Page:" id="homepagetitle"/>
        <text class="url" id="homepage" onclick="openHomePage();"/>
      </vbox>
    </vbox>

    <spacer flex="1"/>
    <image src="chrome://helloworld/skin/helloworld.png" id="logo"
        onclick="openHomePage();"/>
  </groupbox>
  <separator/>
  <hbox align="end">
    <spacer flex="1"/>
    <button label="Close" command="cmd_close" id="btnClose"/>
  </hbox>
</window>

以上 XUL 标记描述了一个窗口及其中所显示的内容。同以前的 overlay.xul 文件不同,它是用 window 做为根标记的。XUL 和 HMTL 在格式上有许多相同之处,每个标记都有自己所负责的功能,并且有些标记还可以在 HTML 中找到功能对应的标记。我不打算去讲解那些 XUL 标记的用法和细节,因为许多内容在 XULPlanet 上都可以找到相应的参考。如果你对上面的哪个标记不清楚,你可以在这个网站查阅到详细的用法。

如果你对 XUL 还只是一知半解,以上的有些内容可能会让你不知所措。因为,上面确实用到了些比较“高级”的 XUL 技巧,目的只是为了让界面更加美观。如果你仅仅是学习 XUL,你不用建立任何的扩展项目,简单的 XUL 加一些 CSS 和 JavaScript 即可。

4.3.3.2 helloworld.css
text{
    padding:2px;
}

#helloWorld{
    background-color: #FFFFFF;
    margin: 1em;
}

#infobox{
    border: 2px dotted #FF1010;
}

#title{
    font-weight: bold;
    font-size: x-large;
}

#version{
    font-weight: bold;
    color: #909090;
}

#titlebox{
    margin: 20px;
    border-bottom: 5px solid #FF1010;
}

#createby, #homepagetitle{
    font-weight: bold;
}

#creatorbox{
    margin-top:10px;
    margin-right:20px;
    margin-bottom:10px;
}

#homebox{
    margin-right:20px;
    margin-bottom:10px;
}

#btnClose{
    font-weight: bold;
}

#logo{
    margin-right:20px;
    -moz-opacity: 0.5;
}

#logo:hover{
    -moz-opacity: 1;
    cursor: pointer;
}

.url:hover{
    color: #FF1010;
    cursor: pointer;
    text-decoration: underline;
}

你可看到 XUL 与 HMTL 一样,都需要 CSS 来负责那些“样式化”的东西,语法与 HTML 一样。为了让 helloworld.xul 能引用这些样式,必须在 helloworld.xul 中加入样式表引用的“处理指令”,如下:

<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://helloworld/skin/" type="text/css"?>

上面的样式表引用地址是上一章介绍过的 chrome 缩略格式。与 HTML 中的 CSS 不太一样,这里会要求在 hellworld.xul 中的许多标记中加入 id 属性,而不是 class 属性,目的是为了利用 id 来针对性地控制指定标记的外观。

4.3.3.3 helloworld.js

在 helloworld.xul 中,你可以看到有些标记被加入了 onclick 事件属性,与 HTML 标记中的事件属性一样,它是为了能将“界面”与“功能”结合起来。那些被封装在 JS 文件中的函数用来完成指定的功能,如: 打开主页或邮件客户端程序。helloworld.js 的内容如下:

// define constants
const HOMEPAGE = http://developer.mozilla.org;
const CREATOR = "Lewis Lv";
const CREATOR_EMAIL = "lewislv@gmail.com";

// This function use to initialize window's elements
function onLoad()
{
    var homepage = document.getElementById("homepage");
    homepage.setAttribute("value", HOMEPAGE);
    homepage.setAttribute("tooltiptext", HOMEPAGE);

    var creator = document.getElementById("creator");
    creator.setAttribute("value", CREATOR);
    creator.setAttribute("tooltiptext", CREATOR_EMAIL);
}

// This function use to open homepage
function openHomePage()
{
    window.opener != null ? window.opener.open(HOMEPAGE) : window.open(HOMEPAGE);
}

// This function use to open mail application
function contactCreator()
{
    var mailURL = "mailto:" + CREATOR_EMAIL;
    var _blank = window.opener != null ? window.opener.open(mailURL) : window.open(mailURL);
    _blank.close();
}

以上的内容只是简单地定义了 3 个函数,分为用来处理窗体装载时的事件,用户点击“主页”时的事件,用户点击“作者”时的事件。负责处理窗口装载的函数 onLoad,它利用 DOM 接口来操纵所显示的内容,将主页地址和作者名称填充到窗口界面上。而 openHomePage 和 contactCreator 则是利用新窗口来打开主页地址和邮件客户端程序。

4.3.3.4 自定义窗口的启动

通过上面的修改,“Hello World”项目又增加了几个文件。现在,再来让我们来看一下它的结构。

helloworld/
  chrome.manifest
  install.rdf
  chrome/
    helloworld/
      content/
        contents.rdf
        helloworld.js
        helloworld.xul
        overlay.js
        overlay.xul
      skin/
        classic/
          contents.rdf
          helloworld.css
          icon.png
          helloworld.png

但是,我们还要将前面的 overlay.js 做修改,因为它只是简单地调用了 alert 来显示“Hello,world!”内容。我们必须让它打开自定义的“Hello World”窗口,因此,我们必须对 overlay.js 中的 helloWorld 函数做修改。修改后的代码如下:

// This is the main function
function helloWorld()
{
    window.open("chrome://helloworld/content/","helloworld","chrome,centerscreen");
}

通过点击“工具(tools)”菜单下的“Hello, world!”菜单项,或扩展管理器中的 “关于(about)”菜单项,它将显示下面的效果。

custom.jpg

至此,“Hello World”项目就已经结束了。它只是一个简单的演示,你需要把它按 XPI 格式打包,用你负责测试的 Profile 来对其进行安装测试;或者你还可以把它安装到任何一台装有 Firefox 的机器上,看看是否和你在项目环境下运行的一样。

4.3.4 示例下载

附录

I. 参考网址

 

 

 

实战 Firefox 扩展开发

成 富, 软件工程师, IBM 中国软件开发中心

简介: Firefox 浏览器自身提供良好的扩展结构,使得开发人员可以方便的扩展其行为。很多网站,比如 del.icio.us,都提供 Firefox 扩展来提供更好的用户体验。学习这方面的知识不仅对于网站开发人员是有用的,其他人也可以通过开发扩展来解决一些使用 Firefox 中遇到的具体问题。本文以一个能够批量下载某个 HTML 页面上所有图片的 Firefox 扩展作为案例,详细的介绍了 Firefox 扩展的开发流程。这其中包括构建开发环境,使用 XUL 来描述用户界面,使用 JavaScript 来为扩展增加行为,扩展的打包、发布和更新等方面的内容。

案例介绍

本文中所要构建的是一个能够批量下载某个 HTML 页面上所有图片的 Firefox 扩展。通常我们在浏览包含许多图片的网页时,如果想要把自己感兴趣的图片全部下载下来,需要逐一在图片上点击右键,然后选择另存为,再选择文件存放的目录,最后才能把图片保存在本机上。另外一种做法是把整个网页都保存下来,不过这样会保存不需要的信息,包括 JavaScript 脚本和 CSS 文件等,会增加所需的磁盘空间,浏览起来也不方便。该扩展要做的事情就是把网页上所有的图片在一个新窗口中列出来,用户可以勾选其感兴趣的图片,并指定需要保存的目录。然后该扩展能够一次性把用户选择的图片都下载下来。用户以后浏览起来也更加方便。

回页首

构建开发环境

在动手开发之前,首先需要构建扩展开发所需的环境。Firefox 把用户的个人信息,包括设置、已安装的扩展等,都保存在一个概要文件中,默认是使用名为 default 的概要文件。通过创建一个专门为开发使用的概要文件,可以不影响正常的使用,也不会破坏个人信息。为了创建另外一个概要文件,运行 firefox –P,在弹出的“选择概要文件”的对话框中,新建一个名为 dev 的概要文件,并使用此概要文件来运行 Firefox。接下来需要安装几个帮助开发的扩展,分别是 Venkman、Extension Developer's Extension 、Console2 、Chrome List 和 Firebug。可以在 参考资源 部分找到这些扩展的下载地址。最后修改 Firefox 的设置使得调试更加容易。在地址栏输入 about:config 可以打开 Firefox 的参数设置页面。按照如下的设置修改参数:

清单 1. Firefox 扩展开发环境参数设置

                
javascript.options.showInConsole = true //把 JavaScript 的出错信息显示在错误控制台
nglayout.debug.disable_xul_cache = true //禁用 XUL 缓存,使得对窗口和对话框的修改不需要重新加载 XUL 文件
browser.dom.window.dump.enabled  = true //允许使用 dump() 语句向标准控制台输出信息
javascript.options.strict        = true //在错误控制台中启用严格的 JavaScript 警告信息

至此,开发环境就构建完成了。当需要进行扩展开发时,运行 firefox -P dev 启动 Firefox 即可。

回页首

构建初始的扩展目录结构

接下来正式进行扩展开发。首先介绍一下一个 Firefox 扩展的基本目录结构。

图 1. Firefox 扩展目录结构
Firefox 扩展目录结构

在 图 1 中,content 目录下面存放的是扩展的描述界面的 XUL 文件和增加行为的 JavaScript 文件。locale 目录存放的是本地化相关的文件。如果需要支持英文和中文,就可以在 locale 目录下面新建 en-US 和 zh-CN 目录来存放相应的本地化字符串。skin 目录存放的是一些 CSS 文件,用来定义扩展的外观。chrome.manifest 是 Chrome 注册的清单文件(参见 侧栏)。install.rdf 分别包含了扩展安装的信息。

什么是 Chrome?

Chrome 指的是应用程序窗口的内容区域之外的用户界面元素的集合,这些用户界面元素包括工具条,菜单,进度条和窗口的标题栏等。Chrome 提供者能为特定的窗口类型(如浏览器窗口)提供 chrome。有三种基本的 chrome 提供者:

  • 内容(Content):通常是 XUL 文件。
  • 区域(Locale) :存放本地化信息。
  • 皮肤(Skin):描述 chrome 的外观。通常包含 CSS 和图像文件。

在构建了初始的目录结构之后,需要让 Firefox 能够识别并加载该扩展。首先需要找到当前的概要文件所对应的目录。在 Microsoft Windows 2000 和 XP 的电脑上面,该目录是 C:\Documents and Settings\<您的登录用户名>\Application Data\Mozilla\Firefox\Profiles。找到该目录之后,可以看到以 dev 结尾的目录,那就是我们之前构建的开发环境的概要文件所在的目录。在其下的 extensions 目录下面,新建一个文件,其文件名为 install.rdf 中指定的扩展的 ID,此处为 batchimagesdownloader@cn.ibm.com。该文件的内容就是扩展内容所在的实际目录,比如: C:\FirefoxExtDev\batchimagesdownloader。Firefox 就能识别并加载我们添加的扩展了。在每次对扩展做了一定的修改之后,不需要重新启动 Firefox,只需要安装之前介绍的 Extension Developer's Extension,并在 Tools 菜单中单击 Extension Developer> Reload all Chrome 即可。接下来就可以尝试为扩展添加功能了。

回页首

构建基本的用户界面

我们首先从用户界面入手。如之前所述,我们希望在一个新的窗口中显示当前 HTML 页面中所有的图片,并可以让用户进行选择。Firefox 扩展使用 XUL 来描述其用户界面。XUL 提供了一套基于 XML 的描述方式,可以用来描述用户界面的各种组件,比如按钮、菜单和工具条等。最初始的界面包含显示图片的一个表格以及 OK 和 Cancel 两个按钮。

清单 2. 基本用户界面的 XUL 描述

                
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<window
  id="batchimagesdownloader-mainwindow" title="Batch Images Downloader"
  orient="horizontal" onload="mainWindowOnLoad();"
  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <vbox flex="1">
    <vbox flex="1">
      <label value="Images on this web page:" />
      <spacer style="height: 5px"/>
      <hbox height="500" width="750" style="overflow:auto;">
        <grid>
          <columns>
            <column/>
            <column/>
            <column/>
          </columns>
          <rows id="imagesContainer"></rows> <!-- 显示图片的表格 -->
        </grid>
      </hbox>
    </vbox>
    <spacer style="height: 10px"/>
    <hbox>
      <spacer flex="1"/>
      <button id="mainWindow-add-button" label="OK" default="true" 
	    oncommand="download();"/>
      <button id="mainWindow-cancel-button" label="Cancel" oncommand="close();"/>
    </hbox>
    <spacer style="height: 5px"/>
  </vbox>
</window>

上面就是显示图片的新窗口的界面元素的声明。下面需要添加用户的交互行为。

回页首

添加菜单事件响应

我们希望当用户点击 Firefox 上面的一个菜单项时,弹出刚才构建的新窗口。我们这里要做的是向 Firefox 自带的 Tools 菜单添加一个新的名为 Batch Images Downloader 的菜单项。当用户点击此菜单项时,就会弹出 代码清单2中定义的窗口。在扩展中可以使用覆盖(Overlay)来向已有的界面中添加元素。使用覆盖可以在运行时向一个 XUL 文档添加新的组件。我们在 chrome.manifest 中定义了名为 overlay.xul 的文件,会对 Firefox 已有的用户界面进行一定的修改。只需要在 overlay.xul 中添加下面的内容即可:

清单 3. 增加菜单项的 XUL 描述

                
<menupopup id="menu_ToolsPopup">
  <menuitem id="batchimagesdownloader-show" label="Batch Images Downloader" 
              oncommand="BatchImagesDownloader.show(event);"/>
</menupopup>

上面定义了当点击菜单项时,会调用 BatchImagesDownloader.show方法,这是在 overlay.js 中定义的一个 JavaScript 方法,用来处理新窗口的弹出。overlay.js 由 overlay.xul 包含进来。

清单 4. 菜单项的事件响应方法

                
var BatchImagesDownloader = {
  show : function() {
    var doc = window.getBrowser().selectedBrowser.contentDocument;
    var imageNodes = doc.getElementsByTagName("img"); //获取所有的 img 节点
    var params = {"imageNodes" : imageNodes};
    this.openWindow("BatchImagesDownloader.mainWindow", 
	  "chrome://batchimagesdownloader/content/mainWindow.xul", 
	  "chrome=yes,centerscreen", params);
  },
  
  //打开一个新的窗口,或是使得已经创建的窗口获得焦点
  openWindow : function(windowName, url, flags, params) {
    var windowsMediator = Components.classes["@mozilla.org/appshell/window-mediator;1"]
	  .getService(Components.interfaces.nsIWindowMediator);
    var aWindow = windowsMediator.getMostRecentWindow(windowName);
    if (aWindow) { 
      aWindow.focus();
    }	  
    else {
      aWindow = window.openDialog(url, windowName, flags, params);
    }
    return aWindow;
  }
};

添加上述的代码之后,可以通过点击 Tools 菜单项下来的 Batch Images Downloader 菜单项来弹出新的窗口。

回页首

显示图片

可以在 BatchImagesDownloader.show方法中看到,当弹出新窗口的时候,会把当前页面上的所有 img 节点都作为参数传递给新打开的窗口。这些 img 节点就是需要展现给用户并供其选择的。接下来要做的就是在新窗口中显示这些图片。在 JavaScript 的方法中,可以像在 HTML 中的 DOM 操作一样,对 XUL 定义的 DOM 树进行修改。这其中包括使用 document.createElementNS 来创建新的 XUL 元素,同样也可以使用 CSS 来修改 XUL 元素的外观。

清单 5. 显示图片的 JavaScript 方法

                
const COLUMNS_PER_ROW = 3;	//每行显示3张图片
function mainWindowOnLoad() {
  var params = window.arguments[0];
  var imageNodes = params.imageNodes;
  displayImages(imageNodes);
}

function displayImages(imageNodes) {
  imageNodes = imageNodes || [];
  var cols = COLUMNS_PER_ROW, row, image, hbox, checkbox;
  var rows = document.getElementById("imagesContainer");
  for (var i = 0, n = imageNodes.length; i < n; i++) {
    var imageNode = imageNodes[i];
    var imageSrc = imageNode.getAttribute("src");
    if (imageSrc == "") {
      continue;
    }
    if (cols >= COLUMNS_PER_ROW) {
      row = document.createElementNS(XUL_NS, "row"); //开始新的一行
      row.setAttribute("align", "center");
      rows.appendChild(row);
      cols = 0;
    }
    else {
      hbox = document.createElementNS(XUL_NS, "hbox");
      hbox.setAttribute("style", "padding:5px 5px 5px 5px;");
      image = document.createElementNS(XUL_NS, "image");//创建 XUL 图像元素来显示图片
      image.setAttribute("src", imageSrc);
      checkbox = document.createElementNS(XUL_NS, "checkbox");//创建 XUL 复选框元素以供用户选择
      checkbox.setAttribute("imageUrl", imageSrc);
      hbox.appendChild(checkbox);
      hbox.appendChild(image);
      row.appendChild(hbox);
      cols++;
    }
  }
}

我们需要在新窗口加载完成之后,就显示当前页面的所有图片。因此需要注册新窗口的 onload 事件的响应方法。这里是mainWindowOnLoad。在 mainWindowOnLoad 中,通过 window.arguments[0]可以获得作为打开新窗口的参数传进来的 img 节点列表。然后根据这些 img 节点的 src 属性,创建相应的 XUL 图像元素并显示在表格中,并在每个图片下面创建一个复选框以供用户选择。

回页首

下载图片

为了让用户能够下载所选的图片,需要添加新的界面元素让用户可以指定下载图片存放的目录,并提供一个进度条来显示当前的下载进度。

清单 6. 支持下载的用户界面 XUL 描述

                
<hbox>
  <label value="Save images to" />
  <textbox id="mainWindow-save-path" readonly="true" style="min-width: 15em;" flex="1"/>
  <button label="Browse..." oncommand="selectSaveDirectory();"/>
</hbox>
<spacer style="height: 10px"/>
<progressmeter mode="determined" id="downloadProgress" 
  value="0" style="visibility:hidden;"/>

该扩展提供了一个默认的图片保存路径,那就是当前用户的根目录。用户也可以选择他想要的保存图片的目录。

清单 7. 用户选择图片保存目录的 JavaScript 方法

                
var saveDirectory = getDefaultSaveDirectory();
function selectSaveDirectory() {
  const nsIFilePicker = Components.interfaces.nsIFilePicker;
  var fp = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
  fp.init(window, "", nsIFilePicker.modeGetFolder);
  var result = fp.show(); //显示目录选择对话框
  if (result == nsIFilePicker.returnOK) {
    var file = fp.file;
    saveDirectory = file;
    byId("mainWindow-save-path").value = file.path;//把目录的路径显示在文本框中
  }
}
	
//获得默认的图片保存目录,也就是当前用户的根目录
function getDefaultSaveDirectory() {
  var file = Components.classes["@mozilla.org/file/directory_service;1"]
                .getService(Components.interfaces.nsIProperties)
                .get("Home", Components.interfaces.nsIFile);
  return file;
}

当用户对图片进行了选择,并点击 OK 之后,需要执行图片下载的任务。在下载图片中,会使用 Firefox 的 XPCOM 的实现,请参看侧栏参考资料,获得关于 XPCOM 的更多信息。

清单 8. 下载单张图片的 JavaScript 方法

                
function downloadSingleImage(uri, callback) {
  var ios = Components.classes["@mozilla.org/network/io-service;1"]
              .getService(Components.interfaces.nsIIOService);
  var imageURI = ios.newURI(uri, null, null); //创建图像的 URI
  var imageFileName = uri.substring(uri.lastIndexOf("/") + 1);
  var channel = ios.newChannelFromURI(imageURI);  //创建读取 URI 指定的数据流的通道
  var observer = {
    onStreamComplete : function(loader, context, status, length, result) {
      var file = Components.classes["@mozilla.org/file/local;1"]
                    .createInstance(Components.interfaces.nsILocalFile);
      file.initWithFile(saveDirectory);  //图片保存的目录
      file.appendRelativePath(imageFileName);
      var stream = Components.classes["@mozilla.org/network/safe-file-output-stream;1"]
                     .createInstance(Components.interfaces.nsIFileOutputStream);
      stream.init(file, -1, -1, 0);
      var bstream = Components.classes["@mozilla.org/binaryoutputstream;1"]
                      .createInstance(Components.interfaces.nsIBinaryOutputStream);
      bstream.setOutputStream(stream);        
      bstream.writeByteArray(result, length); //把图片流的全部字节写入输出文件流中
      if (stream instanceof Components.interfaces.nsISafeOutputStream) {
        stream.finish();
      } 
      else {
        stream.close();
      }
      if (typeof callback == "function") {
        callback();
      }
    }
  };
  var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"]
             .createInstance(Components.interfaces.nsIStreamLoader);
  streamLoader.init(channel, observer, null);
}

什么是 XPCOM?

XPCOM 是一种跨平台的组件对象模型,类似于微软的 COM。它有多种的语言绑定,可以在 JavaScript,Java,Python 和 C++ 中使用和实现 XPCOM 的组件。XPCOM 本身提供了一系列核心组件和类,比如文件和内存管理,线程,基本的数据结构(字符串,数组)等。

这里的实现方式是读取从远程获取的图片数据流,并把相应的数据写入到本地磁盘指定的目录中。为了实现以异步的方式读取和保存数据,使用了 nsIStreamLoader 接口的实现。它从指定的通道读取数据,当数据读取完成之后,会通知相应的监听器。在这里,我们用图片的URL地址来初始化一个通道,同时创建了一个监听器。在 onStreamComplete 的方法中,把得到的图片字节流写入到本地文件存储中。最后,如果注册了回调函数,就执行此回调函数。

清单 9. 下载用户选择的全部图片的 JavaScript 方法

                
function download() {
  var rows = document.getElementById("imagesContainer");
  var checkboxes = rows.getElementsByTagName("checkbox");
  var imageUrls = [];
  for (var i = 0, n = checkboxes.length; i < n; i++) {
    if (checkboxes[i].checked) {
      imageUrls.push(checkboxes[i].getAttribute("imageUrl")); //用户选择的图片的 URL
    }
  }
  var progressmeter = byId("downloadProgress");
  progressmeter.style.visibility = "visible";
  var total = imageUrls.length, step = 100 / total, current = 0;
  for (var i = 0; i < total; i++) {
    downloadSingleImage(imageUrls[i], function() {
      var value = parseInt(progressmeter.value); //更新进度条
      progressmeter.value = value + step;
    });
  }
  close();
}

下载全部图片时,会逐个检查复选框的状态,把用户选择的图片的 URL 记录下来。对每张图片,都会调用 downloadSingleImage 以异步的方式来下载。在单张图片下载完成之后,会有回调函数来通知主窗口,更新进度条的状态。当所有图片都下载完成之后,关闭当前窗口。

至此,整个扩展就开发完成了。实际的扩展的截图如下:

图 2. 该 Firefox 扩展实际使用的截图
该 Firefox 扩展实际使用的截图

在 图 2 中,HTML 页面的内容是使用“flower”作为关键字来访问百度图片搜索。

回页首

打包、发布和更新

打包

扩展打包的过程非常简单。只需要把整个目录的内容打包成一个 ZIP 格式的文件,并把文件的扩展名改为 xpi 即可。需要注意的是 install.rdf 要在 ZIP 文件的根目录下面,这样扩展才能安装到 Firefox 中。

发布

如果您想让别人也来使用您开发的扩展,一种方式是直接把打包好的 xpi 文件发给对方,他只需要用 Firefox 打开这个文件,就会自动提示安装。另外一种方式是把该 xpi 文件存放在某个公开的 HTTP 服务器中,对方只需要用 Firefox 访问该 xpi 文件,就同样会自动提示安装。这里需要注意的是要正确的设置 xpi 文件的 MIME 类型。Firefox 能识别的 xpi 文件的 MIME 类型是 application/x-xpinstall。您可能需要配置您的 HTTP 服务器。

更新

如果您的扩展的开发周期较长,需要发布多个版本的话,可以利用扩展的自动更新的能力。这样当有新的版本发布时,Firefox 会自动提示用户去获取最新的版本。要实现自动更新,需要在扩展的 install.rdf 中指定描述更新信息的 rdf 文件的位置,该 rdf 文件通常命名为 update.rdf。在 update.rdf 中声明了当前最新的版本号和最新版本的下载地址。如果用户安装的扩展的版本低于 update.rdf 中声明的版本,则 Firefox 会提示用户是否更新。请参考本文附带的 源代码 中的 update.rdf 文件。

回页首

声明

本文章仅代表作者本人观点,与 IBM 公司无关。

回页首

下载

描述
名字
大小
下载方法

批量下载图片的 Firefox 扩展的源代码
batchimagesdownloader.zip
4KB
HTTP

关于下载方法的信息

参考资料

学习

获得产品和技术

讨论

 

Firefox 扩展开发进阶指南

在上一篇与 Firefox 扩展开发相关的《实战 Firefox 扩展开发 》中,笔者介绍了 Firefox 扩展开发的基本内容。这篇文章作为上一篇的后续,主要会讨论一些高级话题,而且以 Firefox 4 作为扩展开发平台。本文中的示例代码都在 Firefox 4 下测试通过。在开发的时候,建议使用 Firefox 便携版作为开发环境,下载地址见 参考资料 。Firefox 便携版中已安装扩展的文件存放在Data/profile/extensions 目录中,方便进行查看。下面首先介绍高级用户界面元素相关的内容。

高级用户界面元素及其操作

Firefox 扩展一般使用 XUL 来创建用户界面。相对于 HTML 来说,XUL 提供了更加丰富的用户界面组件,同时也支持用 CSS 来定义 XUL 元素的外观样式。大部分简单 XUL 元素的使用比较好理解,下面主要介绍一些复杂的用户界面组件及其用法。

窗口

一般的 Firefox 扩展都会打开一个新的窗口来展示界面。通常的做法是在用户点击菜单项或是工具栏上的按钮之后,弹出来一个新的窗口,与用户进行交互。通过 XUL 的 window 元素可以创建一个新的窗口,并在窗口中包含其它的 XUL 用户界面元素作为内容。通过window.open() 方法就可以打开一个新的窗口。该方法的返回值是窗口对象的一个引用,可以用来查看窗口的属性和对其进行操作,比如通过其 document 属性就可以获取到窗口文档的 DOM 对象。

在扩展中经常会需要对窗口进行查询。这里可以用 nsIWindowMediator 服务。该服务可以用来根据窗口类型进行查询。nsIWindowMediator 服务是一个 XPCOM 组件。关于 XPCOM 组件的细节,会在下面的章节中介绍。代码清单 1 中给出了nsIWindowMediator 的使用示例。

清单 1. 使用 nsIWindowMediator 查询窗口

 var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] 
    .getService(Components.interfaces.nsIWindowMediator); 
 var enumerator = wm.getEnumerator("navigator:browser"); 
 while(enumerator.hasMoreElements()) { 
    var win = enumerator.getNext(); // 迭代器中的每个元素都是一个窗口对象
    alert(win.document.title); 
 }

代码清单 1 所示,navigator:browser 是 Firefox 中浏览网页的窗口的类型。nsIWindowMediator 服务的 getEnumerator() 方法可以得到一个用来遍历所有指定类型窗口的迭代器。nsIWindowMediator 的另一个方法 getMostRecentWindow() 可以用来获取到最近打开的指定类型的窗口。该方法通常用来检查某个窗口是否已经被打开了。一般的做法是在窗口的 windowtype 属性上设置一个自定义的窗口类型,然后就可以用 getMostRecentWindow() 方法来查询此类型的窗口是否已经被打开。代码清单 2 给出了一个在扩展中常用的方法。该方法首先判断某个窗口是否已经被打开,如果没有,就打开此窗口,否则就把焦点移到该窗口上。

清单 2. 打开窗口或把焦点移到已有窗口上

 var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] 
    .getService(Components.interfaces.nsIWindowMediator); 
 var win = wm.getMostRecentWindow("dwSample-custom-window"); 
 if (win) { 
    win.focus(); // 设置窗口为当前焦点
 } 
 else { 
    window.open("chrome://dwSample/content/custom-window.xul", 
        "dwSample-test-custom-window","chrome,centerscreen"); 
 }

代码清单 2 所示,这里打开的窗口 custom-window.xul 中通过 windowtype 属性设置了自定义的窗口类型 dwSample-custom-window。该类型是 getMostRecentWindow() 方法的参数。

在使用窗口时的另一个常见需求是向新打开的窗口传递数据,比如新窗口初始化时所需的数据。当一个新窗口被打开的时候,在新窗口中可以通过 window.opener 来访问打开它的窗口的 window 对象。如果新窗口总是以固定的方式被打开,也就是说 window.opener的值是确定的话,可以通过 window.opener 来进行数据传递。如果 window.opener 属性的值不确定的话,可以使用 nsIWindowWatcher服务在打开新窗口的时候传递数据,具体用法见 代码清单 3

清单 3. 使用 nsIWindowWatcher 传递数据

 var args = { name : "Alex" }; 
 args.wrappedJSObject = args; 
 var watcher = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] 
    .getService(Components.interfaces.nsIWindowWatcher); 
 watcher.openWindow(null, "chrome://dwSample/content/window.xul", 
    "dwSample-test-window","chrome,centerscreen", args); 

代码清单 3 所示,args 就是传递给新窗口的参数。在新窗口中,可以通过 window.arguments 来获取到参数的值。window.arguments[0].wrappedJSObject 的值就是传递过来的 args 对象。

对话框

Firefox 扩展中可以使用两种类型的对话框:一种是 Firefox 提供的标准对话框,包括消息提示、确认和输入对话框等;另外一种则是由扩展本身使用 dialog 元素创建的自定义对话框。

在 HTML 页面中,可以使用 window.alert()window.confirm()window.prompt() 等方法来弹出浏览器标准的提示、确认和输入对话框。这些方法在扩展的 JavaScript 代码中也是可以使用的。但是在扩展中推荐的方式是使用 nsIPromptService 服务。使用nsIPromptService 中的方法创建对话框的一个重要优势是可以设置对话框的标题,另外还可以在对话框中添加一个额外的复选框。这个额外的复选框可以用来实现“下次不要再提示我”这样的功能。代码清单 4 中给出了 nsIPromptService 服务提供的对话框的使用方式。

清单 4. nsIPromptService 服务提供的对话框的使用

 var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 
    .getService(Components.interfaces.nsIPromptService); 
 ps.alert(null, "Simple Alert", "Hello World!"); 
 var checkValue = {value : true}; 
 ps.alertCheck(null, "Alert with checkbox", "Hello!", "Don't show again.", checkValue); 
 var result = ps.confirmCheck(null, 
     "Confirm", "Confirm", "Don't show again.", checkValue);
 var content = {value : "Alex"}; 
 result = ps.prompt(null, "Input something", "Your name :", content, null, {}); 
 if (result) { 
    ps.alert(null, "Input value", content.value); 
 }

图 1. 简单提示对话框
简单提示对话框
图 2. 带复选框的提示对话框
带复选框的提示对话框
图 3. 带复选框的确认对话框
带复选框的确认对话框
图 4. 输入对话框
输入对话框

图 1图 2图 3图 4 分别展示了 代码清单 4 中定义的四种不同对话框的运行效果。如 代码清单 4 中所示,alert()alertCheck() 方法用来打开消息提示对话框,不过后者可以在对话框上添加一个复选框。参数 checkValue 用来设置复选框初始时是否选中。类似的,confirm()confirmCheck() 都是用来打开确认对话框的。从 prompt() 方法的用法中可以得出打开对话框的代码和对话框之间的数据传递方式。创建一个包含属性 value 的 JavaScript 对象,并作为参数传递给 prompt() 方法。这个 JavaScript 对象的属性 value 的初始值作为对话框中显示的初始值。当对话框关闭之后,从属性 value 就可以获取到用户所输入的值。也就是说这个 JavaScript 对象既做输入,又做输出。

对于由 dialog 元素创建的对话框,在外观和作用上类似于由 window 元素创建的窗口,不同之处在于 dialog 提供了标准的按钮和对应的事件处理能力。这些标准按钮包括确定(accept)、取消(cancel)、帮助(help)、更多信息(disclosure) 和两个额外的自定义按钮(extra1 和 extra2)。对每个按钮都可以分别设置其标签、事件处理方法和快捷访问键。例如对确认按钮来说,可以通过buttonlabelacceptondialogacceptbuttonaccesskeyaccept 来分别设置其标签、事件处理方法和快捷访问键。通过 dialog 元素的 buttons 属性可以设置对话框上要包含的按钮名称,如 buttons="accept,cancel,help" 就声明了对话框中包含确定、取消和帮助等 3 个按钮。图 5 中给出了一个自定义对话框的运行效果图。

图 5. 自定义对话框
自定义对话框

通过 window.openDialog() 可以打开新的对话框,与 window.open() 不同的是,在调用 window.openDialog() 的时候可以传递额外的参数给对话框,这比打开新窗口时的参数传递要简单得多,如 代码清单 5 所示。

清单 5. window.openDialog() 打开对话框时的数据传递

 var args = { name : "Alex" }; 
 window.openDialog("chrome://dwSample/content/dialog.xul","dwSample-test-dialog", 
    "chrome,modal,centerscreen", args);

在对话框的代码中,可以通过 window.arguments[0] 来获取到 args 对象。

侧栏

有些扩展选择使用侧栏来作为其主界面,而不是用一般的新窗口。侧栏的界面声明应该被放在一个单独的 XUL 文件中,并且该 XUL 文件的根元素是 page 。一般来说,侧栏可以通过 Firefox 主菜单的“查看”->"侧栏"来打开和关闭。图 6 给出了一个侧栏的运行效果图。新的侧栏也需要在覆层(overlay )中进行注册。注册的方式如 代码清单 6 所示。

图 6. 侧栏
侧栏
清单 6. 在覆层中注册侧栏

 <menupopup id="viewSidebarMenu"> 
    <menuitem observes="viewDwSampleSidebar"  /> 
 </menupopup> 
 
 <broadcasterset> 
    <broadcaster id="viewDwSampleSidebar" 
                 label="dwSample 侧栏"
                 autoCheck="false"
                 type="checkbox"
                 group="sidebar"
                 sidebarurl="chrome://dwSample/content/sidebar.xul"
                 sidebartitle="dwSample 侧栏"
                 oncommand="toggleSidebar('viewDwSampleSidebar');" /> 
 </broadcasterset>

代码清单 6 所示,ID 为 viewSidebarMenu 的菜单上就是 Firefox 主菜单上的“查看”->“侧栏”项。在这个菜单项上多加一个子菜单用来打开和关闭侧栏。通过 toggleSidebar() 方法就可以切换某个侧栏的打开和关闭状态。

如果在扩展的其它地方需要访问侧栏的话,可以用 代码清单 7 中的方法。

清单 7. 与侧栏进行交互

 var sidebarWindow = document.getElementById("sidebar").contentWindow; 
 if (sidebarWindow.location.href == 
    "chrome://dwsample/content/sidebar.xul") { 
    sidebarWindow.sayHi(); 
 }

表格

在用户界面中常见的表格在 Firefox 扩展中是通过包括 listboxlistheadlistheaderlistcolslistcollistitemlistcell 等 XUL 元素来创建的。从与 HTML 表格的相关元素对应的角度来说,listbox 即表格本身,相当于 tablelisthead 是表头,相当于 theadlistheader 是表头中每一列的标题,相当于 thlistcol 是表格中的一列;listcolslistcol 的集合;listitem 是表格中的一行,相当于 trlistcell 是表格中的单元格,相当于 td 。以这种方式进行类比,就很容易了解在扩展中创建表格的方式。

listbox 的单元格中只能包含文本和图片。如果希望表格的单元格中能包含其它内容,如 XUL 组件,就需要使用 richlistboxrichlistitem代码清单 8 给出了在 richlistitem 中添加一个按钮的示例。

清单 8. 在 richlistitem 中添加一个按钮

 <richlistbox> 
    <richlistitem><button label="Button"></button></richlistitem> 
 </richlistbox> 

布局

在创建扩展的用户界面的时候,布局是其中一个很重要的方面。XUL 中的布局的基本元素是作为容器来使用的盒子(box),分成水平盒子和垂直盒子,分别由 hboxvbox 元素来创建。hboxvbox 分别在水平和垂直方向上依次排列其内部的元素。比如希望 3 个按钮(button )水平排列的话,只需要用一个 hbox 来包含它们就可以了。hboxvbox 内的 XUL 元素的一个重要属性 flex 用来声明盒子中剩余空间的分配方式。如果包含在盒子内部的组件不能占满 hboxvbox 盒子的全部空间,就会留出来相应的空白。通过设置内部组件的 flex 属性就可以分配这些空白区域。flex 属性的值是整数,定义的是空白区域在各个组件之间分配的相对比例。例如,3 个组件分别声明了 flex 属性的值是 422 的话,那么 3 个组件分别占据 50%25%25% 的剩余空白区域。hboxvbox的属性 alignpack 用来声明盒子中间组件的对齐方式。对于 hbox 来说,alignpack 分别用来声明在垂直和水平方向上的对齐方式,而对 vbox 来说则正好相反。

如果需要对多个 XUL 组件进行复杂的布局的话,可以使用 grid 元素。grid 可以用来实现复杂的表格式布局。在 grid 中可以包含多行和多列。行和列分别用 rowcolumn 表示,而 rowscolumns 则分别是行和列的集合。rowcolumn 的每个子元素都分别占据其所对应的单元格。通常是在其中嵌套使用 hboxvbox 来实现复杂的布局。

使用 HTML 创建界面

虽然 XUL 在使用方式上非常类似 HTML,但是有一部分扩展开发者还是对 HTML 语言比较熟悉,对 HTML 本身的布局方式和使用 CSS 来设计样式也比较了解。在 Firefox 扩展中,同样也是可以使用 HTML 页面来作为用户界面的。XUL 中的 browseriframe 元素可以用来显示一个 XUL 或 HTML 页面。这两个元素都有一个属性叫 type 用来声明所包含页面的内容类型。这个属性的值有安全性方面的作用,进而会影响包含 browseriframe 的窗口与其中的页面的数据传递方式。属性 type 默认值是 chrome ,指的是 HTML 页面是属于扩展的一部分。对于使用 HTML 来构建界面的扩展来说,这个值是很合适的。在这种情况下,HTML 页面是可以通过parent 属性来访问其父窗口的 window 对象的,如 代码清单 9 所示。如果属性 type 的值是其它值的话,从安全的角度考虑,这种访问方式是不允许的。

清单 9. HTML 页面与其父对话框的数据传递

 // 包含 HTML 页面的 XUL 对话框
 <script> 
    window.hobby = "Game"; 
 </script> 
 <hbox flex="1" width="300" height="300"> 
    <browser id="content" type="chrome" 
        src="chrome://dwSample/content/hobby.html" flex="1" /> 
 </hbox> 

 // 在 HTML 页面 hobby.html 中
 <script type="text/javascript"> 
    function onLoad() { 
        var node = document.getElementById("hobby"); 
        node.value = window.parent.hobby; 
    } 
 </script> 

代码清单 9 中,父对话框的 window 对象中定义了一个 hobby 变量,在子 HTML 页面中,通过 window.parent.hobby 来获取这个变量。图 7 给出了使用 HTML 创建界面的对话框的运行效果图,图中的对话框内容界面是由 chrome://dwSample/content/hobby.html页面来创建的。

图 7. 使用 HTML 创建界面的对话框
使用 HTML 创建界面的对话框

在介绍完高级用户界面元素及其操作之后,下面介绍如何通过 XBL 实现用户界面的组件化。

回页首

使用 XBL 实现组件化

在开发 Firefox 扩展的用户界面的时候,会遇到的一个现实问题就是用户界面的组件化。大多数时候,开发人员都是使用 XUL 的基本组件来构建用户界面。在有些情况下,扩展中的某些部分的用户界面可能需要在不同的地方被重复使用。这种代码重复显然是在开发中是要避免的。最直接的解决办法就是创建自己的用户界面组件。这样就只需要在一个地方进行定义和修改,在其它地方只是引用即可。在 XUL 中,这种自定义的组件是通过 XBL(XML Binding Language)来实现的。简单来说,通过 XBL 可以创建出新的 XUL 元素。这些 XUL 元素可以像基本的 XUL 元素一样在扩展中使用。自定义的 XUL 元素在内部封装了组件的展现和相关的逻辑,可以有自己的属性、方法和事件等。实际上,比较复杂的基本 XUL 元素也是通过 XBL 创建出来的。

在扩展中使用 XBL 由两部分组成,一个部分是自定义 XUL 元素的声明,另一个部分则是具体的使用这个 XUL 元素。XBL 组件的声明包含在一个 XML 文件中,而应用部分则通过一个 CSS 文件来指定。本文用一个简单的 XUL 元素作为示例来进行说明。该 XUL 元素是一个地址输入组件,允许用户选择所在省份和城市,以及输入详细的地址。完整的示例代码见 参考资料代码清单 10 中给出了该 XUL 元素的 XML 文件的部分内容和使用它的 CSS 文件的内容。

清单 10. XUL 元素声明的 XML 文件与使用它的 CSS 文件

 //XML 文件内容
 <?xml version="1.0"?> 
 <bindings xmlns="http://www.mozilla.org/xbl"
  xmlns:xbl="http://www.mozilla.org/xbl"
  xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> 
  <binding id="addressinput"> 
  </binding> 
 </bindings> 

 //CSS 文件内容
 dwaddressinput { 
  -moz-binding: url("chrome://dwSample/content/addressinput.xml#addressinput"); 
 }

代码清单 10 所示,XML 文档中根元素 bindings 下面的每个 binding 元素表示一个新的 XUL 元素。binding 的属性 id 是该 XUL 元素的标识符。在 CSS 文件中声明了一条规则,对 dwaddressinput 元素添加了 -moz-binding 声明。这条规则的含义是把元素dwaddressinput 关联到 chrome://dwSample/content/addressinput.xml 这个 XML 文件所声明的绑定上。由于一个 XML 文件中可以声明多个新的 XUL 元素,URL 后面的 #dwaddressinput 用来指定所关联的 XUL 元素的 ID,即 binding 元素的属性 id 的值。这样在引用了该 CSS 文件的 XUL 文件中,就可以创建出 dwaddressinput 元素。

内容

下面具体说明声明 XUL 元素的 XML 文件中包含的内容。在 binding 元素下的 content 元素用来定义该 XUL 元素的用户界面内容,可以包含其它的基本的 XUL 元素。需要注意的是,由于 XML 名称空间的要求,在声明基本的 XUL 元素的时候,需要使用相应的名称空间前缀,如 xul:hbox代码清单 11 中给出了示例 XUL 元素的 content 元素的内容。

清单 11. 示例 XUL 元素的 content 元素的内容

 <content> 
    <xul:hbox> 
        <xul:menulist label="省份" anonid="province-select" 
            oncommand="document.getBindingParent(this).provinceSelectChanged(event);"> 
        </xul:menulist> 
        <xul:menulist label="城市" anonid="city-select"> 
        </xul:menulist> 
        <xul:textbox anonid="location-input" /> 
    </xul:hbox> 
 </content>

代码清单 11 所示,有几个地方值得说明一下。首先是属性 anonid 用来声明元素的 ID。在这里不能直接使用 id 。这是因为一个 XUL 元素可能在一个页面上被多次使用。如果使用 id 的话,会造成页面上多个元素具有相同的 ID。属性 anonid 是一种替代 id 的标识符。在 XUL 元素的 JavaScript 代码中可以通过 document.getAnonymousElementByAttribute() 方法来根据 anonid 的值查询元素。代码中通过属性 oncommandmenulist 添加了事件处理代码。其中的 document.getBindingParent(this) 用来获取包含它的 XUL 元素,再调用其中的方法。provinceSelectChanged 是声明的 XUL 元素中添加的自定义方法。

方法

为新的 XUL 元素添加一个方法是通过 method 元素来实现的。代码清单 12 中给出了 provinceSelectChanged() 方法的声明。

清单 12. 示例 XUL 元素中的方法声明

 <method name="provinceSelectChanged"> 
    <parameter name="aEvent" /> 
    <body> 
        <![CDATA[ 
            var provinceSelect = document. 
                getAnonymousElementByAttribute(this, "anonid", "province-select"); 
            var province = provinceSelect.selectedItem.label; 
            this.selectProvince(province); 
        ]]> 
    </body> 
 </method>

代码清单 12 所示,method 元素的属性 name 表示的是方法的名称;子元素 parameter 表示的是该方法的参数,可以用多个parameter 元素来表示多个参数;子元素 body 中包含的是方法体的 JavaScript 代码。在该方法体被执行的时候,关键词 this 所指向的对象是 XUL 元素本身。在编写 JavaScript 方法体的时候,需要注意这一点。通过 method 元素来声明的方法可以被外部的 JavaScript 代码所使用。

属性

除了方法之外,还可以声明 XUL 元素的自定义属性。属性声明是通过 property 元素来实现的。代码清单 13 中给出了属性 province的声明。

清单 13. 示例 XUL 元素中的属性声明

 <property name="province"> 
    <getter> 
        <![CDATA[ 
            var provinceSelect = document. 
                getAnonymousElementByAttribute(this, "anonid", "province-select"); 
            return provinceSelect.selectedItem.label; 
        ]]> 
    </getter> 
    <setter> 
        <![CDATA[ 
            var province = val ? val : DEFAULT_PROVINCE; 
            this.selectItemByLabel("province-select", province); 
            this.selectProvince(province); 
            return val; 
        ]]> 
  </setter> 
 </property>

代码清单 13 所示,property 元素的 name 属性声明了属性的名称。声明属性中最重要的是定义获取和设置属性时的逻辑。这分别是通过 gettersetter 子元素来实现的。这两个子元素的文本内容就是获取或设置时要执行的 JavaScript 代码。如果要执行的 JavaScript 代码比较简短的话,可以省去这两个子元素,而用 property 元素的属性 ongetonset 来替代。在设置属性的 JavaScript 代码中,可以直接使用变量 val 。该变量的值是调用者提供的该属性的新值。另外设置属性的方法需要把 val 作为其返回值,这样就可以实现级联赋值。

property 元素具备类似功能的是 field 元素,它也可以用来定义 XUL 元素中的属性,不过只提供了基本的属性值存储功能,并不能自定义读取和设置时的逻辑。

具体使用

下面介绍如何具体的使用新创建出来的 XUL 元素。首先需要在 XUL 文件中引用声明了绑定关系的 代码清单 10 中给出的 CSS 文件,然后就可以像基本 XUL 元素一样直接声明,或是通过 JavaScript 代码来动态创建。代码清单 14 中给出了动态创建新声明的 XUL 元素 dwaddressinput 的方式。

清单 14. 动态创建 dwaddressinput 元素

 //JavaScript 代码
 function onLoad() { 
    var container = document.getElementById("container"); 
    var addressInput = createAddressInput(container, 
        'dwSample-address-input', "湖南省", "湘潭市", "韶山市"); 
    window.sizeToContent(); 
 } 

 function createAddressInput(parentNode, id, province, city, location) { 
    var addressInput = document.createElement("dwaddressinput"); 
    addressInput.setAttribute("id", id); 
    parentNode.appendChild(addressInput); 
    addressInput.province = province; 
    addressInput.city = city; 
    addressInput.location = location; 
    return addressInput; 
 } 

 function showAddress() { 
    var addressInput = document.getElementById("dwSample-address-input"); 
    alert(addressInput.getAddress()); 
 } 

 //XUL 代码
 <vbox flex="1" id="container"></vbox>

代码清单 14 所示,在 XUL 中创建了一个 vbox 元素作为新创建出来的 XUL 元素的容器。onLoad() 方法在加载完成之后被调用。JavaScript 方法 createAddressInput() 用来创建 dwaddressinput 元素,并通过其属性来设置初始值。新创建的 dwaddressinput 元素的 ID 是 dwSample-address-input 。方法 showAddress() 首先通过 ID 来获取到 dwaddressinput 元素的引用,再调用其中的方法getAddress() 。这个 getAddress() 方法也是通过 method 元素来声明的。图 8 中给出了实际的运行效果图。

图 8. XBL 使用示例
XBL 使用示例

在介绍完使用 XBL 创建自定义 XUL 元素之后,下面说明如何使用 XUL 数据模板。

回页首

XUL 数据模板

在 Firefox 扩展开发中总是免不了与各种不同类型的数据打交道,不管是扩展本身的内部数据,还是来自远程的数据。典型的处理数据的做法是获取数据之后先进行解析,提取出其中感兴趣的内容,再创建相应的用户界面元素来显示。整个过程繁琐而且容易出错。事实上,如果扩展所使用的数据类型是 RDF、XML 或 SQLite 数据库的话,使用 XUL 数据模板技术是更好的选择。

XUL 数据模板是一个强大的系统,在其中封装了数据的获取、查询和用户界面生成等操作。使用该技术可以用简洁的代码实现复杂的功能。下面使用一个简易的 RSS 订阅源阅读器扩展作为示例来进行说明。典型的 RSS 订阅源阅读器需要涉及到数据的获取、XML 格式的解析和查询、以及用 JavaScript 代码动态创建用户界面等操作。代码清单 15 给出了使用 XUL 数据模板实现的相关代码。

清单 15. XUL 数据模板示例

 <vbox height="600" style="overflow:auto;" 
    datasources="http://news.163.com/special/00011K6L/rss_newstop.xml" 
    ref="*" querytype="xml" flex="1"> 
    <template> 
        <query expr="channel/item"> 
            <assign var="?title" expr="./title/text()"/> 
            <assign var="?command" 
                expr="concat('viewContent(&quot;', ./link/text() , '&quot;);')" /> 
        </query> 
        <action> 
            <hbox uri="?"> 
                <description flex="1" value="?title" /> 
                <button label="查看" oncommand="?command" /> 
            </hbox> 
        </action> 
    </template> 
 </vbox>

代码清单 15 所示,使用 XUL 数据模板的实现非常简洁易懂,并没有复杂的动态创建用户界面的 JavaScript 代码。下面进行具体的分析和说明。首先是声明要使用的是数据源。vbox 元素的 datasources 属性来声明数据源的 URL。这个属性可以添加在任何 XUL 元素上。对于 RDF 类型的数据来说,可以指定以空格分隔的多个数据源 URL。XUL 数据模板会自动聚合多个 RDF 数据源。属性querytype 用来指定数据类型,可以是 rdfxmlstorage ,分别表示 RDF、XML 和 SQLite 数据类型。属性 ref 用来表示进行数据查询时候的起始位置。对于 XML 数据来说,数据查询总是从 XML 文档根节点开始的,因此这个属性并没有使用,一般设成 * 即可。

vbox 元素的子元素 template 中包含的就是 XUL 数据模板的定义。模板定义通常由两个部分组成,分别是查询 query 和动作 action。查询是用来从数据源中选择感兴趣的内容。对于 XML 数据来说,查询是通过 XPath 来完成的。query 元素的 expr 属性是查询的 XPath 表达式,这里是选择了 RSS 订阅源中所有的 item 元素。query 元素的子元素 assign 用来定义可以在模板中使用的变量。这主要是为了方便在模板中使用数据。在示例应用中,我们感兴趣的是每个条目的标题和链接。对于标题来说,通过 XPath 表达式./title/text() 获取到了 title 元素的文本内容,并赋值给变量 ?title 。变量名称前面的问号 ? 用来表明这是一个变量。而对于链接的处理,这里创建了一个变量 ?command 用来表示一个 JavaScript 方法调用,并把条目的链接封装在方法调用中。

action 元素里面包含的就是用户界面的模板。这里面可以使用任何基本的 XUL 元素。需要注意的是其中声明了属性 uri="?" 的 XUL 元素。对于查询结果中的每条记录,该元素及其子元素在最后的用户界面中都会被重复一次。就示例应用来说,在生成的用户界面中,对于 RSS 订阅源中的每个 item ,都会有一个 hbox 及其子 descriptionbutton 元素与之对应。descriptionbutton 元素都直接引用了查询中通过 assign 定义的变量。图 9 给出了示例的 RSS 阅读器的运行效果图。

图 9. RSS 阅读器
RSS 阅读器

在介绍完 XUL 数据模板之后,下面介绍如何在扩展中添加偏好设置。

回页首

偏好设置

有些 Firefox 扩展提供了一些配置选项,允许用户进行自定义。这些偏好设置在 Firefox 重启之后仍然是生效的。通过 Firefox 的“工具”菜单下的“选项”,就可以打开 Firefox 自己的偏好设置对话框。另外通过在浏览器地址栏输入 about:config 也可以进行修改。

偏好设置中的每个配置项都是一个名值对。名称是由扩展开发者自己指定的,一般是以固定的名称空间作为前缀以避免冲突。而配置项的值可以是字符串、整数和布尔值等简单类型,还可以是 Unicode 字符串、本地化字符串和文件路径等复杂类型。定义好扩展中所需的配置项的名称之后,就可以声明它们了。在扩展根目录下的 defaults/preferences 目录下创建一个 JavaScript 文件,用来声明配置项及其默认值。其内容如 代码清单 16 所示。

清单 16. 声明偏好设置配置项及其默认值

 pref("extensions.dwSample.locale", "zh_CN"); 

代码清单 16 所示,pref 是一个 JavaScript 方法,用来设置配置项的默认值。示例扩展中的所有配置项都使用extensions.dwSample 作为前缀。虽然通过这种方式声明配置项及其默认值不是必需的,但大多数情况下,这是一种好的实践。

定义好配置项之后,需要提供相应的界面让用户可以修改这些配置。虽然通过 about:config 可以直接修改,但对普通用户来说,这种用户体验并不友好。一般只适用于高级配置选项。事实上,通过 XUL 元素来创建扩展自己的配置修改界面是很容易的。代码清单 17 中给出了示例界面。

清单 17. 配置修改界面

 <?xml version="1.0"?> 
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?> 
 <?xml-stylesheet 
    href="chrome://browser/skin/preferences/preferences.css" type="text/css" ?> 
 <prefwindow xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> 
    <prefpane id="basic-options" label="基本设置"> 
        <preferences> 
      <preference id="pref-locale" 
      name="extensions.dwSample.locale" type="string"/> 
        </preferences> 
        <textbox label="语言:" preference="pref-locale"/> 
    </prefpane> 
 </prefwindow>

代码清单 17 所示,配置修改界面的根元素是 prefwindow ,而不是一般的 windowdialog 。在一个 prefwindow 中可以包含多个prefpane 。每个 prefpane 会被显示成一个标签页。preferences 元素下的每个 preference 元素用来声明一个配置项,其 nametype 属性分别是配置项的名称和类型。在声明了要修改的配置项之后,还需要提供相应的用户界面组件来让用户进行修改,如这里的textbox 元素。通过属性 preference 可以把一个组件与配置项绑定起来,其值应该是之前声明的 preference 元素的 id 。绑定之后,用户所做的修改会被自动同步到配置项中。

配置修改界面创建完成之后,在扩展代码中可以像其它对话框一样,通过 window.openDialog() 来打开它。一般还需要在扩展的install.rdf 中通过 <em:optionsURL>chrome://dwSample/content/settings.xul</em:optionsURL> 来声明。这样的话,用户就可以通过 Firefox 的扩展管理界面来修改设置。

接下来是如何在扩展代码中使用配置项,见 代码清单 18

清单 18. 在 JavaScript 代码中使用配置项

 var prefs = Components.classes["@mozilla.org/preferences-service;1"] 
    .getService(Components.interfaces.nsIPrefService); 
 prefs = prefs.getBranch("extensions.dwSample."); 
 var locale = prefs.getCharPref("locale"); 
 prefs.setCharPref("locale", "zh_CN"); 

代码清单 18 所示,首先获取到配置服务 nsIPrefService ,接着通过 getBranch() 方法获取到扩展本身的相关分支。最后根据配置项的数据类型,调用相应的获取和设置的方法即可,包括 getCharPref() /setCharPref()getIntPref() /setIntPref()getBoolPref() /setBoolPref() 。前面提到过,扩展的配置项的名称一般都带有固定的名称空间前缀,使用类似 x.y.z 的格式。这使得配置项的名称实际上形成了一种树形结构。通过 getBranch() 方法获取到树上的某个分支之后,再引用配置项名称的时候就可以不用带前缀了。

另外一个常见的需求是在配置项被用户修改之后得到通知,并执行相应的处理逻辑。这是通过添加配置项监听器来实现的,见 代码清单 19

清单 19. 添加配置项监听器

 var observer = { 
    observe : function(subject, topic, data) { 
        if(topic != "nsPref:changed") return; 
        switch (data) { 
            case "locale": 
                sayHello();    
                break; 
        } 
    } 
 }; 

 function addPrefListener() { 
    var prefs = Components.classes["@mozilla.org/preferences-service;1"] 
        .getService(Components.interfaces.nsIPrefService); 
    prefs = prefs.getBranch("extensions.dwSample."); 
    prefs.QueryInterface(Components.interfaces.nsIPrefBranch2); 
    prefs.addObserver("", observer, false); 
 }

代码清单 19 所示,通过 addObserver() 方法可以添加一个配置项的监听器。该方法的第一个参数表示的是感兴趣的配置项名称,可以是一个分支或是具体的配置项。第二个参数则是监听器对象本身。监听器对象需要包含一个 observe() 方法。当感兴趣的配置项发生变化的时候,observe() 方法会被调用。调用的时候 observe() 方法会接受 3 个参数:第一个参数 subject 表示的是配置服务对象,即上面的变量 prefs ;第二个参数 topic 表示的是变化的类型;第三个参数 data 表示的是发生变化的配置项的名称。

在介绍完偏好设置相关的内容之后,下面介绍 JavaScript 代码模块。

回页首

JavaScript 代码模块

JavaScript 代码模块(JavaScript code module)是 Firefox 3 中引入的新概念,用来在不同的作用域范围内共享 JavaScript 代码, 以及创建全局的 JavaScript 单例对象。除了在扩展中开发自己的代码模块之外,还可以使用 Firefox 提供的已有模块来简化扩展的开发。

一个 JavaScript 代码模块就是一个普通的 JavaScript 文件,惟一的不同是在文件中通过 EXPORTED_SYMBOLS 来声明该模块所暴露出来的对象。当该 JavaScript 模块被引入到其它作用域的时候,这些暴露出来的对象会被混入进去,可以直接使用。代码清单 20 给了一个简单的 JavaScript 代码模块。

清单 20. JavaScript 代码模块示例

 var EXPORTED_SYMBOLS = ["dwSampleModule"]; 

 var dwSampleModule = { 
    echo : function(message) { 
        return "echo ==> " + message; 
    } 
 }; 

代码清单 20 所示,该 JavaScript 代码模块暴露了一个对象 dwSampleModule ,其中包含了一个 JavaScript 方法 echo() 。模块文件以 .jsm.js 为后缀,并放在扩展根目录下的 modules 子目录中。除此之外,还需要在扩展的 chrome.manifest 文件中为 modules目录添加一个别名,如 resource dwSample modules/ 。这里的 dwSample 就是新声明的别名。在需要使用该 JavaScript 代码模块的地方,通过 Components.utils.import() 方法来引入,如 Components.utils.import("resource://dwSample/dwSample.jsm");Components.utils.import() 方法有两个参数:第一个参数是模块文件的路径,使用的是 resource:// 协议,路径中的 dwSample 就是之前声明的别名;第二个参数是模块中暴露的对象所混入的作用域,默认是当前的全局对象。如果希望为引入的模块再添加额外的名称空间,可以使用这个参数。代码清单 21 给出了如何在代码中使用 JavaScript 代码模块。

清单 21. 使用 JavaScript 代码模块

 Components.utils.import("resource://dwSample/dwSample.jsm"); 
 var message = dwSampleModule.echo("Hello!"); 

代码清单 21 所示,在引入了模块之后,就可以直接引用模块中暴露出来的对象 dwSampleModule 。需要注意的是,如果一个模块在不同的地方被多次引用,所有这些引用都共享一个对象。因此尽量不要修改引用的对象。

Firefox 也提供了一些标准 JavaScript 代码模块,可供扩展使用。通过这些代码模块,可以简化一些常见的操作。如resource://gre/modules/NetUtil.jsmresource://gre/modules/FileUtils.jsm 分别提供了与网络和文件操作相关的实用方法。

在介绍完 JavaScript 代码模块相关的内容之后,下面介绍 XPCOM 组件。

回页首

XPCOM

XPCOM(cross platform component object model)是 Mozilla 产品中使用的组件对象模型,类似于微软的 COM 组件。Mozilla 产品底层的很多服务都是用 XPCOM 组件来实现的。在上面的章节中,很多地方都用到了 XPCOM 组件,比如窗口管理、标准对话框和偏好设置等。XPCOM 组件的接口定义用 XPIDL 来描述,一个组件可能实现多个接口。同样的 XPCOM 组件接口,在不同的平台上可能有不同的实现。使用者通过接口来调用组件所提供的服务。XPCOM 组件可以有多种语言绑定,包括 C/C++、JavaScript、Java、Python、Perl 和 Ruby 等。可以用这些语言来编写 XPCOM 组件的实现,也可以访问 XPCOM 组件提供的服务。对于扩展开发来说,最常见的 XPCOM 绑定语言就是 JavaScript。

在 JavaScript 中使用 XPCOM 组件时候的两个重要的对象是 Components.classesComponents.interfacesComponents.classes包含的是系统中当前已经注册的所有 XPCOM 组件类,而 Components.interfaces 包含的则是所有的 XPCOM 组件接口。当需要使用某个组件类的时候,通过其标识符从 Components.classes 中获取。比如之前介绍过的标准对话框 XPCOM 组件的标识符是@mozilla.org/embedcomp/prompt-service;1 ,通过 Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 就可以获取到组件类。组件类分成一般类和单例对象两种。对于一般类来说,使用之前需要创建出相应的实例,这是通过 createInstance()方法来实现的;对于单例对象来说,只需要通过 getService() 获取到这个对象即可。createInstance()getService() 方法的参数都是 Components.interfaces 中包含的接口引用。因为 XPCOM 组件是通过接口来访问的,因此在使用之前,需要把组件对象转换成某个接口,再通过接口的方法来访问组件。对于实现了多个接口的组件,可以通过 QueryInterface() 来把组件对象转换成特定的接口。在偏好设置的 代码清单 19 中展示了这种用法。实际上,在组件类对象上调用 createInstance()getService() 并传入接口作为参数的时候,就相当于先创建或找到对应的对象,再转换成参数所指定的接口。

当需要使用某个 XPCOM 组件的时候,应该通过相关的文档说明来了解该组件所实现的接口,以及每个接口的声明。在有些时候,可能会需要创建自己的 XPCOM 组件。创建 XPCOM 组件比较复杂,而且使用的场景比较少。在这里就不做论述,相关的内容见 参考资料

在介绍完 XPCOM 组件之后,下面介绍扩展国际化相关的内容。

回页首

国际化

如果你开发的扩展希望被国外的用户使用的话,就需要添加国际化的支持。完整的国际化支持包括很多方面,如界面上文本,数字、日期和货币的格式等。这里只介绍其中最常见的界面文本的国际化。

首先需要把界面上可能出现的文本都提取出来。这些文本一般来自 XUL 文件和 JavaScript 文件。对于 XUL 文件来说,其中的文本应该被提取到文档类型声明(Document Type Definition,DTD)文件中。每个 XUL 文件都应该有多个与之对应的不同语言的 DTD 文件。这些 DTD 文件应该被放在扩展根目录下的 locale 目录下对应的语言代码子目录中。如英语和中文的 DTD 文件,应该分别被放在 en_USzh_CN 目录下。DTD 文件中包含的是实体的声明,每个实体对应于所需被本地化的一段文本。代码清单 22 给出了一个 DTD 文件的示例。

清单 22. 本地化 DTD 文件示例

 <!ENTITY dwSample.l11nMenuItem.label "本地化测试"> 

在 XUL 文件中,需要通过文档类型声明来引用 DTD 文件。如 <!DOCTYPE overlay SYSTEM "chrome://dwSample/locale/overlay.dtd"> 。通过 &dwSample.l11nMenuItem.label; 的方式来引用这些实体,即在实体名称前面加上 & ,后面加上 ;

DTD 文件适用于包含在 XUL 文件中的文本。如果希望在 JavaScript 代码中显示本地化文本的话,就需要用到属性文件。这里的属性文件与 Java 国际化中使用的属性文件并没有差别,是简单的名值对。为了方便读取属性文件中的内容,可以使用 XUL 中的stringbundle 元素。这个元素提供了一些方法用来从属性文件中读取文本。代码清单 23 中给出了 stringbundle 元素的使用示例。

清单 23. stringbundle 元素的使用示例

 <script> 
    function onLoad() { 
        var bundle = document.getElementById("dwSample-string-bundle"); 
        var message = bundle.getFormattedString("dwSample.greeting", ["Alex"]); 
        document.getElementById("messageContainer").value = message; 
    } 
 </script> 
 <stringbundleset> 
    <stringbundle id="dwSample-string-bundle"
      src="chrome://dwSample/locale/localized-dialog.properties" /> 
 </stringbundleset> 

代码清单 23 所示,stringbundleset 元素用来包含多个 stringbundle 元素。每个 stringbundle 元素用来引用一个属性文件。当需要读取文本的时候,首先获取到 stringbundle 元素,再调用其 getString()getFormattedString() 方法。getString() 方法用来直接根据名称获取对应的文本,getFormattedString() 方法则可以传入参数进行替换来生成文本。属性文件中的 %S%n$S 都可以作为占位符,不过后者指定了替换时候的参数顺序。比如同样的调用方式 getFormattedString("message", ["Alex", "Bob"]) ,如果属性文件中的写法是 message = Hello, %S and %S ,那么得到的文本是 Hello, Alex and Bobb ;如果写法换成 message = Hello, %2$S and %1$S ,那么得到的文本就是 Hello, Bob and Alex

在介绍完扩展国际化相关的内容之后,下面介绍如何实现扩展的自动更新。

回页首

自动更新

如果你的扩展是一个长期的开发项目,那么就会不断有版本的更新。这个时候,使用 Firefox 扩展的自动更新功能就是一个很好的选择。Firefox 会定期查找扩展的可用更新,并提示用户进行升级。对于扩展开发人员来说,要做的就是在扩展中启用这项能力,并满足 Firefox 对扩展更新的要求。

为了启用扩展的自动更新能力,首先需要创建一个 update.rdf 文件。这个文件中包含了扩展的最新版本的相关信息,主要包括新版本的版本号和对应的 XPI 文件的下载地址等。当有新版本发布的时候,只需要更新这个文件中的相关信息即可。在扩展的install.rdf 文件中通过 updateURL 来声明该扩展更新时使用的 update.rdf 文件的 URL。update.rdf 应该被存放在可访问的 Web 服务器上面。从 Firefox 3 开始,从安全的角度出发,Firefox 对扩展的更新增加了更加严格的限制:update.rdf 的 URL 需要使用 HTTPS 链接,如果使用 HTTP 的话,则需要添加额外的数字签名以进行验证;扩展新版本的 XPI 文件也需要使用 HTTPS 链接,如果使用 HTTP 的话,则需要添加额外的 XPI 文件的报文摘要信息。HTTPS 链接的证书失效等问题也会导致更新失败。对于大多数扩展开发者来说,找到一个支持 HTTPS 链接的服务器存放 update.rdf 和相关的 XPI 文件是一件比较困难的事情。因此本文主要介绍如何使用 HTTP 链接进行更新。

首先需要下载和安装工具 McCoy。这个工具可以在 install.rdf 中添加数字签名所需的公钥,以及对 update.rdf 进行签名。运行 McCoy 之后,首先需要创建数字签名所需的密钥。接着通过 Install 功能来处理 install.rdf ,这一步会加上密钥的声明。对于新版本的 XPI 文件,用 SHA 算法计算其报文摘要,并作为 updateHash 的值更新到 update.rdf 中。最后再通过 McCoy 的 Sign 功能处理update.rdf 文件。这一步会加上数字签名。把处理好的 update.rdf 和 XPI 文件存放到 Web 服务器上,就可以实现扩展的自动更新。

回页首

总结

通过开发 Firefox 扩展可以从不同的方面增强 Firefox 浏览器的功能。用户也习惯于使用扩展来优化浏览网页时的体验。本文详细介绍了 Firefox 扩展开发中的一些高级话题,包括高级用户界面元素及其操作、使用 XBL 创建自定义 XUL 元素、XUL 数据模板、JavaScript 代码模块、XPCOM、国际化和实现扩展的自动更新等。理解了本文中的这些内容之后,开发人员可以更加高效的开发出高质量 Firefox 扩展。

回页首

下载

描述
名字
大小
下载方法

包含本文中各个示例的 Firefox 扩展源代码1
dwSample.zip
15KB
HTTP

关于下载方法的信息

注意:

  1. 下载之后重命名为 dwSample.xpi 并进行安装。从 Firefox 的“工具”->“dwSample 菜单”选择各个示例进行查看。

参考资料

学习

获得产品和技术

讨论

posted @ 2011-12-14 19:25 Vegetable Bird 阅读(...) 评论(...) 编辑 收藏