chrome插件开发

dev guide总体概览

按照开发的目的,chrome extension可以分为

回到顶部


能做什么?

extensions可以使用浏览器提供给网页的api,扩展可以通过content scripts或者ajax与网页或者服务器进行交互,也可以通过编程与浏览器特性比如书签,其他tab进行交互

回到顶部


工具箱

manifest

必须的字段:

1
2
3
4
5
6
7
{
// Required
"manifest_version": 2,
"name": "My Extension",
"version": "versionString",
...
}

permissions

需要使用chrome.*APIs的时候,扩展在manifest中声明permission字段。如果API代表超能力,那么该字段做的事情就是可以给你开启这些能力。

1
2
3
4
5
6
7
8
"permissions": [
"tabs",
"bookmarks",
"http://www.blogger.com/",
"http://*.google.com/",
"unlimitedStorage",
...
],

当用户注册扩展的时候会显示permission warningpermission warning

  • activeTab

用户调用扩展的时候,给予扩展权限可以访问当前激活的tab。如果没有这个权限的话,扩展就得每访问页面一次就请求一次,也有可能被Hacker所利用。而有了该权限,获得权限仅仅是在用户的交互下(比如点击browser action,具体看下方)获得对应的访问权限,Hacker要攻击的话得等用户来主动触发,并且访问权限只会持续到该tab关闭。

什么情况下可以调用activeTab

  - 点击browser action
  - 点击page action
  - 点击context menu item
  - 通过commands API执行键盘快捷键
  - 通过地址栏API接受建议

当我们有这个权限之后也就有了访问API的超能力,这些超能力包括

- tabs.executeScript或tabs.insertCSS
- 调用API返回的tabs.Tab可以获得当前tab的URL,title和favicon

options_permissions

1
2
3
4
5
6
7
8
9
{
"name": "My extension",
...
"optional_permissions": [
"tabs",
"http://www.google.com/"
],
...
}

如果增加了权限,chrome会提示用户,和permission warning不同,不是在注册阶段。显示

browser action

在地址栏的右边,通过icon,popup.html,popup.js来控制icon

如要引用js,需通过src引入,不能直接script写入html页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name": "My extension",
...
"browser_action": {
"default_icon": { // optional
"16": "images/icon16.png", // optional
"24": "images/icon24.png", // optional
"32": "images/icon32.png" // optional
},
"default_title": "Google Mail", // optional; shown in tooltip
"default_popup": "popup.html" // optional
},
...
}

(与之对应的有page action,出现在地址栏内侧)

options pages

允许用户自定义扩展的外观,可以通过该页面来对自己的扩展进行配置

1
2
3
4
5
6
{
"name": "My extension",
...
"options_page": "options.html",
...
}

event pages

作为扩展程序的资源调度脚本,管理一些任务或者状态。在扩展的整个生命周期都存在,可以与content script通过事件机制来通信

content script

  • 访问的页面加载时注入,需要注意的是它在一个独立的环境下,与访问的页面通过shared DOM来通信,但不能访问页面的js变量或者函数,页面中的脚本也不能访问content scripts的变量和函数
  • 响应用户交互,程序动态注入,见例子
  • 通过API与页面,与扩展的其他部分通信的能力

chrome.*APIs

每个API都有Types,Properties,Methods和Events

Web APIs

除了chrome.*APIs,扩展也能使用浏览器提供给网页和web app的API。如果你想使用的API浏览器不支持,也可以通过绑定其他API库到你的扩展

1
2
3
4
5
6
<style>
div:hover {
transform: rotate(360deg);
transition: all 1s ease-out;
}
</style>
  • V8 APIs, 比如JSON,使用json相关函数
  • 自己添加的API库

回到顶部


架构

background pages

1
2
3
4
5
6
7
8
9
{
"name": "My extension",
...
"background": {
"scripts": ["eventPage.js"],
"persistent": false
},
...
}

background.html定义了Background pages,包含了控制extension的JavaScript脚本,有以下两种页面形式。一种是一直打开,一种是需要的时候加载,不需要的时候写在并且释放内存和其他资源。如果不是想一直开着节省资源,就用event pages。

  • persistent background pages
  • event pages

UI pages

点击ui弹出层popup.html,该页面可以调用background pages内的函数。一个页面中的extensions可以互相访问dom,可以互相调用函数
图片

Content scripts

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"content_scripts": [
{
"matches": ["http://www.google.com/*"],
"css": ["mystyles.css"],
"js": ["jquery.js", "myscript.js"]
}
],
// permissions字段表示限制在某些条件下注入content scripts
"permissions": [
"tabs", "http://www.google.com/*"
],
}

该文件用于与页面交互的时候。content scripts就是当前页面在浏览器中加载时需要执行的JavaScript代码。就把content scripts当做页面加载时候执行的一个脚本,而不是一个插件的一部分就好理解多了。
图片

能做的事情:

  • 找到网页中没有链接的url并且转为超链接
  • 增加字号
  • 找到并且处理DOM中的microformat数据

