背景
今天在寫(xiě)一個(gè)某網(wǎng)站限流檢測(cè)的chrome插件,需要捕獲頁(yè)面的某個(gè)請(qǐng)求結(jié)果。那么問(wèn)題就來(lái)了,我們?cè)撊绾尾东@頁(yè)面的請(qǐng)求結(jié)果呢?我們來(lái)捋捋都有哪些方案。
我開(kāi)發(fā)的時(shí)候的配置為
manifest_version: 3,下文內(nèi)容也是在這個(gè)基礎(chǔ)上展開(kāi)的。本文只列舉方案,一些需同步在
manifest_version進(jìn)行配置地方并未提及,請(qǐng)自行配置。
可行的方案
一、chrome.webRequest
chrome插件提供有chrome.webRequest這么個(gè)API,這是一個(gè)系列API,允許開(kāi)發(fā)者對(duì)請(qǐng)求的多個(gè)階段進(jìn)行事件監(jiān)聽(tīng)。

但比較可惜的是這個(gè)API能力比較有限,你不能通過(guò)它直接獲取到響應(yīng)內(nèi)容。
以OnBeforeRequest和onCompleted這兩個(gè)階段的監(jiān)聽(tīng)來(lái)說(shuō),你能獲取到的數(shù)據(jù)有這些:
當(dāng)然,人家也不是完全沒(méi)用,如果是以下幾種情況就適用
- 所需要的內(nèi)容在
responseHeaders上 - 利用他給的這些參數(shù)足夠重新發(fā)起一次請(qǐng)求
聽(tīng)起來(lái)略微費(fèi)勁,我放棄這種方案,感覺(jué)它更適合用來(lái)統(tǒng)計(jì)或者屏蔽一些請(qǐng)求。
二、chrome.devtools.network
devtools就是我們經(jīng)常用的開(kāi)發(fā)者工具欄,chrome.devtools.network的onRequestFinished事件可以在回調(diào)中拿到完整的請(qǐng)求和響應(yīng),然后通過(guò)getContent方法拿到具體的返回結(jié)果。
chrome.devtools.panels.create(
"My Panel",
"icon.png",
"devtools.html",
function (panel) {
chrome.devtools.network.onRequestFinished.addListener(function (response) {
console.log("onRequestFinished", response);
response.getContent(function (content) {
console.log("content", content);
});
});
}
);創(chuàng)建好的panel見(jiàn)下圖
拿到的結(jié)果如下:
這確實(shí)是一個(gè)能拿到完整信息的方案,但前提是你得一直開(kāi)著devtools才行,如果是交付用戶使用的場(chǎng)景,這簡(jiǎn)直是不可容忍的,所以需酌情使用。
三、chrome.declarativeNetRequest請(qǐng)求重定向 + 本地服務(wù)
如果你在本地起一個(gè)服務(wù),然后使用chrome.declarativeNetRequest方法通過(guò)配置把請(qǐng)求重定向到本地服務(wù),這樣也可以拿到請(qǐng)求的結(jié)果,相當(dāng)于加了一層代理。
這種方法有點(diǎn)臃腫,但是有些特殊的場(chǎng)景下確實(shí)不失為一種解決方案。
四、替換頁(yè)面請(qǐng)求方法(XMLHttpRequest)(推薦??)
其實(shí)我們還可以替換頁(yè)面中的請(qǐng)求方法,讓頁(yè)面用我們的方法去發(fā)請(qǐng)求,那我們拿請(qǐng)求的結(jié)果就如探囊取物,這是目前最理想的方式了。以XMLHttpRequest為例,可以這么寫(xiě)。
// inject.js
(function (xhr) {
if (XMLHttpRequest.prototype.sayMyName) return;
console.log("%c>>>>> replace XMLHttpRequest", "color:yellow;background:red");
var XHR = XMLHttpRequest.prototype;
XHR.sayMyName = "aqinogbei";
var open = XHR.open;
var send = XHR.send;
XHR.open = function (method, url) {
this._method = method; // 記錄method和url
this._url = url;
return open.apply(this, arguments);
};
XHR.send = function () {
if (this._url.includes("target_path")) {
this.addEventListener("load", function (xhr) {
console.log('xhr', xhr)
});
}
return send.apply(this, arguments);
};
})(XMLHttpRequest);這樣,我們就可以貍貓換太子,把頁(yè)面中的請(qǐng)求方法換成自己的了。
但是現(xiàn)在又遇到一個(gè)問(wèn)題,我們?cè)撊绾螌⑦@段代碼注入到頁(yè)面中呢?這個(gè)方法還是較多的。
1. content_scripts注入 (推薦??)
我們知道content_scripts是和目標(biāo)頁(yè)面運(yùn)行在一起的,所以把上面的代碼直接寫(xiě)在content.js中就行了。但是,如果你就這么做了,你就會(huì)發(fā)現(xiàn):
>>>>> replace XMLHttpRequest這條log也能打出來(lái),但是我們期待的console.log('xhr', xhr)卻沒(méi)有執(zhí)行,替換沒(méi)生效。
那這是咋回事呢?這就不得不提出這樣一個(gè)問(wèn)題:content_scripts的運(yùn)行環(huán)境和目標(biāo)頁(yè)面到底是不是在一塊的?
chrome的開(kāi)發(fā)者文檔里是這么寫(xiě)的:
Content scripts are files that run in the context of web pages. Using the standard Document Object Model (DOM), they are able to read details of the web pages the browser visits, make changes to them, and pass information to their parent extension.
這里面有一句關(guān)鍵的,run in the context of web pages,隨后還有更關(guān)鍵的。
Work in isolated worlds
Content scripts live in an isolated world, allowing a content script to make changes to its JavaScript environment without conflicting with the page or other extensions' content scripts.
Key term: An isolated world is a private execution environment that isn't accessible to the page or other extensions. A practical consequence of this isolation is that JavaScript variables in an extension's content scripts are not visible to the host page or other extensions' content scripts. The concept was originally introduced with the initial launch of Chrome, providing isolation for browser tabs.
簡(jiǎn)單來(lái)說(shuō)就是,content_scripts和目標(biāo)頁(yè)面是運(yùn)行在一塊的,DOM這些是共用一套,但是Javascript執(zhí)行環(huán)境是隔離的。這也就解釋了為啥上面替換沒(méi)生效,因?yàn)樯厦娴哪_本換掉的是自己執(zhí)行環(huán)境中的XMLHttpRequest,而不是目標(biāo)頁(yè)面的。
那么,能不能讓注入代碼的執(zhí)行環(huán)境和目標(biāo)頁(yè)面的執(zhí)行環(huán)境不隔離呢?
這是個(gè)好問(wèn)題。答案是可以的,chrome插件提供了一個(gè)叫world的配置項(xiàng),它有兩個(gè)值:ISOLATED(默認(rèn)值)和MAIN。前者指明content_scripts是在隔離的環(huán)境中執(zhí)行的,后者指明content_scripts和目標(biāo)頁(yè)面在一個(gè)環(huán)境中執(zhí)行。
在一個(gè)環(huán)境執(zhí)行,也就意味著,注入的腳本可以獲取、修改目標(biāo)頁(yè)面的全局變量,替換請(qǐng)求方法更是不在話下。(對(duì)于爬蟲(chóng)開(kāi)發(fā)者來(lái)說(shuō),這個(gè)配置意味著很多,是個(gè)值錢(qián)的知識(shí)點(diǎn))。
所以,我們大致如下這么配置就可以實(shí)現(xiàn)替換。
// manifest.json
{
"content_scripts": [
{
"matches": ["target_page_url"],// 改為自己的目標(biāo)網(wǎng)站url
"js": ["inject.js"], // 要注入的腳本
"world": "MAIN", // 注入代碼和目標(biāo)頁(yè)面在一個(gè)環(huán)境中執(zhí)行
"run_at": "document_start" // 注入腳本的時(shí)機(jī)
}
]
}到目前為止,上面這段核心manifest.json配置,加上inject.js,不需要額外的background.js,甚至無(wú)需permissions配置即可實(shí)現(xiàn)自動(dòng)在目標(biāo)頁(yè)面注入我們的代碼,獲取請(qǐng)求結(jié)果,甚為簡(jiǎn)單、優(yōu)雅。
但是需要注意的,這里有個(gè)bug。
雖然說(shuō)chrome文檔里寫(xiě)的是
Content scripts可以直接使用這些API
domi18nstorageruntime.connect()runtime.getManifest()runtime.getURL()runtime.idruntime.onConnectruntime.onMessage但是,如果你在
mainfest.json中將content_scripts內(nèi)的js配置為了"world": "MAIN",那么,上面那些API就無(wú)法使用了,這點(diǎn)是文檔里沒(méi)有提到的。
(細(xì)想下,如果這樣可行的話,那content_scripts簡(jiǎn)直太強(qiáng)了,既和頁(yè)面在一個(gè)執(zhí)行環(huán)境,能夠獲取頁(yè)面的變量、DOM等,又擁有Chrome插件的的一些API,屌爆簡(jiǎn)直)解決辦法有兩種:
- 在
mainfest.json中將"world"改為"ISOLATED",或刪除 (默認(rèn)"ISOLATED")- 在
background.js中動(dòng)態(tài)注入mainfest.jsonchrome.scripting.registerContentScripts([ { id: 'script-id', js: [mainWorldLoader], persistAcrossSessions: false, world: 'MAIN' } ])參考鏈接
當(dāng)然,上面那種情況注入是主動(dòng),適合一打開(kāi)頁(yè)面就注入腳本的場(chǎng)景,如果場(chǎng)景不一樣,還有別的注入方式。
2. 從background中注入
在background里,我們可以使用chrome.scripting.executeScript方法向頁(yè)面注入代碼,相關(guān)配置參數(shù)較多,可以自行查看,比較關(guān)鍵的是,它支持world
配置,值的情況同上面的一樣。
chrome.action.onClicked.addListener(function(tab) {
chrome.scripting.executeScript({
target: {tabId: tab.id},
files: ["inject.js"],
world: 'MAIN'
});
});它適合被動(dòng)觸發(fā)的情況,比如某個(gè)頁(yè)面給background.js一個(gè)消息,然后background里執(zhí)行chrome.scripting.executeScript方法,發(fā)起注入操作。這里需要注意的是,調(diào)用chrome.scripting.executeScript方法,需要申請(qǐng)scripting權(quán)限。
3. 其他注入方式
在content_scripts里你還可以通過(guò)向頁(yè)面中插入script標(biāo)簽的形式實(shí)現(xiàn)動(dòng)態(tài)注入,這里不展開(kāi)描述。
總結(jié)
至此,我們實(shí)現(xiàn)了優(yōu)雅的捕獲頁(yè)面的請(qǐng)求結(jié)果這一目的。來(lái)總結(jié)下。
chrome.webRequest只能拿到請(qǐng)求頭和響應(yīng)頭,不能獲取響應(yīng)內(nèi)容chrome.devtools.network可以獲取完整響應(yīng)內(nèi)容,但需一直開(kāi)著devtoolschrome.declarativeNetRequest重定向請(qǐng)求到本地服務(wù),略顯臃腫,特殊場(chǎng)景可以考慮替換頁(yè)面請(qǐng)求方法,操作簡(jiǎn)單,需要考慮注入方式
- 主動(dòng)注入使用
content_scripts+world方式 - 被動(dòng)注入在
background中執(zhí)行chrome.scripting.executeScript方法
- 主動(dòng)注入使用
完結(jié),撒花??ヽ(°▽°)ノ?





