开发FireFox浏览器扩展(Extension)并实现与原生应用之间的消息传递

一、什么是插件(Plug-in),什么是扩展(Extension)。

插件指的是在页面HTML源代码里通过 或者 标签声明的部分,工作在内核层面的一种扩展技术,与浏览器无关,理论上可以用任何一种生成本地可执行文件的语言开发,比如C/C++、Delphi、VB、VS.Net等等,目前主流的浏览器插件技术主要有以下几种:

  1. ActiveX,由Microsoft提出,仅Internet Explorer(IE)浏览器支持,微软最新的Edge浏览器不再支持ActiveX技术。
  2. NPAPI(Netscape Plugin API),由Netscape提出,FireFox、Chrome等非IE浏览器基本都支持该项技术,不过后来因为安全性问题,Chrome从45版本之后放弃了对NPAPI的支持另搞了一套PPAPI技术,FireFox从53版本开始也放弃了对NPAPI的支持,据说Chrome的开源版本Chromium还可以用,不管怎么样这种技术的淘汰只是时间的问题。
  3. PPAPI(Pepper Plugin API),是 Google 在原有网景 NPAPI基础上搞的,它将插件放到沙盒里运行,所以具有更高的安全性。 目前只有 Chrome 浏览器及基于 Chromium 内核的浏览器支持。

扩展指的是利用浏览器提供的API来扩展浏览器功能的一种组件,工作在浏览器层面,通常使用HTML + Javascript 语言开发,与浏览器相关,与页面的HTML源代码无关。因此插件与扩展实质是两种不同的东西。

好了,废话不多说,下面进入FireFox浏览器扩展开发的详细介绍。

二、manifest.json

首先使用记事本一类的文本编辑器新建一个文本文件,然后在文件中输入以下内容后另存为manifest.json。

{
	"description":"Firefox extension demo description",
	"name": "Firefox extension demo",	
	"manifest_version":2,  
	"version":"1.0",
	"icons":{"48": "ICONS/ICON_48.png","64": "ICONS/ICON_64.png"},
	"applications":{"gecko":{"id": "[email protected]","strict_min_version": "50.0"}},
	"background":{"scripts":["background.js"]},
	"browser_action":{"default_icon":{"48": "ICONS/ICON_48.png"}}, 
	"permissions":["nativeMessaging"]
}

manifest.json是扩展的配置文件,主要作用是向FireFox浏览器说明扩展的名称、介绍、图标路径、版本、浏览器版本、后台脚本等基本信息。其中:

"name":扩展的名称,必须项。

"manifest_version" :manifest版本,必须项,且目前值必须为2。

"version":当前扩展的版本,必须项。

"icons":图标列表,可选项,它由图像大小(以px为单位)的键值对以及相对于manifest.json文件所在目录的图像路径组成,最好是至少提供一个48*48像素大小的图标,如果没有的话浏览器会使用标准扩展名图标,如果有多个图标的话浏览器会根据实际情况自动选择最合适的大小。

"applications":在Firefox 48版本之前为必须项。目前只包含了一个键:gecko,其中包含了4个属性:

  • id即扩展名ID。从Firefox 48起为可选项,在此之前为必须项,如果没有指定的话浏览器会随机分配一个,如果要实现与原生应用之间的消息传递则必须指定。
  • strict_min_version:Gecko所能支持的最小版本号。可以使用“ *”来定义版本号。替换为“ 42a1”。
  • strict_max_version:Gecko所能支持的最大版本号。如果安装或运行附加组件的Firefox版本号高于这个最大版本号,附加组件将不能运行或替换被安装。替代“ *”,意味着为不对最大版本号做检查。
  • update_url: 更新地址链接。注意链接必须以“ https”开头。这是为了使你自己就能够管理附加组件的更新。

"background":后台页面及脚本列表,可选项,通过background可以引入一个或者多个后台脚本文件,以及一个可选的后台页面文件。当扩展被加载的时候就会自动加载background中的内容并一直运行,直到扩展被禁止或卸载。

