原文請通路個人部落格:chrome拓展開發實戰:頁面腳本的攔截注入
目前公司産品的無線站點已經實作了業務平台元件化,所有業務元件的轉場都是通過路由來完成,而各個子產品是通過
requirejs
進行統一管理,在灰階測試時會通過grunt進行打包操作,雖然工程化的開發流程已經大大提升了效率,但是為了滿足不同平台在任意業務入口頁面一鍵注入業務腳本即可進行測試的實際需求,團隊嘗試通過chrome拓展來進行實作。由于我本人是第一次開發chrome拓展插件,是以開發的過程中遇到不少坑,某些功能的實作方式也未必是最好,但還是有很多難得的收獲。接下來就圍繞“攔截與注入”的功能點,詳細介紹一下開發過程。
首先來看一看開發完成後的元件界面:

拓展的主要功能點:
1,頁面腳本的嗅探
2,指定腳本的下載下傳
3,指定域名下腳本的自動攔截(加載時不執行)
4,普通方式直接向頁面中注入腳本
5,通過requirejs向頁面注入腳本
6,攔截指定域名下資源後彈出通知視窗
在正式開始開發上述功能點之前,還是有必要先對chrome拓展的相關概念進行介紹。
關于chrome拓展
chrome拓展可以大大的擴充你的浏覽器的功能。包括但不僅限于這些功能:捕捉特定網頁的内容,捕捉HTTP封包,捕捉使用者浏覽動作,改變浏覽器位址欄/起始頁/書簽/Tab等界面元素的行為,與别的站點通信,修改網頁内容……不過,浏覽器插件也有一定的弊端,那就是會帶來一些安全隐患,也可能讓你的浏覽器變得緩慢甚至不穩定。
開始開發chrome拓展的時候,你幾乎不需要準備任何東西,隻需要一個編輯器,然後準備好API文檔随時查閱即可。關于如何開始一個chrome拓展,官方有一篇文章介紹,文章不是特别長,但足夠你了解一個chrome拓展是如何産生的。官方的DEMO中一共有4個檔案:
manifest.json - 所有插件都要有這個檔案,這是插件的配置檔案,可看作插件的“入口”。
icon.png - 拓展的小圖示,推薦使用19*19的半透明png圖檔,也就是上圖中拓展的入口小圖示。
popup.html - 就是你看到的打開拓展後的界面。
popup.js - 拓展界面引用的js檔案。
manifest.json
作為配置檔案,在拓展中是核心檔案,内容也非常顯而易見:
{
"manifest_version": 2,
"name": "One-click Kittens",
"description": "This extension demonstrates a browser action with kittens.",
"version": "1.0",
"permissions": [
"https://secure.flickr.com/"
],
"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html"
}
}
manifest_version:現在應該總是2。
permissions:很重要的東西,即允許插件做哪些事情,通路哪些站點,假如一個插件的"permissions"裡寫有“http://*.hacker.com/”,那麼這個插件就被允許訪hacker.com上的所有内容,包括可能會把你的一些個人資訊送出給hacker.com,危險性不言而喻,檢視一個插件能通路那些站點的方法是:在chrome的位址欄裡輸入“chrome://extensions/”,然後點對應插件的旁邊的那個“權限”。
default_popup:用來指定點選小圖示後彈出的小視窗中預設顯示的是哪個html,這個彈出的小視窗就叫做“popup”。
browser_action:這是一個浏覽器級的動作,也就是說,不管你現在在通路哪個頁面,那個小按鈕總是顯示出來,而我們的插件如果僅僅是針對某些頁面的話,就不适合用這個"browser_action"了,而要用"page_action",如:
{
"manifest_version": 2,
"name": "ihorve.com viewer",
"version": "0.0.1",
"background": { "scripts": ["background.js"] },
"permissions": ["tabs"],
"page_action": {
"default_icon": {
"19": "ihorve_19.png",
"38": "ihorve_38.png"
},
"default_title": "ihorve.com article information",
"default_popup": "popup.html"
}
}
Page Actions與Browser Actions非常類似,除了Page Actions沒有badge外,其他Browser Actions所有的方法Page Actions都有。另外的差別就是,Page Actions并不像Browser Actions那樣一直顯示圖示,而是可以在特定标簽特定情況下顯示或隐藏,是以它還具有獨有的
show
和
hide
方法。
chrome.pageAction.show(integer tabId);
chrome.pageAction.hide(integer tabId);
tabId
為标簽id,可以通過tabs接口擷取,有關tab相關的内容将在後面進行講解。
在page_action中,“
permissions
”屬性裡的“
tabs
”是必須的,否則下面的js不能擷取到tab裡的
url
,而這個url是我們判斷是否要把小圖示show出來的依據。這樣,拓展小圖示隻會在指定url被打開時出現在位址欄裡。
關于拓展的組成檔案,可以參考360翻譯成中文的官方文檔,很好了解,這裡不再贅述,有些不好了解的就是拓展中消息的傳遞,也就是如何通過拓展界面與頁面進行通信,在涉及到的地方我會進行詳細說明。接下來我們就圍繞相關的功能點介紹對應的API及實作過程。我的拓展包中的主要檔案如下:
manifest.json - 同上
icon.png - 拓展的小圖示
popup.html - 拓展界面html
popup.js - 拓展界面引用的js檔案
returnjs.js - 攔截頁面腳本時,阻止頁面腳本執行的注入腳本
sendlink.js - 嗅探頁面腳本時的注入腳本
background.js - chrome拓展的主程式
在這裡先介紹一下
background.js
。
background
是什麼概念?這是一個很重要的東西,可以把它認為是chrome插件的主程式,了解這個很關鍵,一旦插件被啟用(有些插件對所有頁面都啟用,有些則隻對某些頁面啟用),chrome就給插件開辟了一個獨立的javascript運作環境(又稱作運作上下文),用來跑你指定的
background script
,在這個例子中,也就是
background.js
。在
background.js
中,可以指定插件要立即執行的任務,以及配置在哪些域名中要立即執行這些任務。
background.js
通過
manifest.json
檔案中的background配置項進行指定:
"background": {
"scripts": ["background.js"]
},
頁面腳本的嗅探
嗅探頁面腳本的流程大概是:
1,擷取目前打開的标簽
2,向目前标簽注入腳本sendlink.js(在目前标簽的頁面中執行,收集頁面外鍊腳本并向拓展發送擷取到的腳本清單)
3,拓展中監聽目前頁面發送的腳本清單并展現
上述流程都在
popup.js
檔案中實作。首先來看如何擷取目前打開的标簽,以及如何向目前标簽注入一個sendlink.js檔案。
chrome.windows.getCurrent(function( currentWindow ) {
//擷取有指定屬性的标簽,為空擷取全部标簽
chrome.tabs.query( {
active: true, windowId: currentWindow.id
}, function(activeTabs) {
console.log("TabId:" + activeTabs[0].id);
//執行注入,第一個參數表示向哪個标簽注入
//第二個參數是一個option對象,file表示要注入的檔案,也可以是code
//是code時,對應的值為要執行的js腳本内容,如:code: "alert(1);"
//allFrames表示如果頁面中有iframe,是否也向iframe中注入腳本
chrome.tabs.executeScript(activeTabs[0].id, {
file: "sendlink.js",
allFrames: false
});
});
});
擷取目前打開标簽和向标簽中注入腳本檔案的操作都已經完成,現在我們來看一看
sendlink.js
檔案中的具體内容:
var links = document.getElementsByTagName("script"),
arr = [];
[].forEach.call(links, function(el) {
var href = el.src;
if(/[http|https]:\/\//gi.test(href)){
arr.push(href);
}
});
arr.sort();
//向拓展發送消息,這裡就涉及到了消息通訊
chrome.extension.sendMessage(arr);
上述代碼中出現了消息通訊,如果你僅僅需要給你自己的擴充的另外一部分發送一個消息(可選的是否得到答複),你可以簡單地使用
chrome.extension.sendMessage()
方法。這個方法可以幫助你從目前的标簽頁面到擴充傳送一次JSON序列化消息。
而在拓展中,可以使用
chrome.extension.onMessage()
方法進行監聽,并且在回調中處理監聽到的消息内容。詳情請查閱360翻譯的中文文檔。文檔中的
chrome.extension.sendRequest()
chrome.extension.sendRequest()
已經被更新的
onMessage
sendMessage
代替。下面就來看一看在popup.js中如何監聽消息。
chrome.extension.onMessage.addListener(function(links) {
//處理接收到的links,展現在拓展頁面中的DOM裡
});
這樣就完成了一次從拓展向目前标簽頁注入腳本,在注入的腳本中收集
script
外鍊腳本,并且将腳本清單通過消息發送給拓展,然後在拓展中接收并處理消息的過程。
指定腳本的下載下傳
下載下傳功能就相對簡單,使用chrome拓展的
downloads API
即可。因為下載下傳功能是在拓展中實作的,是以js腳本應該寫在
popup.js
檔案中。此外,下載下傳功能需要在
manifest.json
檔案中配置
permissions
,增加
downloads
權限:
"permissions": ["downloads"],
執行下載下傳連結的邏輯。應該在按鈕的點選事件後執行。
//下載下傳所選連結
downloadLinks: function() {
for(var i = 0, n = MainLogic.visibleLinks.length; i < n; i++) {
if (MainLogic.$id("cb" + i).checked){
//chrome拓展的下載下傳API
chrome.downloads.download({url: MainLogic.visibleLinks[i]});
}
}
window.close();
}
指定域名下腳本的自動攔截
資源攔截的功能需要為
manfest.json中
的
permissions
字段配置
webRequest
webRequestBlocking
權限。而進行資源攔截的原理也很容易從這兩個詞的意思上看出來:在web發送請求的時候執行操作。其實
webRequest
的核心意思就是要僞造各種
request
,那麼就不單單是寫某個對象的資料這麼簡單,還需要選擇合适的時機,在發送某種request之前僞造好它,或者在真實的request到來之後半路截獲它,替換成假的然後再發出去。
"permissions": [
"webRequest",
"webRequestBlocking"
],
Chrome提供了較為完整的方法供擴充程式分析、阻斷及更改網絡請求,同時也提供了一系列較為全面的監聽事件以監聽整個網絡請求生命周期的各個階段。網絡請求的整個生命周期所觸發事件的時間順序如下圖所示。
因為我們需要在指定的域名的資源開始發送請求的時候就進行攔截,是以不能等到拓展打開的時候才去執行攔截操作,必須在頁面一打開就進行攔截的部署,是以攔截的邏輯應該放在
background.js
中,而非
popup.js
中。
// 監聽發送請求
chrome.webRequest.onBeforeRequest.addListener(
function(details) {
console.log(details);
//攔截到執行資源後,為資源進行重定向
//也就是是隻要請求的資源比對攔截規則,就轉而執行returnjs.js
return {redirectUrl: chrome.extension.getURL("returnjs.js")};
},
{
//配置攔截比對的url,數組裡域名下的資源都将被攔截
urls: [
"*://*.jquery.top/*",
"*://*.elongstatic.com/*"
],
//攔截的資源類型,在這裡隻攔截script腳本,也可以攔截image等其他靜态資源
types: ["script"]
},
//要執行的操作,這裡配置為阻斷
["blocking"]
);
在這裡,攔截資源我們用到了一個監聽事件:
chrome.webRequest.onBeforeRequest.addListener()
,隻要有比對域名下的資源将要發送請求,就立即執行回調:
chrome.webRequest.onBeforeRequest.addListener(
callback, filter, opt_extraInfoSpec
);
回調函數所接收到的資訊對象均包括如下屬性:
requestId
、
url
method
frameId
parentFrameId
tabId
type
timeStamp
。其中
type
可能的值包括
main_frame
sub_frame
stylesheet
script
image
object
xmlhttprequest
other
攔截指定域名下資源後彈出通知視窗
在攔截到指定資源後,比較好的體驗是告訴使用者頁面資源已被攔截,這樣就可以使用chrome的通知接口向使用者發出通知。
chrome.notifications.create()
可以幫我們做到向使用者發出浏覽器通知。
// 彈出通知
chrome.notifications.clear("newNotice", function( wasClear ) {
chrome.notifications.create("newNotice", {
type: "basic",
iconUrl: chrome.runtime.getURL("images/logo.png"),
title: "頁面JS攔截提醒",
message: "拓展将開啟頁面JS攔截,若要恢複js執行請關閉拓展。"
}, function( notificationId ) {
console.log(notificationId);
} );
});
chrome通知的API介紹,請閱讀這篇文章:Chrome插件桌面通知API的變化。
普通方式注入js腳本
腳本的注入在前文已經介紹過,就是将指定的腳本資源在合适的時機放到頁面中執行。在這裡,我需要在拓展中輸入遠端腳本URL,在點選注入按鈕後向頁面注入,基本邏輯也很簡單,就是通過
ajax
發送請求,在
responseText
傳回時,将傳回的腳本作為
code
注入到頁面裡。
//擷取遠端腳本并進行普通注入
getScript: function() {
MainLogic.setInjectUrl();
var url = MainLogic.injecturl;
if( url ) {
$("#injectValue").removeClass("errbox");
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
var code = xhr.responseText;
console.log(code);
//第一個參數為null時,表示注入的目标是目前打開的tab
//擷取到傳回值時,通過code注入到頁面中,在回調中列印注入成功的提示
chrome.tabs.executeScript(null, {code: code, allFrames: false}, function(){
console.log("executeScript success!!!!!!!!!");
});
} else {
$('#xhr-errbox').show();
setTimeout(function() {
$('#xhr-errbox').hide();
}, 2000);
}
}
}
var ts = new Date().getTime();
var u;
if(url.indexOf('?') === -1){
u = url + '?_t=' + ts;
} else {
u = url + '&_t=' + ts;
}
xhr.open('GET',u,true);
xhr.send(null);
} else {
$("#injectValue").addClass("errbox");
MainLogic.msgBox("遠端腳本不能為空!");
}
},
上述提到的注入方式中,注入時機是響應操作後進行注入,還有一種方式是通過内容腳本的方式如,也就是
content script
。這種方式需要在
manifest.json
中進行配置,即在拓展有通路權限的頁面打開時立即向頁面注入資源。如:
"content_scripts": [
{
"matches": ["http://*/*"],//比對url
"js": ["jquery-1.9.1.js"],//向比對url中注入指定腳本
"css": ["css.css"],//向比對url中注入css樣式
"run_at": "document_end"//注入時機,這裡是在document節點加載完成時注入
}
],
具體的配置可參見360翻譯的中文API文檔。
通過requirejs向頁面注入腳本
通過requirejs向頁面注入腳本比普通方式稍有特殊,因為
requirejs
的執行需要在頁面中引入
require.js
,并在
data-main
屬性中配置入口腳本,是以使用普通方式注入顯然不符合實際,這裡的解決方案就是,在
domready
後向頁面通過
document.write
的方式注入腳本。
// 執行注入requirejs
injectRequire: function() {
MainLogic.setInjectUrl();
//require.js打在拓展包中,通過chrome.extension.getURL來擷取資源路徑
var requireurl = chrome.extension.getURL("require.js");
var datamainjs = MainLogic.injecturl;
if( datamainjs ) {
var executeCode = '' +
'var scripts = document.getElementsByTagName("script");' +
'[].forEach.call(scripts, function(script) {' +
' if(!!script.src && script.src == "' + requireurl + '"){' +
' script.parentNode.removeChild(script);' +
' }' +
'});' +
'var Req_script = document.createElement("script");' +
'Req_script.src = "' + requireurl + '";' +
'Req_script.setAttribute("data-main","' + datamainjs + '");' +
'document.body.appendChild(Req_script);';
chrome.tabs.executeScript(null, {
code: executeCode
});
MainLogic.msgBox("已成功注入!");
} else {
$("#injectValue").addClass("errbox");
MainLogic.msgBox("遠端腳本不能為空!");
}
},
從那個面的代碼中可以看出,首先需要将拓展包内的資源路徑取出,然後将要注入的腳本内容拼接成字元串,最後進行執行。這裡還有一個問題,就是通過
chrome.extension.getURL
來擷取包内資源的路徑。在擷取路徑的時候,需要通過
manifest.json
檔案中的的
web_accessible_resources
屬性為資源配置通路權限。
"web_accessible_resources": [
"require.js",
"returnjs.js",
"images/*" //images目錄下的所有資源,拓展都将有權通路
]
測試你的chrome拓展
因為在正式上線到chrome拓展托管平台需要将拓展包打包成.crx格式的檔案,是以我們剛才所做的一切都隻是開發版,那開發版如何測試呢?其實非常簡單,你隻需要在Chrome浏覽器中打開
chrome://extensions/
,點選“加載已解壓的拓展程式”,選中你的拓展開發目錄,拓展小圖示就出來了。當你拓展的代碼有更改時,記得點一下“重新加載”按鈕,重新加載你的拓展程式,以保證你能看到的拓展是最新的版本。
裡面的“權限”就是你在
manifest.json
檔案的
permissions
中配置的
url
到這裡,開發流程和功能點相關的API都已介紹完畢,整體來說開發一個chrome拓展并不複雜,隻要找到對應的API,然後理清
background.js
和拓展頁面js以及要注入到标簽頁面中的js之間的邏輯關系,并且知道如何通過監聽事件互相發送和接受消息,一個滿足你不同需求的chrome拓展就很容易開發出來。因部落客也是第一次接觸chrome拓展開發,如果在文章中有地方描述有誤,歡迎在評論中指出。也希望本文的分享能為大家帶來一些解決問題的思路。
項目源碼已經開放到github:點選這裡,歡迎各種fork star~
外部API資源文檔
360極速浏覽器開放平台(chrome官方API的中文版本,但不是最新): http://open.chrome.360.cn/extension_dev/overview.html
chrome插件中文開發文檔(非官方,與官方文檔一緻,不用FQ): http://chrome.liuyixi.com/overview.html
Chrome擴充及應用開發(電子書): http://www.ituring.com.cn/book/1472