摘要:關(guān)于我的博客掘金專欄路易斯專欄原文鏈接擴(kuò)展開發(fā)定制請(qǐng)求響應(yīng)頭域本文共字,閱讀需分鐘。那么,我會(huì)放棄嗎反向代理顯然不會(huì),既然問題出在上,我去掉就行了。然而無(wú)論多少次的學(xué)習(xí)和模仿,最終的目的還是為了使用,故開發(fā)一款定制請(qǐng)求的勢(shì)在必行。
本文首發(fā)于《程序員》雜志2017年第9、10、11期,下面的版本又經(jīng)過(guò)進(jìn)一步的修訂。
關(guān)于Github:IHeader
我的博客:louis blog
掘金專欄:路易斯專欄
原文鏈接:【Chrome擴(kuò)展開發(fā)】定制HTTP請(qǐng)求響應(yīng)頭域
本文共15k字,閱讀需15分鐘。
導(dǎo)讀搜索是程序員的靈魂,為了提升搜索的效率,以便更快的查詢信息,我試著同時(shí)搜索4個(gè)網(wǎng)站,分別是百度、Google、維基、Bing。一個(gè)可行的做法就是網(wǎng)頁(yè)中嵌入4個(gè)iframe,通過(guò)js拼接前面4個(gè)搜索引擎的Search URL并依次在iframe中加載。這個(gè)構(gòu)思絲毫沒有問題,簡(jiǎn)單粗暴。然而就是這么簡(jiǎn)單的功能,也無(wú)法實(shí)現(xiàn)。由于Google網(wǎng)站在HTML的response header中添加了X-Frame-Options字段以防止網(wǎng)頁(yè)被Frame(這項(xiàng)設(shè)置常被用來(lái)防止Click Cheats),因此我無(wú)法將Google Search加入到iframe中來(lái)。那么,我會(huì)放棄Google嗎?
Nginx反向代理Google顯然不會(huì),既然問題出在X-Frame-Options上,我去掉就行了。對(duì)于請(qǐng)求或響應(yīng)頭域定制,nginx是個(gè)不錯(cuò)的選擇,其第三方的ngx_headers_more模塊就特別擅長(zhǎng)這種處理。由于nginx無(wú)法動(dòng)態(tài)加載第三方模塊,我動(dòng)態(tài)編譯了nginx以便加入ngx_headers_more模塊。至此,第一步完成,以下是nginx的部分配置。
location / { more_clear_headers "X-Frame-Options"; }
為了讓www.google.com正常訪問,我需要使用另外一個(gè)域名比如louis.google.com。通過(guò)nginx,讓louis.google.com轉(zhuǎn)發(fā)到www.google.com,轉(zhuǎn)發(fā)的同時(shí)去掉響應(yīng)頭域中的X-Frame-Options字段。于是nginx配置看起來(lái)像這樣:
server { listen 80; server_name louis.google.com; location / { proxy_pass https://www.google.com/; more_clear_headers "X-Frame-Options"; } }
以上的配置有什么問題嗎?且不說(shuō)http直接轉(zhuǎn)https的問題,即使能轉(zhuǎn)發(fā),實(shí)際上由于Google的安全策略限制,我們也訪問不了Google首頁(yè)!
最終我使用了一個(gè)Nginx Google代理模塊ngx_http_google_filter_module),nginx配置如下:
server { listen 80; server_name louis.google.com; resolver 192.168.1.1; # 需要設(shè)置為當(dāng)前路由的網(wǎng)關(guān) location / { google on; google_robots_allow on; more_clear_headers "X-Frame-Options"; } }
以上,通過(guò)實(shí)現(xiàn)一個(gè)Google網(wǎng)站的反向代理,代理的同時(shí)去掉了響應(yīng)頭域中的X-Frame-Options字段。至此,nginx方案完結(jié)。
nginx方案有一個(gè)明顯的缺陷是,配置中resolver對(duì)應(yīng)的網(wǎng)關(guān)IP192.168.1.1是隨著路由器的改變而改變的,家里和公司就是兩個(gè)不同的網(wǎng)關(guān)(更別說(shuō)去星巴克了辦公了),因此經(jīng)常需要手動(dòng)去修改網(wǎng)關(guān)然后重啟nginx。
IHeader緣起nginx方案的這個(gè)缺陷多少有些麻煩,恰好Chrome Extension可以定制headers,為了解決這個(gè)問題,我便嘗試開發(fā)Chrome Extension。(使用Chrome以來(lái),我下載試用過(guò)無(wú)數(shù)的Chrome Extension。每每看到一款優(yōu)秀的Extension,都要激動(dòng)好久,總有一種相見恨晚的感覺。Extension以其強(qiáng)大的定制能力,神奇的運(yùn)行機(jī)制征服了無(wú)數(shù)的開發(fā)者,我也不例外。然而無(wú)論多少次的學(xué)習(xí)和模仿,最終的目的還是為了使用,故開發(fā)一款定制請(qǐng)求的Extension勢(shì)在必行。)由于Chrome瀏覽器與網(wǎng)頁(yè)的天然聯(lián)系,使用Chrome Extension的方式去掉響應(yīng)頭域字段,比其它方案要更加簡(jiǎn)單高效。
要知道,Chrome Extension提供的API中有chrome.webRequest.onHeadersReceived。它能夠添加對(duì)響應(yīng)頭的監(jiān)聽并同步修改響應(yīng)頭域,去掉X-Frame-Options似乎是小case。
于是新建項(xiàng)目,取名IHeader。目錄結(jié)構(gòu)如下:
其中,_locales是國(guó)際化配置,目前IHeader支持中文和英文兩種語(yǔ)言。
res是資源目錄,index.html是extension的首頁(yè),options.html是選項(xiàng)頁(yè)面。
manifest.json是extension的聲明配置(總?cè)肟冢?,在這里配置extension的名稱、版本號(hào)、圖標(biāo)、快捷鍵、資源路徑以及權(quán)限等。
manifest.json貼出來(lái)如下:
{ "name": "IHeader", // 擴(kuò)展名稱 "version": "1.1.0", // 擴(kuò)展版本號(hào) "icons": { // 上傳到chrome webstore需要32px、64px、128px邊長(zhǎng)的方形圖標(biāo) "128": "res/images/lightning_green128.png", "32": "res/images/lightning_green.png", "64": "res/images/lightning_green64.png" }, "page_action": { // 擴(kuò)展的一種類型,說(shuō)明這是頁(yè)面級(jí)的擴(kuò)展 "default_title": "IHeader", // 默認(rèn)名稱 "default_icon": "res/images/lightning_default.png", // 默認(rèn)圖標(biāo) "default_popup": "res/index.html" // 點(diǎn)擊時(shí)彈出的頁(yè)面路徑 }, "background": { // 擴(kuò)展在后臺(tái)運(yùn)行的腳本 "persistent": true, // 由于后臺(tái)腳本需要持續(xù)運(yùn)行,需要設(shè)置為true,反之?dāng)U展不活動(dòng)時(shí)可能被瀏覽器關(guān)閉 "scripts": ["res/js/message.js", "res/js/background.js"] // 指定運(yùn)行的腳本,實(shí)際上Chrome會(huì)啟用一個(gè)匿名的html去引用這些js腳本。等同于"pages":["background.html"]這種方式(注意這兩種互斥,同時(shí)設(shè)置時(shí),后一種有效) }, "commands": { // 指定快捷鍵 "toggle_status": { // 快捷命令的名稱 "suggested_key": { // 快捷命令的熱鍵 "default": "Alt+H", "windows": "Alt+H", "mac": "Alt+H", "chromeos": "Alt+H", "linux": "Alt+H" }, "description": "Toggle IHeader" // 描述 } }, "content_scripts": [ // 隨著每個(gè)頁(yè)面加載的內(nèi)容腳本,通過(guò)它可以訪問到頁(yè)面的DOM { "all_frames": false, // frame中不加載 "matches": ["u003Call_urls>"], // 匹配所有URL "js": ["res/js/message.js", "res/js/content.js"] // 內(nèi)容腳本的路徑 } ], "default_locale": "en", // 默認(rèn)語(yǔ)言 "description": "__MSG_description__", // 擴(kuò)展描述 "manifest_version": 2, // Chrome 18及更高版本中,應(yīng)該指定為2,低于v18版本的Chrome瀏覽器可以指定為1或不指定 "minimum_chrome_version": "26.0", // 最低支持到v26版本,主要受制于chrome.runtime api "options_page": "res/options.html", // 選項(xiàng)頁(yè)面的路徑 "permissions": [ "tabs" , "webRequest", "webRequestBlocking", "http://*/*", "https://*/*", "contextMenus", "notifications"] // 擴(kuò)展需要的權(quán)限 }Chrome Extension簡(jiǎn)介
開始開發(fā)之前,我們先來(lái)刷一波基礎(chǔ)知識(shí)。
Chrome官方明確規(guī)定了插件、擴(kuò)展和應(yīng)用的區(qū)別:
插件(Plugin)是通過(guò)調(diào)用 Webkit 內(nèi)核 NPAPI 來(lái)擴(kuò)展內(nèi)核功能的一種組件,工作在內(nèi)核層面,理論上可以用任何一種生成本地二進(jìn)制程序的語(yǔ)言開發(fā),比如 C/C++、Java 等。插件重點(diǎn)在于接入瀏覽器底層,擁有更多的權(quán)限,可調(diào)用系統(tǒng)API,因此插件一般都不能跨系統(tǒng)。比如說(shuō)最近Adobe宣布放棄的Flash,下載資源的迅雷以及網(wǎng)上付款的網(wǎng)銀,它們都提供了Chrome插件,用以在特定場(chǎng)景啟用并運(yùn)行,從而實(shí)現(xiàn)豐富的功能。
擴(kuò)展(Extension)是通過(guò)調(diào)用 Chrome 提供的 Chrome API 來(lái)擴(kuò)展瀏覽器功能的一種組件,它完全基于Chrome瀏覽器,借助HTML,CSS,JS等web技術(shù)實(shí)現(xiàn)功能,是Chrome提供的一種可開發(fā)的擴(kuò)展技術(shù)。比如說(shuō)今年橫空出世的微信小程序,它就是微信提供的一種擴(kuò)展技術(shù)。相對(duì)于插件而言,擴(kuò)展程序擁有有限的權(quán)限和API,對(duì)底層系統(tǒng)不感知,從而具有良好的跨平臺(tái)特性。注意插件和擴(kuò)展都只有在Chrome啟動(dòng)后才會(huì)運(yùn)行。
應(yīng)用(Application)同樣是用于擴(kuò)充Chrome瀏覽器功能。它與擴(kuò)展的區(qū)別就在于,它擁有獨(dú)立運(yùn)行的用戶界面,并且Chrome未啟動(dòng)時(shí)也能獨(dú)立調(diào)用,就像一個(gè)獨(dú)立的App一樣。
不注意區(qū)分的話,我們講到Chrome插件,往往指的就是以上三者之一。為了避免引起誤解,本篇將嚴(yán)格區(qū)分概念,避免使用插件這種含糊的說(shuō)法。
如何安裝擴(kuò)展開發(fā)擴(kuò)展,首先得從安裝開始,從Chrome 21起,Chrome瀏覽器就增加了對(duì)擴(kuò)展安裝的限制,默認(rèn)只允許從 Chrome Web Store (Chrome 網(wǎng)上應(yīng)用店)安裝擴(kuò)展和應(yīng)用,這意味著用戶一般只能安裝Chrome Web Store內(nèi)的擴(kuò)展和應(yīng)用。
如果你拖動(dòng)一個(gè)crx安裝文件到Chrome瀏覽器的任何一個(gè)普通網(wǎng)頁(yè),將會(huì)出現(xiàn)如下提示。
點(diǎn)擊繼續(xù)按鈕,則會(huì)在瀏覽器左上角彈出如下警告。
如果你恰好在Github上發(fā)現(xiàn)一個(gè)不錯(cuò)的Chrome擴(kuò)展程序,而Chrome Web Store中沒有。是不是就沒有辦法安裝呢?當(dāng)然不是的,Chrome瀏覽器還有三種其它的方式可以加載擴(kuò)展程序。
如果是擴(kuò)展程序源碼目錄,點(diǎn)擊chrome://extensions/頁(yè)面的加載已解壓的擴(kuò)展程序按鈕就可以直接安裝。
如果是crx安裝文件,直接拖動(dòng)至chrome://extensions/頁(yè)面即可安裝。安裝過(guò)程如下所示:
1) 拖放安裝
? 2)點(diǎn)擊添加擴(kuò)展程序
? 3)添加好的擴(kuò)展如下所示。
啟動(dòng)Chrome時(shí)添加參數(shù)--enable-easy-off-store-extension-install ,用以開啟簡(jiǎn)單的擴(kuò)展安裝模式,然后就能像之前一樣隨意拖動(dòng)crx文件到瀏覽器頁(yè)面進(jìn)行安裝。
說(shuō)到安裝,自然有人會(huì)問,安裝了某款擴(kuò)展后,怎么查看該擴(kuò)展的源碼呢?Mac系統(tǒng)的用戶請(qǐng)記住這個(gè)目錄~/Library/Application Support/Google/Chrome/Default/Extensions/(windows的擴(kuò)展目錄暫無(wú))。
擴(kuò)展打包和更新另外,中間的打包擴(kuò)展程序按鈕用于將本地開發(fā)的擴(kuò)展程序打包成crx包,首次打包還會(huì)生成秘鑰文件(如IHeader.pem),如下所示。
打包好的擴(kuò)展程序,可以發(fā)送給其他人安裝,或發(fā)布到Chrome Web Store(開發(fā)者注冊(cè)費(fèi)用為5$)。
右邊的立即更新擴(kuò)展程序按鈕則用于更新擴(kuò)展。
擴(kuò)展的基本組成通常一個(gè)Chrome擴(kuò)展包含如下資源或目錄:
manifest.json入口配置文件(1個(gè),位于根目錄)
js文件(至少1個(gè),位于根目錄或子級(jí)目錄)
32px、64px、128px的方形icon各1個(gè)(位于根目錄或子級(jí)目錄)
_locales目錄, 用于提供國(guó)際化支持(可選,位于根目錄)
popup.html 彈出頁(yè)面(可選,位于根目錄或子級(jí)目錄)
background.html 后臺(tái)運(yùn)行的頁(yè)面,主要用于引入多個(gè)后臺(tái)運(yùn)行的js(可選,位于根目錄或子級(jí)目錄)
options.html 選項(xiàng)頁(yè)面,用于擴(kuò)展的設(shè)置(可選,位于根目錄或子級(jí)目錄)
為了方便管理,個(gè)人傾向于將HTML、JS、CSS,ICON等資源分類統(tǒng)一到同一個(gè)目錄。
擴(kuò)展的分類從使用場(chǎng)景上看,Chrome擴(kuò)展可分為以下三類:
1)Browser Action,瀏覽器擴(kuò)展,可通過(guò)manifest.json中的browser_action屬性設(shè)置,如下所示。
"browser_action": { "default_title": "Qrcode", "default_icon": "images/icon.png", "default_popup": "index.html" // 可選的 },
以上是URL生成二維碼的Browser Action擴(kuò)展,運(yùn)行如下所示:
該類擴(kuò)展特點(diǎn):全局?jǐn)U展,icon長(zhǎng)期占據(jù)瀏覽器右上角工具欄,每個(gè)頁(yè)面均可用。
2)Page Action,頁(yè)面級(jí)擴(kuò)展,可通過(guò)manifest.json中的page_action屬性設(shè)置,如下所示。
"page_action": { "default_title": "IHeader", "default_icon": "res/images/lightning_default.png", "default_popup": "res/index.html" // 可選的 },
以上是本篇將要講解的Page Action的擴(kuò)展——IHeader,它被指定為所有頁(yè)面可見,其icon狀態(tài)切換如下所示。
該類擴(kuò)展特點(diǎn):不同頁(yè)面可以擁有不同的狀態(tài)和不同的icon,icon在指定的頁(yè)面可見,可見時(shí)位于瀏覽器右上角工具欄。
由上可見,Browser Action與Page Action功能上非常相似,配置上各自的內(nèi)部屬性也完全一致,它們不僅可以配置點(diǎn)擊時(shí)彈出的頁(yè)面,同時(shí)還可以綁定點(diǎn)擊事件,如下所示。
// 以下事件綁定一般在background.js中運(yùn)行 // Browser Action chrome.browserAction.onClicked.addListener(function(tab) { console.log(tab.id, tab.url); chrome.tabs.executeScript(tab.id, {file: "content.js"}); }); // Page Action chrome.pageAction.onClicked.addListener(function(tab) { console.log(tab.id, tab.url); });
如果非要說(shuō)兩者的差別,開發(fā)中能夠感受到的就是:前者不需要維護(hù)icon狀態(tài),后者需要針對(duì)每個(gè)啟用的頁(yè)面管理不同的icon狀態(tài)。
3)Omnibox,全能工具條,可通過(guò)manifest.json中的omnibox屬性設(shè)置,如下所示。
"omnibox": { "keyword": "mdn-" //URL地址欄輸入關(guān)鍵字"mdn-"+空格后,就會(huì)觸發(fā)Omnibox },
以上是MDN網(wǎng)站快捷查詢的Omnibox擴(kuò)展,運(yùn)行如下所示:
很明顯,你可以對(duì)地址欄的各種輸入做定制,Chrome的URL地址欄只所以強(qiáng)大,omnibox可謂功不可沒。
該類擴(kuò)展特點(diǎn):運(yùn)行在URL地址欄,無(wú)彈出界面,用戶在輸入時(shí),擴(kuò)展就可以顯示建議或者自動(dòng)完成一些工作。
以上三類決定了擴(kuò)展如何在瀏覽器中運(yùn)行。除此之外,每個(gè)擴(kuò)展程序還可以任意搭載如下頁(yè)面或腳本。
Background Page,后臺(tái)頁(yè)面,可通過(guò)manifest.json中的background屬性設(shè)置,里面再細(xì)分script或page,分別表示腳本和頁(yè)面,如下所示。
"background": { "persistent": true, //默認(rèn)為false,指定為true時(shí)將在后臺(tái)持續(xù)運(yùn)行 "scripts": ["res/js/background.js"] // 指定后臺(tái)運(yùn)行的js // "page": ["res/background.html"] // 指定后臺(tái)運(yùn)行的html,html中需引入若干個(gè)js,沒有用戶界面,實(shí)際上就相當(dāng)于引入多個(gè)js腳本 },
Background Page在擴(kuò)展中之所以重要,主要?dú)w功于它可以使用所有的Chrome.* API。借助它popup.js 和 content.js 可以隨時(shí)進(jìn)行消息通信,并且調(diào)用它們?cè)緹o(wú)法調(diào)用的API。
根據(jù)persistent值是否為true,Background Page可分為兩類:① Persistent Background Pages,② Event Pages。前者持續(xù)運(yùn)行,隨時(shí)可訪問;后者只有在事件觸發(fā)時(shí)才能訪問。
該頁(yè)面特點(diǎn):運(yùn)行在瀏覽器后臺(tái),無(wú)用戶界面,后臺(tái)頁(yè)面可用于頁(yè)面間消息通信以及后臺(tái)監(jiān)控,一旦瀏覽器啟動(dòng),后臺(tái)頁(yè)面就會(huì)自動(dòng)運(yùn)行。
Content Script,內(nèi)容腳本,可通過(guò)manifest.json中的content_scripts屬性設(shè)置,如下所示。
"content_scripts": [ { "all_frames": true, // 默認(rèn)為false,指定為true意味著frame中也加載內(nèi)容腳本 "matches": ["u003Call_urls>"], // 匹配所有URL,意味著任何頁(yè)面都會(huì)加載 "js": ["res/js/content.js"], // 指定運(yùn)行的內(nèi)容腳本 "run_at": "document_end" // 頁(yè)面加載完成后執(zhí)行 } ],
除了配置之外,內(nèi)容腳本還可以通過(guò)js的方式動(dòng)態(tài)載入。
// 動(dòng)態(tài)載入js文件 chrome.tabs.executeScript(tabId, {file: "res/js/content.js"}); // 動(dòng)態(tài)載入js語(yǔ)句 chrome.tabs.executeScript(tabId, {code: "alert("Hello Extension!")"});
該腳本特點(diǎn):每個(gè)頁(yè)面在加載時(shí)都會(huì)加載內(nèi)容腳本,加載時(shí)機(jī)可以指定為document_start、idel或end(分別為頁(yè)面DOM加載開始時(shí),空閑時(shí)及完成后);內(nèi)容腳本是唯一可以訪問頁(yè)面DOM的腳本,通過(guò)它可以操作頁(yè)面的DOM節(jié)點(diǎn),從而影響視覺呈現(xiàn);基于安全考慮,內(nèi)容腳本被設(shè)計(jì)成與頁(yè)面其他的JS存在于兩個(gè)不同的沙盒,因此無(wú)法互相訪問各自的全局變量。
Option Html,設(shè)置頁(yè)面,可通過(guò)manifest.json中的options_page屬性設(shè)置,如下所示。
"options_page": "res/options.html",
該頁(yè)面特點(diǎn):點(diǎn)擊擴(kuò)展程序icon的右鍵菜單上【選項(xiàng)】按鈕進(jìn)入到設(shè)置頁(yè)面,該頁(yè)面一般用于擴(kuò)展的選項(xiàng)設(shè)置。
Override Html,替換新建標(biāo)簽頁(yè)的空白頁(yè)面,可通過(guò)manifest.json中的chrome_url_overrides屬性設(shè)置,如下所示。
"chrome_url_overrides":{ "newtab": "blank.html" },
該頁(yè)面特點(diǎn):常用于替換瀏覽器默認(rèn)的空白標(biāo)簽頁(yè)內(nèi)容,多見于新開標(biāo)簽頁(yè)時(shí)的壁紙程序,基于它你完全可以打造一個(gè)屬于自己的空白頁(yè)。
Devtool Page,開發(fā)者頁(yè)面,可通過(guò)manifest.json中的devtools_page屬性設(shè)置,如下所示。
"devtools_page": "debug.html",
該頁(yè)面特點(diǎn):隨著控制臺(tái)打開而啟動(dòng),可用于將擴(kuò)展收到的消息輸出到當(dāng)前控制臺(tái)。
總之,對(duì)于Chrome擴(kuò)展而言,Browser Action、Page Action 或 Omnibox之間是互斥的,其它情況下它并不限制你需要添加哪些頁(yè)面或腳本,只要你愿意,就可以隨意組合。
擴(kuò)展如何運(yùn)行調(diào)試只要你會(huì)寫js,就可以開發(fā)Chrome擴(kuò)展程序了。涉及到開發(fā),調(diào)試是不可避免的,Chrome擴(kuò)展的調(diào)試也非常簡(jiǎn)單。我們都知道Chrome瀏覽器的 chrome://extensions/頁(yè)面可以查看所有的Chrome擴(kuò)展,不僅如此,該頁(yè)面下的加載已解壓的擴(kuò)展程序按鈕,便可以直接加載本地開發(fā)的擴(kuò)展程序,如下所示。
注意:需要勾選開發(fā)者模式才會(huì)出現(xiàn)加載已解壓的擴(kuò)展程序按鈕。
成功加載后的擴(kuò)展跟正常安裝的擴(kuò)展程序,沒有什么不同,接下來(lái),我們就可以使用web技術(shù)進(jìn)行調(diào)試了。
點(diǎn)擊以上的選項(xiàng)或背景頁(yè)按鈕,將分別打開選項(xiàng)頁(yè)面和背景頁(yè)。選項(xiàng)頁(yè)面是一個(gè)正常的html頁(yè)面,按?+?+J 鍵打開控制臺(tái)就可以調(diào)試了。背景頁(yè)沒有界面,打開的就是控制臺(tái)。這兩個(gè)頁(yè)面都可以斷點(diǎn)debug。
Browser Action 或 Page Action的擴(kuò)展通常在Chrome瀏覽器的右上角會(huì)出現(xiàn)一個(gè)Icon,右鍵點(diǎn)擊該Icon,點(diǎn)擊右鍵菜單的審查彈出內(nèi)容按鈕,將會(huì)在打開彈出頁(yè)面的同時(shí)打開它的控制臺(tái)。這個(gè)控制臺(tái)也可以直接debug。
Chrome Extension APIChrome陸續(xù)向開發(fā)者開放了大量的API。使用這些API,我們可以監(jiān)聽或代理網(wǎng)絡(luò)請(qǐng)求,存儲(chǔ)數(shù)據(jù),管理標(biāo)簽頁(yè)和Cookie,綁定快捷鍵、設(shè)置右鍵菜單,添加通知和鬧鐘,獲取CPU、電池、內(nèi)存、顯示器的信息等等(還有很多沒有列舉出來(lái))。具體請(qǐng)閱讀Chrome API官方文檔。請(qǐng)注意,使用相應(yīng)的API,往往需要申請(qǐng)對(duì)應(yīng)的權(quán)限,如IHeader申請(qǐng)的權(quán)限如下所示。
"permissions": [ "tabs" , "webRequest", "webRequestBlocking", "http://*/*", "https://*/*", "contextMenus", "notifications"]
以上,IHeader依次申請(qǐng)了標(biāo)簽頁(yè)、請(qǐng)求、請(qǐng)求斷點(diǎn)、http網(wǎng)站,https網(wǎng)站,右鍵菜單,桌面通知的權(quán)限。
WebRequest APIChrome Extension API中,能夠修改請(qǐng)求的,只有chrome.webRequest了。webRequest能夠?yàn)檎?qǐng)求的不同階段添加事件監(jiān)聽器,這些事件監(jiān)聽器,可以收集請(qǐng)求的詳細(xì)信息,甚至修改或取消請(qǐng)求。
事件監(jiān)聽器只在特定階段觸發(fā),它們的觸發(fā)順序如下所示。(圖片來(lái)自MDN)
事件監(jiān)聽器的含義如下所示。
onBeforeRequest,請(qǐng)求發(fā)送之前觸發(fā)(請(qǐng)求的第1個(gè)事件,請(qǐng)求尚未創(chuàng)建,此時(shí)可以取消或者重定向請(qǐng)求)。
onBeforeSendHeaders,請(qǐng)求頭發(fā)送之前觸發(fā)(請(qǐng)求的第2個(gè)事件,此時(shí)可定制請(qǐng)求頭,部分緩存等有關(guān)的請(qǐng)求頭(Authorization、Cache-Control、Connection、Content-
Length、Host、If-Modified-Since、If-None-Match、If-Range、Partial-Data、Pragma、Proxy-
Authorization、Proxy-Connection和Transfer-Encoding)不出現(xiàn)在請(qǐng)求信息中,可以通過(guò)添加同名的key覆蓋修改其值,但是不能刪除)。
onSendHeaders,請(qǐng)求頭發(fā)送之前觸發(fā)(請(qǐng)求的第3個(gè)事件,此時(shí)只能查看請(qǐng)求信息,可以確認(rèn)onBeforeSendHeaders事件中都修改了哪些請(qǐng)求頭)。
onHeadersReceived,響應(yīng)頭收到之后觸發(fā)(請(qǐng)求的第4個(gè)事件,此時(shí)可定制響應(yīng)頭,且只能修改或刪除非緩存相關(guān)字段或添加字段,由于響應(yīng)頭允許多個(gè)同名字段同時(shí)存在,因此無(wú)法覆蓋修改緩存相關(guān)的字段)。
onResponseStarted,響應(yīng)內(nèi)容開始傳輸之后觸發(fā)(請(qǐng)求的第5個(gè)事件,此時(shí)只能查看響應(yīng)信息,可以確認(rèn)onHeadersReceived事件中都修改了哪些響應(yīng)頭)。
onCompleted,響應(yīng)接受完成后觸發(fā)(請(qǐng)求的第6個(gè)事件,此時(shí)只能查看響應(yīng)信息)。
onBeforeRedirect,onHeadersReceived事件之后,請(qǐng)求重定向之前觸發(fā)(此時(shí)只能查看響應(yīng)頭信息)。
onAuthRequired,onHeadersReceived事件之后,收到401或者407狀態(tài)碼時(shí)觸發(fā)(此時(shí)可以取消請(qǐng)求、同步提供憑證或異步提供憑證)。
以上,凡是能夠修改請(qǐng)求的事件監(jiān)聽器,都能夠指定其extraInfoSpec參數(shù)數(shù)組中包含"blocking"字符串(意味著能阻塞請(qǐng)求并修改),反之則不行。
另外請(qǐng)注意,Chrome對(duì)于請(qǐng)求頭和響應(yīng)頭的展示有著明確的規(guī)定,即控制臺(tái)中只展示發(fā)送出去或剛接收到的字段。因此編輯后的請(qǐng)求字段,控制臺(tái)的network欄能夠正常展示;而編輯后的響應(yīng)字段由于不屬于剛接收到的字段,所以從控制臺(tái)上就會(huì)看不到編輯的痕跡,如同沒修改過(guò)一樣,實(shí)際上編輯仍然有效。
事件監(jiān)聽器含義雖不同,但語(yǔ)法卻一致。接下來(lái)我們就以onHeadersReceived為例,進(jìn)行深入分析。
如何綁定header監(jiān)聽還記得我們的目標(biāo)嗎?想要去掉Google網(wǎng)站HTML響應(yīng)頭的X-Frame-Options字段。請(qǐng)看如下代碼:
// 監(jiān)聽的回調(diào) var callback = function(details) { var headers = details.responseHeaders; for (var i = 0; i < headers.length; ++i) { // 移除X-Frame-Options字段 if (headers[i].name === "X-Frame-Options") { headers.splice(i, 1); break; } } // 返回修改后的headers列表 return { responseHeaders: headers }; }; // 監(jiān)聽哪些內(nèi)容 var filter = { urls: [""] }; // 額外的信息規(guī)范,可選的 var extraInfoSpec = ["blocking", "responseHeaders"]; /* 監(jiān)聽response headers接收事件*/ chrome.webRequest.onHeadersReceived.addListener(callback, filter, extraInfoSpec);
chrome.webRequest.onHeadersReceived.addListener表示添加一個(gè)接收響應(yīng)頭的監(jiān)聽。以上代碼中的關(guān)鍵參數(shù)或?qū)傩裕旅嬷鹨恢v解。
callback,即事件觸發(fā)時(shí)的回調(diào),該回調(diào)默認(rèn)傳入一個(gè)參數(shù)(details),details就是請(qǐng)求的詳情。
filter,Object類型,限制事件回調(diào)callback觸發(fā)的過(guò)濾器。filter有四個(gè)屬性可以指定,分別為①urls(包含指定url的數(shù)組)、②types(請(qǐng)求的類型,共8種)、③tabId(標(biāo)簽頁(yè)id)、④windowId(窗口id)。
extraInfoSpec,數(shù)組類型,指的是額外的選項(xiàng)列表。對(duì)于headersReceived事件而言,包含"blocking",意味著要求請(qǐng)求同步,基于此才可以修改響應(yīng)頭;包含"responseHeaders"意味著事件回調(diào)的默認(rèn)參數(shù)details中將包含responseHeaders字段,該字段指向響應(yīng)頭列表。
既然有了添加監(jiān)聽的方法,自然,還會(huì)有移除監(jiān)聽的方法。
chrome.webRequest.onHeadersReceived.removeListener(listener);
除此之外,為了避免重復(fù)監(jiān)聽,還可以判斷監(jiān)聽是否已經(jīng)存在。
var bool = chrome.webRequest.onHeadersReceived.hasListener(listener);
為了保證更好的理清以上屬性、方法或參數(shù)的邏輯關(guān)系,請(qǐng)看如下腦圖:
擴(kuò)展?fàn)顟B(tài)管理 監(jiān)聽器的狀態(tài)管理知道了如何綁定監(jiān)聽器,僅僅是第一步。監(jiān)聽器需要在合適的時(shí)機(jī)綁定,也需要在合適的時(shí)機(jī)解綁。為了不影響Chrome的訪問速度,我們只在需要的標(biāo)簽頁(yè)創(chuàng)建新的監(jiān)聽器,因此監(jiān)聽器需要依賴filter來(lái)區(qū)分不同的tabId,考慮到用戶可能只需要監(jiān)聽一部分請(qǐng)求類型,types的區(qū)分也是不可避免的。又由于一個(gè)Tab里不同的時(shí)間段可能會(huì)加載不同的頁(yè)面,一個(gè)監(jiān)聽器在不同的頁(yè)面下正常運(yùn)行也是必須的(因此監(jiān)聽器的filter中不需要指定urls)。
寥寥數(shù)語(yǔ),可能不足以描述出監(jiān)聽器狀態(tài)管理的原貌,請(qǐng)看下圖進(jìn)一步幫助理解。
以上,一個(gè)請(qǐng)求將依次觸發(fā)上述①②③④⑤五個(gè)事件回調(diào),每個(gè)事件回調(diào)都對(duì)應(yīng)著一個(gè)監(jiān)聽器,這些監(jiān)聽器分為兩類(從顏色上也可看出端倪)。
②③⑤監(jiān)聽器的主要功能是記錄,用于監(jiān)聽頁(yè)面上每一個(gè)Request的請(qǐng)求頭和響應(yīng)頭,以及請(qǐng)求響應(yīng)時(shí)間。
①④監(jiān)聽器的主要功能是更新,用于增加、刪除或修改指定Request的請(qǐng)求頭和響應(yīng)頭。
若Chrome指定的標(biāo)簽頁(yè)激活了IHeader擴(kuò)展,②③⑤監(jiān)聽器就會(huì)記錄當(dāng)前標(biāo)簽頁(yè)后續(xù)的指定類型的請(qǐng)求信息。若用戶在激活了IHeader擴(kuò)展的標(biāo)簽頁(yè)更新了Request的請(qǐng)求頭或響應(yīng)頭,①或④監(jiān)聽器就會(huì)被開啟。不用擔(dān)心監(jiān)聽器開啟無(wú)限個(gè),我準(zhǔn)備了回收機(jī)制,單個(gè)標(biāo)簽頁(yè)的所有監(jiān)聽器都會(huì)在標(biāo)簽頁(yè)關(guān)閉或IHeader擴(kuò)展取消激活后釋放掉。
首先,為方便管理,先封裝下監(jiān)聽器的代碼。
/* 獨(dú)立的監(jiān)聽器 */ var Listener = (function(){ var webRequest = chrome.webRequest; function Listener(type, filter, extraInfoSpec, callback){ this.type = type; // 事件名稱 this.filter = filter; // 過(guò)濾器 this.extraInfoSpec = extraInfoSpec; // 額外的參數(shù) this.callback = callback; // 事件回調(diào) this.init(); } Listener.prototype.init = function(){ webRequest[this.type].addListener( // 添加一個(gè)監(jiān)聽器 this.callback, this.filter, this.extraInfoSpec ); return this; }; Listener.prototype.remove = function(){ webRequest[this.type].removeListener(this.callback); // 移除監(jiān)聽器 return this; }; Listener.prototype.reload = function(){ // 重啟監(jiān)聽器(用于選項(xiàng)頁(yè)面更新請(qǐng)求類型后重啟所有已開啟的監(jiān)聽器) this.remove().init(); return this; }; return Listener; })();
監(jiān)聽器封裝好了,剩下的便是管理,監(jiān)聽器控制器基于標(biāo)簽頁(yè)的維度統(tǒng)一管理標(biāo)簽頁(yè)上所有的監(jiān)聽器,代碼如下。
/* 監(jiān)聽器控制器 */ var ListenerControler = (function(){ var allListeners = {}; /* 所有的監(jiān)聽器控制器列表 */ function ListenerControler(tabId){ if(allListeners[tabId]){ /* 如有就返回已有的實(shí)例 */ return allListeners[tabId]; } if(!(this instanceof ListenerControler)){ /* 強(qiáng)制以構(gòu)造器方式調(diào)用 */ return new ListenerControler(tabId); } /* 初始化變量 */ var _this = this; var filter = getFilter(tabId); // 獲取當(dāng)前監(jiān)聽的filter設(shè)置 /* 捕獲requestHeaders */ var l1 = new Listener("onSendHeaders", filter, ["requestHeaders"], function(details){ _this.saveMesage("request", details); // 記錄請(qǐng)求的頭域信息 }); /* 捕獲responseHeaders */ var l2 = new Listener("onResponseStarted", filter, ["responseHeaders"], function(details){ _this.saveMesage("response", details); // 記錄響應(yīng)的頭域信息 }); /* 捕獲 Completed Details */ var l3 = new Listener("onCompleted", filter, ["responseHeaders"], function(details){ _this.saveMesage("complete", details); // 記錄請(qǐng)求完成時(shí)的時(shí)間等信息 }); allListeners[tabId] = this; // 記錄當(dāng)前的標(biāo)簽頁(yè)控制器 this.tabId = tabId; this.listeners = { // 記錄已開啟的監(jiān)聽器 "onSendHeaders": l1, "onResponseStarted": l2, "onCompleted": l3 }; this.messages = {}; // 當(dāng)前標(biāo)簽頁(yè)的請(qǐng)求信息集合 console.log("tabId=" + tabId + " listener on"); } ListenerControler.has = function(tabId){...} // 判斷是否包含指定標(biāo)簽頁(yè)的控制器 ListenerControler.get = function(tabId){...} // 返回指定標(biāo)簽頁(yè)的控制器 ListenerControler.getAll = function(){...} // 獲取所有的標(biāo)簽頁(yè)控制器 ListenerControler.remove = function(tabId){...} // 移除指定標(biāo)簽頁(yè)下的所有監(jiān)聽器 ListenerControler.prototype.remove = function(){...} // 移除當(dāng)前控制器中的所有監(jiān)聽器 ListenerControler.prototype.saveMesage = function(type, message){...} // 記錄請(qǐng)求信息 return ListenerControler; })();
通過(guò)監(jiān)聽器控制器的統(tǒng)一調(diào)度,標(biāo)簽頁(yè)中的多個(gè)監(jiān)聽器才能高效的工作。
實(shí)際上,還有很多工作,上述代碼還沒有體現(xiàn)出來(lái)。比方說(shuō)用戶在激活了IHeader擴(kuò)展的標(biāo)簽頁(yè)更新了Request的請(qǐng)求頭或響應(yīng)頭,①beforeSendHeaders或④headersReceived監(jiān)聽器又是怎么運(yùn)作的呢?這部分內(nèi)容,請(qǐng)結(jié)合『如何綁定header監(jiān)聽』節(jié)點(diǎn)的內(nèi)容理解。
Page Action圖標(biāo)狀態(tài)管理標(biāo)簽頁(yè)控制器的狀態(tài)需要由視覺體現(xiàn)出來(lái),因此Page Action圖標(biāo)的管理也是不可避免的。通常,默認(rèn)的icon可以在manifest.json中指定。
"page_action": { "default_icon": "res/images/lightning_default.png", // 默認(rèn)圖標(biāo) },
icon有如下3種狀態(tài)(后兩種狀態(tài)可以互相切換)。
默認(rèn)狀態(tài),展示默認(rèn)的icon。
初始狀態(tài),展示擴(kuò)展初始化后的icon。
激活狀態(tài),展示擴(kuò)展激活后的icon。
Chrome提供了chrome.pageAction的API供Page Action使用。目前chrome.pageAction擁有如下方法。
show,在指定的tab下展示Page Action。
hide,在指定的tab下隱藏Page Action。
setTitle,設(shè)置Page Action的標(biāo)題(鼠標(biāo)移動(dòng)到該P(yáng)age Action上時(shí)會(huì)出現(xiàn)設(shè)置好的標(biāo)題提示)
getTitle,獲取Page Action的標(biāo)題。
setIcon,設(shè)置Page Action的圖標(biāo)。
setPopup,設(shè)置點(diǎn)擊時(shí)彈出頁(yè)面的URL。
getPopup,獲取點(diǎn)擊時(shí)彈出頁(yè)面的URL。
以上,setTitle、setIcon 和 show方法比較常用。其中,show方法有兩種作用,①展示icon,②更新icon,因此一般是先設(shè)置好icon的標(biāo)題和路徑,然后調(diào)用show展示出來(lái)(或更新)。需要注意的是,Page Action在show方法被調(diào)用之前,是不會(huì)響應(yīng)點(diǎn)擊的,所以需要在初始化工作結(jié)束之前調(diào)用show方法。千言萬(wàn)語(yǔ)不如上代碼,如下。
/* 聲明3種icon狀態(tài) */ var UNINIT = 0, // 擴(kuò)展未初始化 INITED = 1, // 擴(kuò)展已初始化,但未激活 ACTIVE = 2; // 擴(kuò)展已激活 /* 處理擴(kuò)展icon狀態(tài) */ var PageActionIcon = (function(){ var pageAction = chrome.pageAction, icons = {}, tips = {}; icons[INITED] = "res/images/lightning_green.png"; // 設(shè)置不同狀態(tài)下的icon路徑(相對(duì)于擴(kuò)展根目錄) icons[ACTIVE] = "res/images/lightning_red.png"; tips[INITED] = Text("iconTips"); // 其它地方有處理,Text被指向chrome.i18n.getMessage,用以讀取_locales中指定語(yǔ)言的對(duì)應(yīng)字段的文本信息 tips[ACTIVE] = Text("iconHideTips"); function PageActionIcon(tabId){ // 構(gòu)造器 this.tabId = tabId; this.status = UNINIT; // 默認(rèn)為未初始化狀態(tài) pageAction.show(tabId); // 展示Page Action } PageActionIcon.prototype.init = function(){...} // 初始化icon PageActionIcon.prototype.active = function(){...} // icon切換為激活狀態(tài) PageActionIcon.prototype.hide = function(){...} // 隱藏icon PageActionIcon.prototype.setIcon = function(){ // 設(shè)置icon pageAction.setIcon({ // 設(shè)置icon的路徑 tabId : this.tabId, path : icons[this.status] }); pageAction.setTitle({ // 設(shè)置icon的標(biāo)題 tabId : this.tabId, title : tips[this.status] }); return this; }; PageActionIcon.prototype.restore = function(){// 刷新頁(yè)面后,icon之前的狀態(tài)會(huì)丟失,需要手動(dòng)恢復(fù) this.setIcon(); pageAction.show(this.tabId); return this; }; return PageActionIcon; })();
icon管理的準(zhǔn)備工作ok了,剩下的就是使用了,如下。
new PageActionIcon(this.tabId).init();標(biāo)簽頁(yè)的狀態(tài)管理
對(duì)于IHeader擴(kuò)展程序,一個(gè)標(biāo)簽頁(yè)同時(shí)包含了監(jiān)聽器狀態(tài)和icon狀態(tài)的變化。因此需要再抽象出一個(gè)標(biāo)簽頁(yè)控制器,對(duì)兩者進(jìn)行統(tǒng)一管理,從而供外部調(diào)用。代碼如下。
/* 處理標(biāo)簽頁(yè)狀態(tài) */ var TabControler = (function(){ var tabs = {}; // 所有的標(biāo)簽頁(yè)控制器列表 function TabControler(tabId, url){ if(tabs[tabId]){ /* 如有就返回已有的實(shí)例 */ return tabs[tabId]; } if(!(this instanceof TabControler)){ /* 強(qiáng)制以構(gòu)造器方式調(diào)用 */ return new TabControler(tabId); } /* 初始化屬性 */ tabs[tabId] = this; this.tabId = tabId; this.url = url; this.init(); } TabControler.get = function(tabId){...} // 獲取指定的標(biāo)簽頁(yè)控制器 TabControler.remove = function(tabId){ if(tabs[tabId]){ delete tabs[tabId]; // 移除指定的標(biāo)簽頁(yè)控制器 ListenerControler.remove(tabId); // 移除指定的監(jiān)聽器控制器 } }; TabControler.prototype.init = function(){...} // 初始化標(biāo)簽頁(yè)控制器 TabControler.prototype.switchActive = function(){ // 當(dāng)前標(biāo)簽頁(yè)狀態(tài)切換 var icon = this.icon; if(icon){ var status = icon.status; var tabId = this.tabId; switch(status){ case ACTIVE: // 如果是激活狀態(tài),則恢復(fù)初始狀態(tài),移除監(jiān)聽器控制器 icon.init(); ListenerControler.remove(tabId); Message.send(tabId, "ListeningCancel"); // 通知內(nèi)容腳本從而在控制臺(tái)輸出取消提示(后續(xù)將講到消息通信) break; default: // 如果不是激活狀態(tài),則激活之,添加監(jiān)聽器控制器 icon.active(); ListenerControler(tabId); Message.send(tabId, "Listening"); // 并通知內(nèi)容腳本從而在控制臺(tái)輸出監(jiān)聽提示 } } return this; }; TabControler.prototype.restore = function(){...} // 恢復(fù)標(biāo)簽頁(yè)控制器的狀態(tài)(針對(duì)頁(yè)面刷新場(chǎng)景) TabControler.prototype.remove = function(){...} // 移除標(biāo)簽頁(yè)控制器 return TabControler; })();
標(biāo)簽頁(yè)控制器的抽象,有助于封裝擴(kuò)展的內(nèi)部運(yùn)行細(xì)節(jié),方便了后續(xù)各種場(chǎng)景中對(duì)擴(kuò)展的管理 。
標(biāo)簽頁(yè)關(guān)閉或更新的妥善處理標(biāo)簽頁(yè)關(guān)閉或更新時(shí),為了避免內(nèi)存泄露和運(yùn)行穩(wěn)定,部分?jǐn)?shù)據(jù)需要釋放或者同步。剛剛封裝好的標(biāo)簽頁(yè)控制器就可以用來(lái)做這件事。
首先,Tab關(guān)閉時(shí)需要釋放當(dāng)前標(biāo)簽頁(yè)的控制器和監(jiān)聽器對(duì)象。
/* 監(jiān)聽tab關(guān)閉的事件 */ chrome.tabs.onRemoved.addListener(function(tabId, removeInfo){ TabControler.remove(tabId); // 釋放內(nèi)存,移除標(biāo)簽頁(yè)控制器和監(jiān)聽器 });
其次,每次Tab在執(zhí)行跳轉(zhuǎn)或刷新動(dòng)作時(shí),Page Action的icon都會(huì)回到初始狀態(tài)并且不可點(diǎn)擊,此時(shí)需要恢復(fù)icon之前的狀態(tài)。
/* 監(jiān)聽tab更新的事件、包含跳轉(zhuǎn)或刷新的動(dòng)作 */ chrome.tabs.onUpdated.addListener(function(tabId, changeInfo){ if(changeInfo.status === "loading"){ // 頁(yè)面處于loading時(shí)觸發(fā) TabControler(tabId).restore(); // 恢復(fù)icon狀態(tài) } });
以上,頁(yè)面跳轉(zhuǎn)或刷新時(shí),changeInfo將依次經(jīng)歷兩種狀態(tài):loading 和complete(部分頁(yè)面會(huì)包含favIconUrl或title信息),如下所示。
隨著狀態(tài)管理的逐漸完善,那么,是時(shí)候進(jìn)行消息通信了(不知道你注意到上述代碼中出現(xiàn)的Message對(duì)象沒有?它就是消息處理的對(duì)象)。
消息通信 擴(kuò)展內(nèi)部消息通信Chrome擴(kuò)展內(nèi)的各頁(yè)面之間的消息通信,有如下四種方式(以下接口省略chrome前綴)。
類型 | 消息發(fā)送 | 消息接收 | 支持版本 |
---|---|---|---|
一次性消息 | extension.sendRequest | extension.onRequest | v33起廢棄(早期方案) |
一次性消息 | extension.sendMessage | extension.onMessage | v20+(不建議使用) |
一次性消息 | runtime.sendMessage | runtime.onMessage | v26+(現(xiàn)在主流,推薦使用) |
長(zhǎng)期連接 | runtime.connect | runtime.onConnect | v26+ |
目前以上四種方案都可以使用。其中extension.sendRequest發(fā)送的消息,只有extension.onRequest才能接收到(已廢棄不建議使用,可選讀Issue 9965005)。extension.sendMessage 或 runtime.sendMessage 發(fā)送的消息,雖然extension.onMessage 和 runtime.onMessage都可以接收,但是runtime api的優(yōu)先觸發(fā)。若多個(gè)監(jiān)聽同時(shí)存在,只有第一個(gè)響應(yīng)才能觸發(fā)消息的sendResponse回調(diào),其他響應(yīng)將被忽略,如下所述。
If multiple pages are listening for onMessage events, only the first to call sendResponse() for a particular event will succeed in sending the response. All other responses to that event will be ignored.
我們先看一次性的消息通信,它的基本規(guī)律如下所示。
圖中出現(xiàn)了一種新的消息通信方式,即chrome.extension.getBackgroundPage,通過(guò)它能夠獲取background.js(后臺(tái)腳本)的window對(duì)象,從而調(diào)用window下的任意全局方法。嚴(yán)格來(lái)說(shuō)它不是消息通信,但是它完全能夠勝任消息通信的工作,之所以出現(xiàn)在圖示中,是因?yàn)樗攀窍膒opup.html到background.js的主流溝通方式。那么你可能會(huì)問了,為什么content.js中不具有同樣的API呢?
這是因?yàn)樗鼈兊氖褂梅绞讲煌髯缘臋?quán)限也不同。popup.html或background.js中chrome.extension對(duì)象打印如下:
content.js中chrome.extension對(duì)象打印如下:
可以看出,前者包含了全量的屬性,后者只保留少量的屬性。content.js中并沒有chrome.extension.getBackgroundPage方法,因此content.js不能直接調(diào)用background.js中的全局方法。
回到消息通信的話題,請(qǐng)看消息發(fā)送和監(jiān)聽的簡(jiǎn)單示例,如下所示:
// 消息流:彈窗頁(yè)面、選項(xiàng)頁(yè)面 或 background.js --> content.js // 由于每個(gè)tab都可能加載內(nèi)容腳本,因此需要指定tab chrome.tabs.query( // 查詢tab { active: true, currentWindow: true }, // 獲取當(dāng)前窗口激活的標(biāo)簽頁(yè),即當(dāng)前tab function(tabs) { // 獲取的列表是包含一個(gè)tab對(duì)象的數(shù)組 chrome.tabs.sendMessage( // 向tab發(fā)送消息 tabs[0].id, // 指定tab的id { message: "Hello content.js" }, // 消息內(nèi)容可以為任意對(duì)象 function(response) { // 收到響應(yīng)后的回調(diào) console.log(response); } ); } ); /* 消息流: * 1. 彈窗頁(yè)面或選項(xiàng)頁(yè)面 --> background.js * 2. background.js --> 彈窗頁(yè)面或選項(xiàng)頁(yè)面 * 3. content.js --> 彈窗頁(yè)面、選項(xiàng)頁(yè)面 或 background.js */ chrome.runtime.sendMessage({ message: "runtime-message" }, function(response) { console.log(response); }); // 可任意選用runtime或extension的onMessage方法監(jiān)聽消息 chrome.runtime.onMessage.addListener( // 添加消息監(jiān)聽 function(request, sender, sendResponse) { // 三個(gè)參數(shù)分別為①消息內(nèi)容,②消息發(fā)送者,③發(fā)送響應(yīng)的方法 console.log(sender.tab ? "from a content script:" + sender.tab.url : "from the extension"); if (request.message === "Hello content.js"){ sendResponse({ answer: "goodbye" }); // 發(fā)送響應(yīng)內(nèi)容 } // return true; // 如需異步調(diào)用sendResponse方法,需要顯式返回true } );
上述涉及到的API語(yǔ)法如下:
chrome.tabs.query(object queryInfo, function callback),查詢符合條件的tab。其中,callback為查詢結(jié)果的回調(diào),默認(rèn)傳入tabs列表作為參數(shù);queryInfo為標(biāo)簽頁(yè)的描述信息,包含如下屬性。
屬性 | 類型 | 支持性 | 描述 |
---|---|---|---|
active | boolean | tab是否激活 | |
audible | boolean | v45+ | tab是否允許聲音播放 |
autoDiscardable | boolean | v54+ | tab是否允許被丟棄 |
currentWindow | boolean | v19+ | tab是否在當(dāng)前窗口中 |
discarded | boolean | v54+ | tab是否處于被丟棄狀態(tài) |
highlighted | boolean | tab是否高亮 | |
index | Number | v18+ | tab在窗口中的序號(hào) |
muted | boolean | v45+ | tab是否靜音 |
lastFocusedWindow | boolean | v19+ | tab是否位于最后選中的窗口中 |
pinned | boolean | tab是否固定 | |
status | String | tab的狀態(tài),可選值為loading或complete | |
title | String | tab中頁(yè)面的標(biāo)題(需要申請(qǐng)tabs權(quán)限) | |
url | String or Array | tab中頁(yè)面的鏈接 | |
windowId | Number | tab所處窗口的id | |
windowType | String | tab所處窗口的類型,值包含normal、popup、panel、appordevtools |
注:丟棄的tab指的是tab內(nèi)容已經(jīng)從內(nèi)存中卸載,但是tab未關(guān)閉。
chrome.tabs.sendMessage(integer tabId, any request, object options, function responseCallback),向指定tab下的content.js發(fā)送單次消息。其中tabId為標(biāo)簽頁(yè)的id,request為消息內(nèi)容,options參數(shù)從v41版開始支持,通過(guò)它可以指定frameId的值,以便向指定的frame發(fā)送消息,responseCallback即收到響應(yīng)后的回調(diào)。
chrome.runtime.sendMessage(string extensionId, any message, object options, function responseCallback),向擴(kuò)展內(nèi)或指定的其他擴(kuò)展發(fā)送消息。其中extensionId為其他指定擴(kuò)展的id,擴(kuò)展內(nèi)通信可以忽略該參數(shù),message為消息內(nèi)容,options參數(shù)從v32版開始支持,通過(guò)它可以指定includeTlsChannelId(boolean)的值,以便決定TLS通道ID是否會(huì)傳遞到onMessageExternal事件監(jiān)聽回調(diào)中,responseCallback即收到響應(yīng)后的回調(diào)。
chrome.runtime.onMessage.addListener(function callback),添加單次消息通信的監(jiān)聽。其中callback類似function(any message, MessageSender sender, function sendResponse) {...}這種函數(shù),message為消息內(nèi)容,sender即消息發(fā)送者,sendResponse用于向消息發(fā)送者回復(fù)響應(yīng),如果需要異步發(fā)送響應(yīng),請(qǐng)?jiān)赾allback回調(diào)中return true(此時(shí)將保持消息通道不關(guān)閉直到sendResponse方法被調(diào)用)。
綜上,我們選用chrome.runtime api即可完美的進(jìn)行消息通信,對(duì)于v25,甚至v20以下的版本,請(qǐng)參考以下兼容代碼。
var callback = function(message, sender, sendResponse) { // Do something }); var message = { message: "hello" }; // message if (chrome.extension.sendMessage) { // chrome20+ var runtimeOrExtension = chrome.runtime && chrome.runtime.sendMessage ? "runtime" : "extension"; chrome[runtimeOrExtension].onMessage.addListener(callback); // bind event chrome[runtimeOrExtension].sendMessage(message); // send message } else { // chrome19- chrome.extension.onRequest.addListener(callback); // bind event chrome.extension.sendRequest(message); // send message }
想必,一次性的消息通信你已經(jīng)駕輕就熟了。如果是頻繁的通信呢?此時(shí),一次性的消息通信就顯得有些復(fù)雜。為了滿足這種頻繁通信的需要,Chrome瀏覽器專門提供了Chrome.runtime.connect API。基于它,通信的雙方就可以建立長(zhǎng)期的連接。
長(zhǎng)期連接基本規(guī)律如下所示:
以上,與上述一次性消息通信一樣,長(zhǎng)期連接也可以在popup.html、background.js 和 content.js三者中兩兩之間建立(注意:無(wú)論何時(shí)主動(dòng)與content.js建立連接,都需要指定tabId)。如下是popup.html與content.js之間建立長(zhǎng)期連接的舉例?。
// popup.html 發(fā)起長(zhǎng)期連接 chrome.tabs.query( {active: true, currentWindow: true}, // 獲取當(dāng)前窗口的激活tab function(tabs) { // 建立連接,如果是與background.js建立連接,應(yīng)該使用chrome.runtime.connect api var port = chrome.tabs.connect( // 返回Port對(duì)象 tabs[0].id, // 指定tabId {name: "call2content.js"} // 連接名稱 ); port.postMessage({ greeting: "Hello" }); // 發(fā)送消息 port.onMessage.addListener(function(msg) { // 監(jiān)聽消息 if (msg.say == "Hello, who"s there?") { port.postMessage({ say: "Louis" }); } else if (msg.say == "Oh, Louis, how"s it going?") { port.postMessage({ say: "It"s going well, thanks. How about you?" }); } else if (msg.say == "Not good, can you lend me five bucks?") { port.postMessage({ say: "What did you say? Inaudible? The signal was terrible" }); port.disconnect(); // 斷開長(zhǎng)期連接 } }); } ); // content.js 監(jiān)聽并響應(yīng)長(zhǎng)期連接 chrome.runtime.onConnect.addListener(function(port) { // 監(jiān)聽長(zhǎng)期連接,默認(rèn)傳入Port對(duì)象 console.assert(port.name == "call2content.js"); // 篩選連接名稱 console.group("Long-lived connection is established, sender:" + JSON.stringify(port.sender)); port.onMessage.addListener(function(msg) { var word; if (msg.greeting == "Hello") { word = "Hello, who"s there?"; port.postMessage({ say: word }); } else if (msg.say == "Louis") { word = "Oh, Louis, how"s it going?"; port.postMessage({ say: word }); } else if (msg.say == "It"s going well, thanks. How about you?") { word = "Not good, can you lend me five bucks?"; port.postMessage({ say: word }); } else if (msg.say == "What did you say? Inaudible? The signal was terrible") { word = "Don"t hang up!"; port.postMessage({ say: word }); } console.log(msg); console.log(word); }); port.onDisconnect.addListener(function(port) { // 監(jiān)聽長(zhǎng)期連接的斷開事件 console.groupEnd(); console.warn(port.name + ": The phone went dead"); }); });
控制臺(tái)輸出如下:
建立長(zhǎng)期連接涉及到的API語(yǔ)法如下:
chrome.tabs.connect(integer tabId, object connectInfo),與content.js建立長(zhǎng)期連接。tabId為標(biāo)簽頁(yè)的id,connectInfo為連接的配置信息,可以指定兩個(gè)屬性,分別為name和frameId。name屬性指定連接的名稱,frameId屬性指定tab中唯一的frame去建立連接。
chrome.runtime.connect(string extensionId, object connectInfo),發(fā)起長(zhǎng)期的連接。其中extensionId為擴(kuò)展的id,connectInfo為連接的配置信息,目前可以指定兩個(gè)屬性,分別是name和includeTlsChannelId。name屬性指定連接的名稱,includeTlsChannelId屬性從v32版本開始支持,表示TLS通道ID是否會(huì)傳遞到onConnectExternal的監(jiān)聽器中。
chrome.runtime.onConnect.addListener(function callback),監(jiān)聽長(zhǎng)期連接的建立。callback為連接建立后的事件回調(diào),該回調(diào)默認(rèn)傳入Port對(duì)象,通過(guò)Port對(duì)象可進(jìn)行頁(yè)面間的雙向通信。Port對(duì)象結(jié)構(gòu)如下:
屬性 | 類型 | 描述 |
---|---|---|
name | String | 連接的名稱 |
disconnect | Function | 立即斷開連接(已經(jīng)斷開的連接再次調(diào)用沒有效果,連接斷開后將不會(huì)收到新的消息) |
onDisconnect | Object | 斷開連接時(shí)觸發(fā)(可添加監(jiān)聽器) |
onMessage | Object | 收到消息時(shí)觸發(fā)(可添加監(jiān)聽器) |
postMessage | Function | 發(fā)送消息 |
sender | MessageSender | 連接的發(fā)起者(該屬性只會(huì)出現(xiàn)在連接監(jiān)聽器中,即onConnect 或onConnectExternal中) |
相對(duì)于擴(kuò)展內(nèi)部的消息通信而言,擴(kuò)展間的消息通信更加簡(jiǎn)單。對(duì)于一次性消息通信,共涉及到如下兩個(gè)API:
chrome.runtime.sendMessage,之前講過(guò),需要特別指定第一個(gè)參數(shù)extensionId,其它不變。
chrome.runtime.onMessageExternal,監(jiān)聽其它擴(kuò)展的消息,用法與chrome.runtime.onMessage一致。
對(duì)于長(zhǎng)期連接消息通信,共涉及到如下兩個(gè)API:
chrome.runtime.connect,之前講過(guò),需要特別指定第一個(gè)參數(shù)extensionId,其它不變。
chrome.runtime.onConnectExternal,監(jiān)聽其它擴(kuò)展的消息,用法與chrome.runtime.onConnect一致。
發(fā)送消息可參考如下代碼:
var extensionId = "oknhphbdjjokdjbgnlaikjmfpnhnoend"; // 目標(biāo)擴(kuò)展id // 發(fā)起一次性消息通信 chrome.runtime.sendMessage(extensionId, { message: "hello" }, function(response) { console.log(response); }); // 發(fā)起長(zhǎng)期連接消息通信 var port = chrome.runtime.connect(extensionId, {name: "web-page-messages"}); port.postMessage({ greeting: "Hello" }); port.onMessage.addListener(function(msg) { // 通信邏輯見『長(zhǎng)期連接消息通信』popup.html示例代碼 });
監(jiān)聽消息可參考如下代碼:
// 監(jiān)聽一次性消息 chrome.runtime.onMessageExternal.addListener( function(request, sender, sendResponse) { console.group("simple request arrived"); console.log(JSON.stringify(request)); console.log(JSON.stringify(sender)); sendResponse("bye"); }); // 監(jiān)聽長(zhǎng)期連接 chrome.runtime.onConnect.addListener(function(port) { console.assert(port.name == "web-page-messages"); console.group("Long-lived connection is established, sender:" + JSON.stringify(port.sender)); port.onMessage.addListener(function(msg) { // 通信邏輯見『長(zhǎng)期連接消息通信』content.js示例代碼 }); port.onDisconnect.addListener(function(port) { console.groupEnd(); console.warn(port.name + ": The phone went dead"); }); });
控制臺(tái)輸出如下:
Web頁(yè)面與擴(kuò)展間消息通信除了擴(kuò)展內(nèi)部和擴(kuò)展之間的通信,Web pages 也可以與擴(kuò)展進(jìn)行消息通信(單向)。這種通信方式與擴(kuò)展間的通信非常相似,共需要如下三步便可以通信。
首先,manifest.json指定可接收頁(yè)面的url規(guī)則。
"externally_connectable": { "matches": ["https://developer.chrome.com/*"] }
其次,Web pages 發(fā)送信息,比如說(shuō)在 https://developer.chrome.com/... 頁(yè)面控制臺(tái)執(zhí)行以上『擴(kuò)展程序間消息通信』小節(jié)——消息發(fā)送的語(yǔ)句。
最后,擴(kuò)展監(jiān)聽消息,代碼同以上『擴(kuò)展程序間消息通信』小節(jié)——消息監(jiān)聽部分。
至此,擴(kuò)展程序的消息通信聊得差不多了?;谝陨蟽?nèi)容,你完全可以自行封裝一個(gè)message.js,用于簡(jiǎn)化消息通信。實(shí)際上,閱讀模式擴(kuò)展程序就封裝了一個(gè)message.js,IHeader擴(kuò)展中的消息通信便基于它。
設(shè)置快捷鍵一般涉及到狀態(tài)切換的,快捷鍵能有效提升使用體驗(yàn)。為此我也為IHeader添加了快捷鍵功能。
為擴(kuò)展程序設(shè)置快捷鍵,共需要兩步。
manifest.json中添加commands聲明(可以指定多個(gè)命令)。
"commands": { // 命令 "toggle_status": { // 命令名稱 "suggested_key": { // 指定默認(rèn)的和各個(gè)平臺(tái)上綁定的快捷鍵 "default": "Alt+H", "windows": "Alt+H", "mac": "Alt+H", "chromeos": "Alt+H", "linux": "Alt+H" }, "description": "Toggle IHeader" // 命令的描述 } },
background.js中添加命令的監(jiān)聽。
/* 監(jiān)聽快捷鍵 */ chrome.commands.onCommand.addListener(function(command) { if (command == "toggle_status") { // 匹配命令名稱 chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { // 查詢當(dāng)前激活tab var tab = tabs[0]; tab && TabControler(tab.id, tab.url).switchActive(); // 切換tab控制器的狀態(tài) }); } });
以上,按下Alt+H鍵,便可以切換IHeader擴(kuò)展程序的監(jiān)聽狀態(tài)了。
設(shè)置快捷鍵時(shí),請(qǐng)注意Mac與Windows、linux等系統(tǒng)的差別,Mac既有Ctrl鍵又有Command鍵。另外,若設(shè)置的快捷鍵與Chrome的默認(rèn)快捷鍵沖突,那么設(shè)置將靜默失敗,因此請(qǐng)記得繞過(guò)以下Chrome快捷鍵(KeyCue是查看快捷鍵的應(yīng)用,請(qǐng)忽略之)。
添加右鍵菜單除了快捷鍵外,還可以為擴(kuò)展程序添加右鍵菜單,如IHeader的右鍵菜單。
為擴(kuò)展程序添加右鍵菜單,共需要三步。
申請(qǐng)菜單權(quán)限,需在manifest.json的permissions屬性中添加"contextMenus"權(quán)限。
"permissions": ["contextMenus"]
菜單需在background.js中手動(dòng)創(chuàng)建。
chrome.contextMenus.removeAll(); // 創(chuàng)建之前建議清空菜單 chrome.contextMenus.create({ // 創(chuàng)建右鍵菜單 title: "切換Header監(jiān)聽模式", // 指定菜單名稱 id: "contextMenu-0", // 指定菜單id contexts: ["all"] // 所有地方可見 });
由于chrome.contextMenus.create(object createProperties, function callback)方法默認(rèn)返回新菜單的id,因此它通過(guò)回調(diào)(第二個(gè)參數(shù)callback)來(lái)告知是否創(chuàng)建成功,而第一個(gè)參數(shù)createProperties則為菜單項(xiàng)指定配置信息。
綁定右鍵菜單的功能。
chrome.contextMenus.onClicked.addListener(function (menu, tab){ // 綁定點(diǎn)擊事件 TabControler(tab.id, tab.url).switchActive(); // 切換擴(kuò)展?fàn)顟B(tài) });安裝或更新
Chrome為擴(kuò)展程序提供了豐富的API,比如說(shuō),你可以監(jiān)聽擴(kuò)展安裝或更新事件,進(jìn)行一些初始化處理或給予友好的提示,如下。
/* 安裝提示 */ chrome.runtime.onInstalled.addListener(function(data){ if(data.reason == "install" || data.reason == "update"){ chrome.tabs.query({}, function(tabs){ tabs.forEach(function(tab){ TabControler(tab.id).restore(); // 恢復(fù)所有tab的狀態(tài) }); }); // 初始化時(shí)重啟全局監(jiān)聽器 ... // 動(dòng)態(tài)載入Notification js文件 setTimeout(function(){ var partMessage = data.reason == "install" ? "安裝成功" : "更新成功"; chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { var tab = tabs[0]; if (!/chrome:///.test(tab.url)){ // 只能在url不是"Chrome:// URL"開頭的頁(yè)面注入內(nèi)容腳本 chrome.tabs.executeScript(tab.id, {file: "res/js/notification.js"}, function(){ chrome.tabs.executeScript(tab.id, {code: "notification("IHeader"+ partMessage +"")"}, function(log){ log[0] && console.log("[Notification]: 成功彈出通知"); }); }); } else { console.log("[Notification]: Cannot access a chrome:// URL"); } }); },1000); // 延遲1s的目的是為了調(diào)試時(shí)能夠及時(shí)切換到其他的tab下,從而彈出Notification。 console.log("[擴(kuò)展]:", data.reason); } });
以上,chrome.tabs.executeScript(integer tabId, object details)接口,用于動(dòng)態(tài)注入內(nèi)容腳本,且只能在url不是"Chrome:// URL"開頭的頁(yè)面注入。其中tabId參數(shù)用于指定目標(biāo)標(biāo)簽頁(yè)的id,details參數(shù)用于指定內(nèi)容腳本的路徑或語(yǔ)句,它的file屬性指定腳本路徑,code屬性指定動(dòng)態(tài)語(yǔ)句。若分別往同一個(gè)標(biāo)簽頁(yè)注入多個(gè)腳本或語(yǔ)句,這些注入的腳本或語(yǔ)句處于同一個(gè)沙盒,即全局變量可以共享。
notification.js如下所示。
function notification(message) { if (!("Notification" in window)) { // 判斷瀏覽器是否支持Notification功能 console.log("This browser does not support desktop notification"); } else if (Notification.permission === "granted") { // 判斷是否授予通知的權(quán)限 new Notification(message); // 創(chuàng)建通知 return true; } else if (Notification.permission !== "denied") { // 首次向用戶申請(qǐng)權(quán)限 Notification.requestPermission(function (permission) { // 申請(qǐng)權(quán)限 if (permission === "granted") { // 用戶授予權(quán)限后, 彈出通知 new Notification(message); // 創(chuàng)建通知 return true; } }); } }
最終彈出通知如下。
國(guó)際化為了讓全球都能使用你開發(fā)的擴(kuò)展,國(guó)際化是必須的。從軟件工程的角度講,國(guó)際化就是將產(chǎn)品用戶界面中可見的字符串全部存放在資源文件中,然后根據(jù)用戶所處不同的語(yǔ)言環(huán)境,展示相應(yīng)語(yǔ)言的視覺信息。Chrome從v17版本開始就提供了國(guó)際化標(biāo)準(zhǔn)A
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/89794.html
摘要:協(xié)議采用了請(qǐng)求響應(yīng)模型。報(bào)頭分為通用報(bào)頭,請(qǐng)求報(bào)頭,響應(yīng)報(bào)頭和實(shí)體報(bào)頭。格式支持比鍵值對(duì)復(fù)雜得多的結(jié)構(gòu)化數(shù)據(jù),這一點(diǎn)也很有用。例如下面這段代碼最終發(fā)送的請(qǐng)求是這種方案,可以方便的提交復(fù)雜的結(jié)構(gòu)化數(shù)據(jù),特別適合的接口。 一 前言 ----現(xiàn)在搞前端的不學(xué)好http有關(guān)的知識(shí)已經(jīng)不行啦~筆者也是后知后覺,在搞node的時(shí)候意識(shí)到網(wǎng)絡(luò)方面的薄弱,開始學(xué)起http相關(guān)知識(shí)。這一篇是非?;A(chǔ)的講...
摘要:實(shí)時(shí)通訊越來(lái)越多應(yīng)用于各個(gè)領(lǐng)域。實(shí)現(xiàn)原生實(shí)現(xiàn)對(duì)象一共支持四個(gè)消息和。是基于的實(shí)時(shí)通信庫(kù)。服務(wù)器應(yīng)該用包含相同數(shù)據(jù)的乓包應(yīng)答客戶端發(fā)送探測(cè)幀由服務(wù)器發(fā)送以響應(yīng)數(shù)據(jù)包。主要用于在接收到傳入連接時(shí)強(qiáng)制輪詢周期。該間隔可通過(guò)配置修改。 隨著web技術(shù)的發(fā)展,使用場(chǎng)景和需求也越來(lái)越復(fù)雜,客戶端不再滿足于簡(jiǎn)單的請(qǐng)求得到狀態(tài)的需求。實(shí)時(shí)通訊越來(lái)越多應(yīng)用于各個(gè)領(lǐng)域。 HTTP是最常用的客戶端與服務(wù)端的...
摘要:今天總結(jié)下與網(wǎng)絡(luò)相關(guān)的知識(shí),不是那么詳細(xì),但是包含了我認(rèn)為重要的所有點(diǎn)。概要網(wǎng)絡(luò)知識(shí)我做了個(gè)方面的總結(jié),包括協(xié)議,協(xié)議,協(xié)議,協(xié)議,協(xié)議,,攻擊,其他協(xié)議??缬蛎缃癖黄毡橛迷诰W(wǎng)絡(luò)中,例如等。擁塞窗口的大小又取決于網(wǎng)絡(luò)的擁塞狀況。 前言 無(wú)論是 C/S 開發(fā)還是 B/S 開發(fā),無(wú)論是前端開發(fā)還是后臺(tái)開發(fā),網(wǎng)絡(luò)總是無(wú)法避免的,數(shù)據(jù)如何傳輸,如何保證正確性和可靠性,如何提高傳輸效率,如何解...
閱讀 1122·2021-11-22 13:53
閱讀 1694·2021-11-17 09:33
閱讀 2457·2021-10-14 09:43
閱讀 2991·2021-09-01 11:41
閱讀 2356·2021-09-01 10:44
閱讀 3061·2021-08-31 09:39
閱讀 1577·2019-08-30 15:44
閱讀 1920·2019-08-30 13:02