"browser_action":扩展在浏览器工具栏上的按钮,可选项,该按钮有个图标,并可拥有一个使用 HTML,CSS,和 JavaScript 指定内容的弹出窗口(popup)。如果使用弹窗,则该弹窗将在用户点击该按钮时打开,并且由弹窗中所提供 JavaScript脚本来处理用户与弹窗的交互。如果不使用弹窗,则会在用户点击该按钮时将点击事件传递给background中定义的JavaScript脚本。

"permissions":授权列表,可选项,因为需要与原生应用之间进行消息传递,所以必须具备“nativeMessaging”授权。

详情可参考官网:https://developer.mozilla.org/zh-CN/docs/Mozilla/Add-ons/WebExtensions/manifest.json

三、后台处理程序(background.js)

由于上文的manifest.json示例中"browser_action"项没有定义弹出窗口,所以用户点击浏览器工具栏中扩展对应的快捷按钮时,点击事件会传递给background中定义的background.js脚本,以下是background.js脚本的示例:

function onResponse(response) {
  console.log("background Received " + JSON.stringify(response));
}

function onError(error) {
	console.log(`background Error: ${error}`);
}

/*
On a click on the browser action, send the app a message.
*/
browser.browserAction.onClicked.addListener(() => {
  var JsonObj = {"name":"张三","sex":"男"};
  console.log("send:"+JSON.stringify(JsonObj));
  var sending = browser.runtime.sendNativeMessage("demo",JsonObj);
  sending.then(onResponse, onError);
});

这段代码的实际作用是给浏览器工具栏中扩展对应的按钮添加了一个侦听器来监控按钮的点击事件,当点击按钮时通过browser.runtime.sendNativeMessage方法将JsonObj对象作为参数传递给名为demo的原生应用,并通过回调函数onResponse和onError来处理demo反回的消息和错误。

详情可参考官网:https://developer.mozilla.org/zh-CN/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendNativeMessage

四、原生应用的配置文件(demo.json)

上面通过sendNativeMessage方法把JsonObj对象发送给了名为demo的原生应用,那么这个demo究竟是个什么东西呢,因此还需要一个配置文件(demo.json)来向浏览器进行说明,以下是demo.json的示例:

{
  "name": "demo",
  "description": "demo native app",
  "path": "/usr/bin/demo",
  "type": "stdio",
  "allowed_extensions": [ "[email protected]" ]
}

其中:

"name"必须和browser.runtime.sendNativeMessage("demo",JsonObj)中的第一个参数"demo"保持一致。在 OS X 和 Linux 中,它必须和配置文件的文件名保持一致(除.json文件扩展名外)。在 Windows 中,它必须和注册表项的名称一致。只能包含小写字母、数字、下划线和 . ,并且不允许开头或结束是 . ,并且 . 后面不能是 . 。

"type"必须是"stdio"。

"allowed-extensions"中列出了可以与该原生应用通信的ID,也就是说必须包含上面manifest.json中gecko项下id对应的值。

"path"指定了原生应用的具体路径和文件名,Windows下可以是相对路径,在 OS X 和 Linux 中,必须是绝对路径。

那么这个demo.json具体应该存放在什么位置呢?

  • 如果是Windows平台,那么需要在注册表项HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\NativeMessagingHosts\或者HKEY_CURRENT_USER\SOFTWARE\Mozilla\NativeMessagingHosts\下新建一个名为demo的项,其默认值为demo.json文件的路径。

  • 如果是Mac OS X平台需要把demo.json复制到/Library/Application Support/Mozilla/NativeMessagingHosts/或者~/Library/Application Support/Mozilla/NativeMessagingHosts/目录下

  • 如果是Linux平台需要把demo.json复制到/usr/lib/mozilla/native-messaging-hosts/或者/usr/lib64/mozilla/native-messaging-hosts/或者~/.mozilla/native-messaging-hosts/目录下

