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