[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...**按鈕。然后選擇存放你開發擴展文件的工作目錄。

記住:每次修改了代碼要重新加載擴展。
# Chrome 擴展架構
如下圖示,一個Chrome 擴展的架構:

## 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內容的彈出窗口。

`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地址進行交互。

有趣的是,您的圖標最初是隱藏的,因此您可以決定何時顯示它。例如,在上面的圖片中,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`。然后你會看到這樣:

按下 tab 鍵會產生以下屏幕:

當然是使用了 `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)