成人无码视频,亚洲精品久久久久av无码,午夜精品久久久久久毛片,亚洲 中文字幕 日韩 无码

資訊專欄INFORMATION COLUMN

【Chrome擴(kuò)展開發(fā)】定制HTTP請(qǐng)求響應(yīng)頭域

MadPecker / 1650人閱讀

摘要:關(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ì)分scriptpage,分別表示腳本和頁(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.jscontent.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、idelend(分別為頁(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 API

Chrome陸續(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 API

Chrome 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):loadingcomplete(部分頁(yè)面會(huì)包含favIconUrltitle信息),如下所示。

隨著狀態(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.sendMessageruntime.sendMessage 發(fā)送的消息,雖然extension.onMessageruntime.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

上述涉及到的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),可選值為loadingcomplete
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、panelappordevtools

注:丟棄的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
}
長(zhǎng)期連接消息通信

想必,一次性的消息通信你已經(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中)
擴(kuò)展程序間消息通信

相對(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

相關(guān)文章

  • Http請(qǐng)求中的Content-Type

    摘要:協(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ǔ)的講...

    Betta 評(píng)論0 收藏0
  • 基于socket.io快速實(shí)現(xiàn)一個(gè)實(shí)時(shí)通訊應(yīng)用

    摘要:實(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ù)端的...

    venmos 評(píng)論0 收藏0
  • 夯實(shí)基礎(chǔ)系列二:網(wǎng)絡(luò)知識(shí)總結(jié)

    摘要:今天總結(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ù)如何傳輸,如何保證正確性和可靠性,如何提高傳輸效率,如何解...

    wfc_666 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

閱讀需要支付1元查看
<