不能做的事情:

  • 不能使用插件页面定义的变量或者函数
  • 不能使用网页或者其他content scripts定义的变量或者函数
  • 不能使用chrome.*APIs,除了
    • extension( getURL , inIncognitoContext , lastError , onRequest , sendRequest )
    • i18n
    • runtime( connect , getManifest , getURL , id , onConnect , onMessage , sendMessage )
    • storage

content scripts 可以与父插件交换信息。下面的消息传递详细会讲。图片

消息传递

页面中的extension经常需要通信,因为他们执行在同一个进程的同一线程,页面可以互相直接调各自的函数。

可以使用chrome.extension方法来找到页面中的插件,getViews() 和 getBackgroundPage(), 在插件内一旦一个页面有了其他页面的引用,第一个页面就可以调用其他页面的函数,并且能够操纵他们的dom。

content scripts运行在页面环境中,而扩展不能。通常content scripts需要以某种方式与扩展进行通信。例如:RSS Reader扩展利用content scripts来检测页面中是否有RSS feed,然后通知background page来是否在当前页展示page action(上文提到的page action)

上文提到的某种方式指的就是消息传递,一方能够监听到来自另一方的消息,传递的形式可以是json,null,boolean,number,string,array,object.对于实现在共享上下文中交换多条信息的一次请求和长时间保持连接都有对应的API.也可以实现给不同扩展发消息,只要我们知道另一个扩展的ID,这方面知识可以参照cross-extension messages

简单的一次请求

runtime.sendMessage or tabs.sendMessage,可以实现从content scripts传递一个消息给扩展的其他脚本,然后脚本回传给你一些信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// from content script
chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
console.log(response.farewell);
});
// from extensions
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function( response) {
console.log(response.farewell);
});
});
// 接受方
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
console.log(sender.tab ?
"from a content script:" + sender.tab.url :
"from the extension");
if (request.greeting == "hello")
sendResponse({farewell: "goodbye"});
}
);
长连接

有时候需要长时间的保持会话而不是简单的一来一去,我们可以开启一个从content scripts到扩展的长连接通道,反过来一样。分别使用runtime.connecttabs.connect实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 从content scripts打开的通道,发送和监听消息的实现细节
var port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
if (msg.question == "Who's there?")
port.postMessage({answer: "Madame"});
else if (msg.question == "Madame who?")
port.postMessage({answer: "Madame... Bovary"});
});
// 从扩展打开通道和上述差不多,只不过需要指明要连接到哪个tab,并且使用tabs.connect
// 处理发送来的消息需要注册一个 `runtime.onConnect`事件监听器,不管从content scripts到扩展,还是从扩展到content scripts。当一方触发connect的时候,事件触发,与`runtime.Port`通过连接来收发信息
chrome.runtime.onConnect.addListener(function(port) {
console.assert(port.name == "knockknock");
port.onMessage.addListener(function(msg) {
if (msg.joke == "Knock knock")
port.postMessage({question: "Who's there?"});
else if (msg.answer == "Madame")
port.postMessage({question: "Madame who?"});
else if (msg.answer == "Madame... Bovary")
port.postMessage({question: "I don't get it."});
});
});
从页面传递消息

扩展可以与页面通信,需要在manifest中加入下述代码。

1
2
3
"externally_connectable": {
"matches": ["*://*.example.com/*"]
}

对应匹配到的页面就可以用messaging API,可以使用runtime.sendMessageruntime.connect发送消息给扩展。

1
2
3
4
5
6
7
8
9
// The ID of the extension we want to talk to.
var editorExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";
// Make a simple request:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
function(response) {
if (!response.success)
handleError(url);
});

在扩展中我们可以通过runtime.onMessageExternal or runtime.onConnectExternalAPIs来监听消息

1
2
3
4
5
6
7
8
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
if (sender.url == blacklistedWebsite)
return; // don't allow this web page access
if (request.openUrlInEditor)
openUrl(request.openUrlInEditor);
}
);

保存数据和无痕模式

可以使用storage API来数据持久化。无论什么时候想要保持数据的时候,都要考虑下是否是在无痕模式下。默认情况下,浏览器不会在无痕模式下,你应该考虑下用户在无痕模式下期待从你的插件里获得什么。

无痕模式承诺window不会留下痕迹,当在无痕模式下处理数据的时候,光荣的遵守这一承诺。

检测是否在无痕模式下,原理是检查tabs.Tab或者window.Window的incognito属性

1
2
3
4
5
6
7
8
9
function saveTabData(tab, data) {
if (tab.incognito) {
chrome.runtime.getBackgroundPage(function(bgPage) {
bgPage[tab.url] = data; // Persist data ONLY in memory
});
} else {
localStorage[tab.url] = data; // OK to store data
}
}

回到顶部


调试

chrome-extension://extensionId//filename

回到顶部


发布

开发模式下访问chrome://extensions启用Developer mode,点击Load unpacked extension,上传文件即可。

发布到Chrome 网上应用商店:

  • 有一个已经制作好插件
  • 需要一张信用卡来激活Google Wallet
  • 需要支付5美元用来启用开发者账户

回到顶部


例子

包括browser action,event page,option page,content script,消息传递,页面与content script的交互,content script与event page的交互