背景
今天在写一个某网站限流检测的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
dom
i18n
storage
runtime.connect()
runtime.getManifest()
runtime.getURL()
runtime.id
runtime.onConnect
runtime.onMessage
但是,如果你在
mainfest.json
中将content_scripts
内的js配置为了"world": "MAIN"
,那么,上面那些API就无法使用了,这点是文档里没有提到的。
(细想下,如果这样可行的话,那content_scripts
简直太强了,既和页面在一个执行环境,能够获取页面的变量、DOM等,又拥有Chrome
插件的的一些API,屌爆简直)解决办法有两种:
- 在
mainfest.json
中将"world"
改为"ISOLATED"
,或删除 (默认"ISOLATED"
)- 在
background.js
中动态注入mainfest.json
chrome.scripting.registerContentScripts([ { id: 'script-id', js: [mainWorldLoader], persistAcrossSessions: false, world: 'MAIN' } ])
参考链接
当然,上面那种情况注入是主动,适合一打开页面就注入脚本的场景,如果场景不一样,还有别的注入方式。
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
可以获取完整响应内容,但需一直开着devtools
chrome.declarativeNetRequest
重定向请求到本地服务,略显臃肿,特殊场景可以考虑替换页面请求方法,操作简单,需要考虑注入方式
- 主动注入使用
content_scripts
+world
方式 - 被动注入在
background
中执行chrome.scripting.executeScript
方法
- 主动注入使用
完结,撒花✿✿ヽ(°▽°)ノ✿
EOF
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。