[TOC]
# Google Chrome扩展简介
Google Chrome扩展是一种软件,以增强Chrome浏览器的功能,它是**以chrome浏览器为宿主运行**的一个web程序。
Google Chrome扩展使用HTML、JavaScript、CSS和图片等Web技术开发。
## 扩展(Extension)与插件(Plugin)
Google Chrome扩展与Google Chrome插件不同。
1. Google Chrome扩展无需了解浏览器的源代码,用JS开发。
2. Google Chrome插件是更底层的浏览器功能扩展,需要深入掌握浏览器的源代码,用C/C++开发。
这意味着 Plugin 以Native Code运行,在性能上要优于 Extension,适合执行**计算密集型**工作。不过,以 Native Code 运行,使得 Plugin 在安全上面临更大挑战。
## 两种类型的 Plugin
Chromium最初支持两种类型的 Plugin:NPAPI Plugin 和 PPAPI Plugin。
1. NPAPI 全称是Netscape Plugin Application Programming interface。
2. PPAPI 全称是Pepper Plugin Application Programming Interface。
从表面上看,两者的区别在于使用了不同的API规范。其中,NPAPI来自于Mozilla,而PPAPI来自于Google。但实际上,**PPAPI与另外一种称为Native Client(NaCl)的技术是紧密联系在一起的**。
[NPAPI 为什么会被 Chrome 禁用?受影响的网站有什么普遍性?](https://www.zhihu.com/question/31227185?rf=30953196)
# Chrome扩展的基本组成
至少包括一个 `manifest.json` 和一个 `js 文件`
* **manifest.json 是扩展的调度中心,用于声明各种资源。该文件采用JSON格式定义**
* js 文件中定义要执行的操作
Google Chrome扩展,通常还可以包括图标、页面和CSS等资源
* 图标通常是`19px*19px`的PNG文件
* 页面通常是HTML文件,用于定义显示给用户的窗口,如popup页面或options页面等
## Manifest
示例:
```js
{
"manifest_version": 2,
"name": "我的时钟",
"version": "1.0",
"description": "我的第一个Chrome扩展",
"icons": {
"16": "images/icon16.png",
"48": "images/icon48.png",
"128": "images/icon128.png"
},
"browser_action": {
"default_icon": {
"19": "images/icon19.png",
"38": "images/icon38.png"
},
"default_title": "我的时钟",
"default_popup": "popup.html"
}
```
`manifest.json`文件,就是一个json格式标准的文件。
1. name **必须**,用来标识扩展的简短纯文本。这个文字将出现在安装对话框,扩展管理界面,和store里面
2. version **必须**,定义了扩展的版本,用一个到4个数字来表示,中间用点隔开。
3. manifest_version **必须**,表示manifest文件自身格式的版本号。从Chrome 18开始,必须指定版本号为数字`2`
4. description 定义了插件的详细描述信息
5. app 对象定义了要打开的URL地址
6. iocns 对象定义了几种不同尺寸的图标的地址
7. requirements 对象定义了需要用到资源权限
8. browser_action为设置扩展在浏览器右上角的一些动作,比如鼠标悬停展示的标题default_title、鼠标点击展示的页面default_popup等。
9. 文件中出现了两个icons的key,第一个为在浏览器扩展页面上展示的图标,第二个default_icons为在浏览器右上角展示的图标。
10. 我们需要新建一个images文件夹,并放入一些图标文件。
# 开发&测试你的扩展
还好,有一种方法可以测试您的扩展, 而不必将其发布到 Chrome 的 web 商店。在 chrome 浏览器的地址栏中, 只需键入:
~~~
chrome://extensions
~~~
勾选 **Developer mode**,然后点击**Load unpacked extension...**按钮。然后选择存放你开发扩展文件的工作目录。
![](https://img.kancloud.cn/bd/c1/bdc103efd6ba471525c3c8ddd6359407_600x86.png)
记住:每次修改了代码要重新加载扩展。
# Chrome 扩展架构
如下图示,一个Chrome 扩展的架构:
![the architecture](https://img.kancloud.cn/56/71/5671155ac49515bcb2949bf95b825a89_600x223.png)
## Background Pages
每个扩展都有一个不可见的后台页面由浏览器运行,每个扩展都有一个由浏览器运行的不可见的后台页面。
有两种类型——**持久的后台页面**和**事件页面**。
第一种,会一直保持在后台活动的状态。
第二种,只有在需要时才会活动。
Google鼓励开发人员使用事件页面,因为这样可以节省内存并提高浏览器的整体性能。但是,要知道这也是你应该将主逻辑和初始化的地方。
通常,背景页面/背景脚本 在扩展的其他部分之间扮演桥梁的角色。
在`manifest.json`文件中可以如此定义`background`:
~~~
"background": {
"scripts": ["background.js"],
"persistent": false/true
}
~~~
正如您可能已经猜到的,如果 `persistent` 属性是 `false` ,那么您将使用**事件页面**。否则,您将使用一个持久的后台页面。
顾名思义,可以理解为背景页面脚本,或者直接解释为后台脚本。background 用来处理插件本身的一些逻辑,比如插件加载时需要执行的处理,运行中需要统一维护的数据等等,background 只会在插件加载的时候运行一次,你可以在这个过程中让它绑定一些运行中的事件。
## Content Script
如果您需要访问当前页面的DOM,那么您就需要用到内容脚本 content script。该脚本代码是在当前web页面的上下文中运行的,这意味着它将在每次页面刷新时执行。要添加这样的脚本,请使用以下语法
```
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["content.js"]
}
]
```
请记住,`matches`的值决定了您的脚本会对哪个页面有效。请阅读有关[匹配模式](https://developer.chrome.com/extensions/match_patterns.html)的更多信息。
content script 跟页面 page 共用同一份页面的 dom,也就是说 content script 可以直接去访问或修改当前页面的 dom,但是注意了,它们只是共享了 dom 的访问,js 处理本身却是在两个不同的沙盒中运行的,所以并不能互调各自的js代码。
如下处理是在当前页面中插入一个div节点:
```
var element = document.body.firstChild;
var div = document.createElement("div");
document.body.insertBefore(div, element);
```
那么在 content script 中能跟 background 交互吗?当然。首先在 content script 中可以通过`chrome.extension.sendRequest`给 background 发送消息请求,同时可以通过`chrome.extension.onRequest.addListener`来监听从 background 发送来的消息。
content script 除了跟 background 可以交互,跟 web page 本身也可以有信息交互。一方面 content script 可以直接访问 page 的 dom,同时还可以通过 dom 的 Event 来跟页面进行交互。
## 用户界面
有几种方法可以构建你的扩展的 UI。以下是最受欢迎的四种。
### Browser Action
大多数开发人员使用`browser_action`属性来构建他们的插件。一旦你设置好了,一个代表你的扩展名的图标就会被放在地址栏的右边。然后,用户可以单击图标并打开一个实际上是由您控制的HTML内容的弹出窗口。
![](https://img.kancloud.cn/69/2d/692d99383555b2936ed6b686bc95042f_471x145.png)
`manifest.json`文件中对`browser action`进行配置。
```
"browser_action": {
"default_icon": {
"19": "icons/19x19.png",
"38": "icons/38x38.png"
},
"default_title": "That's the tool tip",
"default_popup": "popup.html"
}
```
`default_title`是一个小工具提示,当用户鼠标悬置在您的图标上时显示。`default_popup`实际上是在弹出窗口中加载的名为`popup.html`的HTML文件。还有一个徽章,你可以把它放在你的图标上。您可以在后台脚本中执行该操作。例如
~~~
chrome.browserAction.setBadgeText({text: "yeah"});
~~~
在`browser action`中通过background对象可以直接调用background中定义的方法或对象,如下所示,假设在`background.js`中定义了`testBG`函数,那么在popup.html中可以这样访问:
~~~
var bg = chrome.extension.getBackgroundPage();
bg.testBG();
~~~
### Page Action
`page_action` 属性类似于browser action,但是它会在地址栏中显示图标,所以一般会跟当前访问的URL地址进行交互。
![](https://img.kancloud.cn/4b/fc/4bfc1c96b80b282374fc4057eeb47f85_600x79.png)
有趣的是,您的图标最初是隐藏的,因此您可以决定何时显示它。例如,在上面的图片中,RSS 图标只有在当前页面包含了 RSS 提要的链接时才会显示。如果你需要一直看到你的图标,直接使用 `browser_action` 是很好的
使用`page action`功能,需要在`manifest.json`中定义如下属性:
```
"page_action": {
"default_icon": {
"19": "images/icon19.png",
"38": "images/icon38.png"
},
"default_title": "Google Mail",
"default_popup": "popup.html"
}
```
与 browser action 的图标不同,页面动作的图标没有 badges。
我们可以把对 page aciton 的设置和处理放在 background page 中,从而直接在 background 中通过 `chrome.pageAction` 来设置page action,比如如下代码实现了当所访问URL中有 rss 字符串时就显示page action的 icon 这样的功能:
```
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
if (tab.url.indexOf("rss") > -1) {
chrome.pageAction.show(tabId);
}
});
```
### DeveloperTools
我经常使用开发者工具,很高兴Chrome提供了一种方法来为这些工具添加新的标签。首先要做的是添加一个HTML页面,当面板打开时,它将被加载。
~~~
"devtools_page": "devtools.html"
~~~
不需要在页面内放置任何HTML,除了在JavaScript文件的链接,这将创建标签:
~~~
<script src="devtools.js"></script>;
~~~
然后在 `devtools.js` 文件中包含以下代码:
```js
chrome.devtools.panels.create(
"TheNameOfYourExtension",
"img/icon16.png",
"index.html",
function() {
}
);
```
现在,上面的代码会添加一个新的名为`TheNameOfYourExtension` 的标签,一旦你点击它,浏览器就会在DeveloperTools里面加载该 `index.html`。
### Omnibox
`omnibox`是在Chrome的地址栏中显示的关键字。例如,如果您将以下属性添加到您的manifest中:
```js
"omnibox": { "keyword" : "yeah" }
```
然后在 background 脚本中添加以下代码:
```js
chrome.omnibox.onInputChanged.addListener(function(text, suggest) {
suggest([
{content: text + " one", description: "the first one"},
{content: text + " number two", description: "the second entry"}
]);
});
chrome.omnibox.onInputEntered.addListener(function(text) {
alert('You just typed "' + text + '"');
});
```
你应该能够在地址栏里输入`yeah`。然后你会看到这样:
![](https://img.kancloud.cn/42/41/4241bd72e93d5e1e6df89fb7b8125433_600x63.png)
按下 tab 键会产生以下屏幕:
![](https://img.kancloud.cn/25/0a/250a1976b8c9e16e96975f6331c6f5f2_600x111.png)
当然是使用了 `chrome.omnibox` API,您可以捕获用户的输入并对该输入作出反应。
## APIs
你可以在扩展中做很多不同的事情。例如,您可以访问用户的书签或历史记录。你可以移动,创建标签,甚至调整主窗口的大小。我强烈建议查阅[文档](https://developer.chrome.com/extensions/api_index.html),以便更好地了解如何完成这些任务。
您应该知道的是,并不是所有的 APIs 都可以在扩展的每个部分中使用。例如,您的内容脚本不能访问`chrome.devtools.panels`或您的开发者工具标签中的脚本不能读取页面的DOM。所以,你肯定想知道为什么有些东西不起作用:
### 消息传送
正如我上面提到的,您并不总是能够访问您想要使用的API。如果是这样,那么您应该使用消息传递。有两种类型的消息传递—— 一次性请求和长时间连接。
#### [兼容性](https://stackoverflow.com/questions/15718066/chrome-runtime-sendmessage-not-working-as-expected)
`chrome.runtime.sendMessage / onMessage`(还有`connect`也是相似的)都是在 Chrome 26 开始引入的。
如果你想兼容 Chrome 20 - 25 版本,那么请使用 `chrome.extension.sendMessage`。
最好的兼容 `chrome.runtime` 方法如下示例:
```js
if (!chrome.runtime) {
// Chrome 20-21
chrome.runtime = chrome.extension;
} else if(!chrome.runtime.onMessage) {
// Chrome 22-25
chrome.runtime.onMessage = chrome.extension.onMessage;
chrome.runtime.sendMessage = chrome.extension.sendMessage;
chrome.runtime.onConnect = chrome.extension.onConnect;
chrome.runtime.connect = chrome.extension.connect;
}
```
#### 一次性请求
这种类型的通信只发生一次。也就是说,你发送一个信息,等待一个回复。例如,您可以在 background 脚本中放置以下代码:
```js
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
switch(request.type) {
case "dom-loaded":
alert(request.data.myProperty);
break;
}
return true;
});
```
然后在你的 content 脚本中使用下面的代码:
```js
window.addEventListener("load", function() {
chrome.runtime.sendMessage({
type: "dom-loaded",
data: {
myProperty: "value"
}
});
}, true);
```
这就是如何获取关于当前页面DOM的信息并在background 脚本中使用它。如果不这样 ,background 脚本通常无法访问这些数据。
#### 长时间连接
如果需要持久的通信通道,请使用这种类型的消息传递。在您的 content 脚本中,放置以下代码:
```js
var port = chrome.runtime.connect({name: "my-channel"});
port.postMessage({myProperty: "value"});
port.onMessage.addListener(function(msg) {
// do some stuff here
});
```
然后在background 脚本中,使用这个:
```js
chrome.runtime.onConnect.addListener(function(port) {
if(port.name == "my-channel"){
port.onMessage.addListener(function(msg) {
// do some stuff here
});
}
});
```
### 覆盖页面
覆盖页面是定制浏览器的一种很好的方式。你也可以替代Chrome中的一些默认的页面。例如,您可以创建自己的历史页面。要做到这一点,需要添加以下代码片段:
```js
"chrome_url_overrides" : {
"<page to override>;": "custom.html"
}
```
`<page to override>`的值可以为:`bookmarks`,`history` 和 `newtab`。创建自己的新的标签页是很酷的。
# 一个扩展示例
为了总结这篇文章,我决定创建一个简单的示例,这样您就可以更好地理解整个所示图片。这个示例扩展使用了我上面所描述的大部分内容,只是为当前页面中的所有`div`设置了一个`#F00`的背景颜色。您可以[下载源代码](https://github.com/tutsplus/developing-google-chrome-extensions)。
## Manifest 文件
我们当然还是先从 manifest 文件开始:
```js
{
"name": "BrowserExtension",
"version": "0.0.1",
"manifest_version": 2,
"description" : "Description ...",
"icons": { "16": "icons/16x16.png", "48": "icons/48x48.png", "128": "icons/128x128.png" },
"omnibox": { "keyword" : "yeah" },
"browser_action": {
"default_icon": { "19": "icons/19x19.png", "38": "icons/38x38.png" },
"default_title": "That's the tool tip",
"default_popup": "browseraction/popup.html"
},
"background": {
"scripts": ["background.js"],
"persistent": false
},
"chrome_url_overrides" : {
"newtab": "newtab/newtab.html"
},
"content_scripts": [{
"matches": ["http://*/*", "https://*/*"],
"js": ["content.js"]
}],
"devtools_page": "devtools/devtools.html"
}
```
请记住,您可以将您的文件组织到文件夹中。另外,请注意该 `version` 属性。每次你想要把你的扩展上传到网络商店时,你都应该更新这个属性。
## Background 脚本
```js
// omnibox
chrome.omnibox.onInputChanged.addListener(function(text, suggest) {
suggest([
{content: "color-divs", description: "Make everything red"}
]);
});
chrome.omnibox.onInputEntered.addListener(function(text) {
if(text == "color-divs") colorDivs();
});
// listening for an event / one-time requests
// coming from the popup
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
switch(request.type) {
case "color-divs":
colorDivs();
break;
}
return true;
});
// listening for an event / long-lived connections
// coming from devtools
chrome.runtime.onConnect.addListener(function (port) {
port.onMessage.addListener(function (message) {
switch(port.name) {
case "color-divs-port":
colorDivs();
break;
}
});
});
// send a message to the content script
var colorDivs = function() {
chrome.tabs.getSelected(null, function(tab){
chrome.tabs.sendMessage(tab.id, {type: "colors-div", color: "#F00"});
// setting a badge
chrome.browserAction.setBadgeText({text: "red!"});
});
}
```
前几行从 omnibox 中获得用户的操作。在此之后,我设置了一个一次性的请求侦听器,它将接受来自 browser action 图标的消息。
接下来一个代码片段是与devtools选项卡进行的长时间连接(这并不是绝对必要的,我只是为了这篇教程才这么做的)。通过使用这些侦听器,我可以从用户那里获得输入,并将其发送到 content 脚本,其脚本可以访问DOM元素。这里的关键是首先选择我想要操作的选项卡,然后向它发送一条消息。最后,我在扩展图标上添加了一个 badge 。
## Browser Action
从我们的`popup.html`文件开始:
```html
// popup.html
<script type="text/javascript" src="popup.js"></script>
<div style="width:200px">
<button id="button">Color all the divs</button>
</div>
```
然后新建 `popup.js` 文件:
```js
// popup.js
window.onload = function() {
document.getElementById("button").onclick = function() {
chrome.runtime.sendMessage({
type: "color-divs"
});
}
}
```
弹出框包含一个按钮,一旦用户点击它,它就会向后台脚本发送一条消息。
## DeveloperTools
```js
window.onload = function() {
var port = chrome.runtime.connect({ name: "color-divs-port" });
document.getElementById("button").onclick = function() {
port.postMessage({ type: "color-divs"});
}
}
```
对于开发者工具,我们在这里做的几乎和在弹出窗口中做的一样,唯一的区别是我使用了一个长时间的连接。
## Content 脚本
```js
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
switch(message.type) {
case "colors-div":
var divs = document.querySelectorAll("div");
if(divs.length === 0) {
alert("There are no any divs in the page.");
} else {
for(var i=0; i<divs.length; i++) {
divs[i].style.backgroundColor = message.color;
}
}
break;
}
});
```
这个content 脚本监听消息,选择当前页面上的所有`div`,并更改它们的背景颜色。注意我将监听器连接到的对象,在 content 脚本中是[`chrome.runtime.onMessage`](https://chajian.baidu.com/developer/extensions/runtime.html#method-sendMessage)。
## 定制新的标签页(`New Tab`)
这个扩展所做的最后一件事是定制`新的`标签页。只需将 `newtab` 属性指向`newtab/newtab.html`文件,我们就可以轻松地做到这一点:
```js
"chrome_url_overrides" : {
"newtab": "newtab/newtab.html"
}
```
请记住,您不能创建默认的新选项卡页面的副本。这个特性的想法是添加一个完全不同的功能。以下是谷歌的说法:
> 不要试图模拟默认的新选项卡页面。创建一个稍微修改过的默认新选项卡页面的api——包括顶部页面、最近关闭的页面、提示、主题背景图像等等——还不存在。在做到这一点之前,你最好试着去做一些完全不同的事情。
# 调试
为谷歌 Chrome 写一个扩展并不总是一件容易的事,你可能会遇到一些问题。好处是,您仍然可以使用控制台输出变量,以帮助调试。你可以随意添加`console.log`到您的 background 或 content 脚本。然而,在开发人员工具的上下文中运行的脚本,这不会起作用,在这种情况下,您可能会考虑使用 `alert` 方法,因为它在任何地方都是有效的。
# 总结
在我看来,Chrome是最好的浏览器之一。Google的开发人员通过让我们有能力在HTML、CSS和JavaScript中创建扩展,使得创建扩展变得相对容易。
是的,虽然有一些棘手的部分,但是总的来说,我们还是能够产生有价值的插件。请记住,本文并没有涵盖与开发Chrome扩展相关的所有内容。还有其他一些有用的东西,比如上下文菜单、选项页面和通知。对于我没有涉及的主题,请参阅[文档](https://developer.chrome.com/extensions/)以获得更详细的信息。
# 参考
[Developing Google Chrome Extensions](https://code.tutsplus.com/tutorials/developing-google-chrome-extensions--net-33076)