详情可参考官网:https://developer.mozilla.org/zh-CN/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#%E6%B8%85%E5%8D%95%E8%B7%AF%E5%BE%84

五、原生应用的开发

原生应用也就是上面demo.json中path指向的可执行文件,该可执行文件必须采用stdin来接收消息,stdout来发送消息,每条消息必须序列化成UTF-8格式的JSON数据,在消息之前有4个字节长度的数据来表示消息的长度,浏览器发送给原生应用的单条消息最大是1MB,总消息不得超过4GB。

以下是C的代码,使用了cJSON库来处理JSON数据。

#include 
#include 
#include 
#include 
#include 
#include "cJSON.h"

void my_handler(int);
void setCatchSignal();
unsigned int message_in(unsigned int,char *);
unsigned int message_out(char *);
unsigned int message_in_len(void);

volatile bool bCommunicationEnds=false;             //结束通讯标志

int main(int argc, char* argv[])
{    
    setCatchSignal();								//拦截信号	        
	char *inbuff;

    do { 		
		unsigned int len=message_in_len();			//取输入内容长度
		inbuff=(char *)malloc(len);					//申请空间		
		message_in(len,inbuff);						//接收数据		
		cJSON *inmessage=cJSON_Parse(inbuff);		//转json对象		
		message_out(cJSON_Print(inmessage));		//输出
		cJSON_Delete(inmessage);					//删除json对象
		free(inbuff);								//释放空间				
    }while(!bCommunicationEnds);

    return 0;
}

void my_handler(int s){ 
	bCommunicationEnds=true;
}

void setCatchSignal(){
	struct sigaction sigHandler;  
   	sigHandler.sa_handler = my_handler;  
   	sigemptyset(&sigHandler.sa_mask);  
   	sigHandler.sa_flags = 0;  
   	sigaction(SIGTERM, &sigHandler, NULL); 
}

//接收消息函数
//inbuff : 接收到收到的数据
unsigned int message_in(unsigned int len,char *inbuff){	
    for(int i=0;i>0) & 0xFF)));				//输出长度,4字节
    putchar(char(((len>>8) & 0xFF)));
    putchar(char(((len>>16) & 0xFF)));
    putchar(char(((len>>24) & 0xFF)));
            
    for(int i=0;i

按照FireFox官网的说法,浏览器与原生应用之间的消息传递有两种模式,基于连接的消息传递和无连接消息传递。

  • 基于连接的传递方式,通过runtime.connectNative()方法返回一个Port对象,再通过Port对象的postMessage()方法发送消息,最后通过Port对象的disconnect()方法或关闭与其连接的页面来关闭原生应用。
  • 基于无连接的传递方式,直接通过runtime.sendNativeMessage()来发送消息,原生应用在收到消息并发送响应后关闭。然而实际上在C里面不使用fclose(stdout)去主动关闭stdout的话原生应用并不会自动退出,c++里面用cout进行输出的话却没有这个问题不知道是为什么,对于一个C/C++的菜鸡来说确实困扰了我好久。

详情可参考官网:https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging

六、测试

打开FireFox浏览器,地址栏中输入about:addons回车后打开扩展管理界面,如下图所示:

开发FireFox浏览器扩展(Extension)并实现与原生应用之间的消息传递_第1张图片

点击"调试附加组件"如下图所示:

开发FireFox浏览器扩展(Extension)并实现与原生应用之间的消息传递_第2张图片

点击"临时载入附加组件",然后选择manifest.json文件,打开后载入浏览器扩展,再点击"检查"进入控制台,如下图所示:

最后点击FireFox工具栏中扩展对应的按钮看到结果,如下图所示:

七、打包和发布

将manifest.json及其中引用到的各类图标、js脚本、html、css等文件制作成一个.zip包,登录addons.mozilla.org提交获取签名,签名之后就可以安装使用了。

详情可参考官网:https://extensionworkshop.com/documentation/publish/

你可能感兴趣的:(浏览器扩展)