原文請訪問個人博客: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插件中文開發文檔(非官方,與官方文檔一致,不用翻牆): http://chrome.liuyixi.com/overview.html
Chrome擴展及應用開發(電子書): http://www.ituring.com.cn/book/1472
原文請訪問個人博客:chrome拓展開發實戰:頁面腳本的攔截注入