背景
今天在寫一個某網站限流檢測的chrome插件,需要捕獲頁面的某個請求結果。那麼問題就來了,我們該如何捕獲頁面的請求結果呢?我們來捋捋都有哪些方案。
我開發的時候的配置為
manifest_version: 3,下文內容也是在這個基礎上展開的。本文只列舉方案,一些需同步在
manifest_version進行配置地方並未提及,請自行配置。
可行的方案
一、chrome.webRequest
chrome插件提供有chrome.webRequest這麼個API,這是一個系列API,允許開發者對請求的多個階段進行事件監聽。
但比較可惜的是這個API能力比較有限,你不能通過它直接獲取到響應內容。
以OnBeforeRequest和onCompleted這兩個階段的監聽來説,你能獲取到的數據有這些:
當然,人家也不是完全沒用,如果是以下幾種情況就適用
- 所需要的內容在
responseHeaders上 - 利用他給的這些參數足夠重新發起一次請求
聽起來略微費勁,我放棄這種方案,感覺它更適合用來統計或者屏蔽一些請求。
二、chrome.devtools.network
devtools就是我們經常用的開發者工具欄,chrome.devtools.network的onRequestFinished事件可以在回調中拿到完整的請求和響應,然後通過getContent方法拿到具體的返回結果。
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);
});
});
}
);
創建好的panel見下圖
拿到的結果如下:
這確實是一個能拿到完整信息的方案,但前提是你得一直開着devtools才行,如果是交付用户使用的場景,這簡直是不可容忍的,所以需酌情使用。
三、chrome.declarativeNetRequest請求重定向 + 本地服務
如果你在本地起一個服務,然後使用chrome.declarativeNetRequest方法通過配置把請求重定向到本地服務,這樣也可以拿到請求的結果,相當於加了一層代理。
這種方法有點臃腫,但是有些特殊的場景下確實不失為一種解決方案。
四、替換頁面請求方法(XMLHttpRequest)(推薦👍)
其實我們還可以替換頁面中的請求方法,讓頁面用我們的方法去發請求,那我們拿請求的結果就如探囊取物,這是目前最理想的方式了。以XMLHttpRequest為例,可以這麼寫。
// 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);
這樣,我們就可以狸貓換太子,把頁面中的請求方法換成自己的了。
但是現在又遇到一個問題,我們該如何將這段代碼注入到頁面中呢?這個方法還是較多的。
1. content_scripts注入 (推薦👍)
我們知道content_scripts是和目標頁面運行在一起的,所以把上面的代碼直接寫在content.js中就行了。但是,如果你就這麼做了,你就會發現:
>>>>> replace XMLHttpRequest這條log也能打出來,但是我們期待的console.log('xhr', xhr)卻沒有執行,替換沒生效。
那這是咋回事呢?這就不得不提出這樣一個問題:content_scripts的運行環境和目標頁面到底是不是在一塊的?
chrome的開發者文檔裏是這麼寫的:
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.
這裏面有一句關鍵的,run in the context of web pages,隨後還有更關鍵的。
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.
簡單來説就是,content_scripts和目標頁面是運行在一塊的,DOM這些是共用一套,但是Javascript執行環境是隔離的。這也就解釋了為啥上面替換沒生效,因為上面的腳本換掉的是自己執行環境中的XMLHttpRequest,而不是目標頁面的。
那麼,能不能讓注入代碼的執行環境和目標頁面的執行環境不隔離呢?
這是個好問題。答案是可以的,chrome插件提供了一個叫world的配置項,它有兩個值:ISOLATED(默認值)和MAIN。前者指明content_scripts是在隔離的環境中執行的,後者指明content_scripts和目標頁面在一個環境中執行。
在一個環境執行,也就意味着,注入的腳本可以獲取、修改目標頁面的全局變量,替換請求方法更是不在話下。(對於爬蟲開發者來説,這個配置意味着很多,是個值錢的知識點)。
所以,我們大致如下這麼配置就可以實現替換。
// manifest.json
{
"content_scripts": [
{
"matches": ["target_page_url"],// 改為自己的目標網站url
"js": ["inject.js"], // 要注入的腳本
"world": "MAIN", // 注入代碼和目標頁面在一個環境中執行
"run_at": "document_start" // 注入腳本的時機
}
]
}
到目前為止,上面這段核心manifest.json配置,加上inject.js,不需要額外的background.js,甚至無需permissions配置即可實現自動在目標頁面注入我們的代碼,獲取請求結果,甚為簡單、優雅。
但是需要注意的,這裏有個bug。
雖然説chrome文檔裏寫的是
Content scripts可以直接使用這些API
domi18nstorageruntime.connect()runtime.getManifest()runtime.getURL()runtime.idruntime.onConnectruntime.onMessage
runtime.sendMessage()但是,如果你在
mainfest.json中將content_scripts內的js配置為了"world": "MAIN",那麼,上面那些API就無法使用了,這點是文檔裏沒有提到的。
(細想下,如果這樣可行的話,那content_scripts簡直太強了,既和頁面在一個執行環境,能夠獲取頁面的變量、DOM等,又擁有Chrome插件的的一些API,屌爆簡直)解決辦法有兩種:
- 在
mainfest.json中將"world"改為"ISOLATED",或刪除 (默認"ISOLATED")- 在
background.js中動態注入mainfest.jsonchrome.scripting.registerContentScripts([ { id: 'script-id', js: [mainWorldLoader], persistAcrossSessions: false, world: 'MAIN' } ])參考鏈接
- CRXJS doesn't work with content script in "MAIN" world #695
- Strategies for injecting code with
registerContentScriptsand CRXJS #643
當然,上面那種情況注入是主動,適合一打開頁面就注入腳本的場景,如果場景不一樣,還有別的注入方式。
2. 從background中注入
在background裏,我們可以使用chrome.scripting.executeScript方法向頁面注入代碼,相關配置參數較多,可以自行查看,比較關鍵的是,它支持world
配置,值的情況同上面的一樣。
chrome.action.onClicked.addListener(function(tab) {
chrome.scripting.executeScript({
target: {tabId: tab.id},
files: ["inject.js"],
world: 'MAIN'
});
});
它適合被動觸發的情況,比如某個頁面給background.js一個消息,然後background裏執行chrome.scripting.executeScript方法,發起注入操作。這裏需要注意的是,調用chrome.scripting.executeScript方法,需要申請scripting權限。
3. 其他注入方式
在content_scripts裏你還可以通過向頁面中插入script標籤的形式實現動態注入,這裏不展開描述。
總結
至此,我們實現了優雅的捕獲頁面的請求結果這一目的。來總結下。
chrome.webRequest只能拿到請求頭和響應頭,不能獲取響應內容chrome.devtools.network可以獲取完整響應內容,但需一直開着devtoolschrome.declarativeNetRequest重定向請求到本地服務,略顯臃腫,特殊場景可以考慮-
替換頁面請求方法,操作簡單,需要考慮注入方式
- 主動注入使用
content_scripts+world方式 - 被動注入在
background中執行chrome.scripting.executeScript方法
- 主動注入使用
完結,撒花✿✿ヽ(°▽°)ノ✿
EOF