背景
最近在开发过程中,总是面临各种环境问题:比如需要添加额外的请求头,才能请求到特定环境的接口;比如接口异常,出现跨域问题等;极大影响了开发调试效率,虽然能处理网络请求/响应头,解决跨域的chrome扩展很多,但是因为我们的场景有一些特殊,所以这些扩展要么不是不好用,就是不起作用。恰好我们自己开发过,用来进行接口Mock的chrome插件,一直用着很方便,那何不在此插件的基础上,再增加可以自定义请求头及跨域的能力呢?
网上一搜,chrome.webRequest API轻松解决此问题,看来很快就能如丝般顺滑的开发调试代码了。 说干就干,代码都快要写好了,结果,什么!webRequest API在MV3中已经废弃了,废弃了!
那怎么办,怎么办?没事,咱MV3不是有declarativeNetRequest API呢,分分钟也能搞定。。。
然而declarativeNetRequest API的教程真是太少了,官方文档太简单,太不清晰了,其他文章只有简单的介绍,但是很少有实战的,这下真是麻烦大了。
真是一坑又一坑,落入了持续的艰难的避坑斗争!
webRequest vs declarativeNetRequest
话说 webRequest API用着好好的,既简单方便,又成熟稳定,为什么在MV3中要废弃呢?这不是自找麻烦么?
Chrome官方的解释是,由于WebRequest API的机制是当网络请求发起进,就会拦截,然后就可以进行各种分析处理,但是很容易被开发者滥用,导致性能受影响,并且给开发者的权限太大,很可能导致安全问题。因此在MV3中,chrome扩展提供了声明式的API,即是chrome.declarativeNetRequest,即在manifest.json中配置一套规则,通过规则匹配,就可以指定要拦截的请求或者类型,这种方式更加安全,性能也更好,也是官方主推的方式。
二者的核心区别就是chrome.webRequest API在实现网络请求修改时,是通过实时拦截,直接修改,可以很方便的在任一个请求中进行拦截,分析和修改,非常简单和方便;但是chrome.declarativeNetRequest API是声明式的,预先通过配置好一套规则,然后当网络请求时,如果匹配上了规则,就会根据规则对网络请求修改,否则不处理。这样网络请求和规则设定就变成了2个非常独立的过程,根本无法监控网络请求及做出相应的处理,整个实现思路和chrome.webRequest API的方法就完全不一样了;
declarativeNetRequest API介绍
官方文档:https://developer.chrome.com/docs/extensions/reference/declarativeNetRequest/
chrome.declarativeNetRequest在官方文档上有非常多的说明和介绍,比较细致,但内容多且是英文,不仅不容易理解,而且缺少案例和Demo,读起来云里雾里,非常吃力;
这个API的核心就是进行规则配置和匹配,我们在开发中主要的工作就是规则配置,declarativeNetRequest主要提供了2种规则配置方式:静态规则和动态规则,具体使用如下:
1. 权限设定
在manifest.json的permissions中添加以下权限:
{
"permissions": ["declarativeNetRequest", "declarativeNetRequestWithHostAccess", "declarativeNetRequestFeedback"],
"host_permissions": ["<all_urls>"]
}
- 若申请的是declarativeNetRequest权限,则阻止或升级请求不需要申请"host_permissions",但是如果需要重定向请求,或者修改请求头,则要申请相应的"host_permissions"
若申请的是declarativeNetRequestWithHostAccess权限,则任何功能都需要申请相应的"host_permissions"。
若申请的是declarativeNetRequestFeedback,则可以使用getMatchedRules()方法。并且能够监听匹配成功事件,但是该监听仅允许用于测试环境使用。
2. 添加规则集
使用declarativeNetRequest对网络请求做处理,实质上是定义一些json格式的规则,这些规则会匹配所有的网络请求,并遵从这些规则做相应处理。匹配规则有静态规则和动态规则两种。
静态规则配置
- 需要创建一个配置规则集的json文件,通过json文件存储一个数组,里面可以放置多个规则。
- 需要在manifest.json中添加一个declarative_net_request的配置信息,通过path指向配置规则集的json文件。
manifest.json
{
"name": "declarativeNetRequest extension",
"version": "1",
"declarative_net_request": {
"rule_resources": [{
"id": "ruleset_1",
"enabled": true,
"path": "rules.json"
}]
},
"permissions": ["*://*.google.com/*", "*://*.abcd.com/*", "*://*.example.com/*", "https://*.xyz.com/*", "*://*.headers.com/*", "declarativeNetRequest"],
"manifest_version": 3
}
rule_resources有三个参数:
id:每个规则集需要对应一个id
enabled:规则集是否启用
path:配置规则的json文件地址
rules.json
[{
"id": 1,
"priority": 1,
"action": {
"type": "block"
},
"condition": {
"urlFilter": "google.com",
"resourceTypes": ["main_frame"]
}
},
{
"id": 2,
"priority": 1,
"action": {
"type": "allow"
},
"condition": {
"urlFilter": "google.com/123",
"resourceTypes": ["main_frame"]
}
},
{
"id": 3,
"priority": 2,
"action": {
"type": "block"
},
"condition": {
"urlFilter": "google.com/12345",
"resourceTypes": ["main_frame"]
}
},
{
"id": 4,
"priority": 1,
"action": {
"type": "redirect",
"redirect": {
"url": "https://example.com"
}
},
"condition": {
"urlFilter": "google.com",
"resourceTypes": ["main_frame"]
}
},
{
"id": 5,
"priority": 1,
"action": {
"type": "redirect",
"redirect": {
"extensionPath": "/a.jpg"
}
},
"condition": {
"urlFilter": "abcd.com",
"resourceTypes": ["main_frame"]
}
},
{
"id": 6,
"priority": 1,
"action": {
"type": "redirect",
"redirect": {
"transform": {
"scheme": "https",
"host": "new.example.com"
}
}
},
"condition": {
"urlFilter": "||example.com",
"resourceTypes": ["main_frame"]
}
},
{
"id": 7,
"priority": 1,
"action": {
"type": "redirect",
"redirect": {
"regexSubstitution": "https://\\1.xyz.com/"
}
},
"condition": {
"regexFilter": "^https://www\\.(abc|def)\\.xyz\\.com/",
"resourceTypes": [
"main_frame"
]
}
},
{
"id": 8,
"priority": 2,
"action": {
"type": "allowAllRequests"
},
"condition": {
"urlFilter": "||b.com/path",
"resourceTypes": ["sub_frame"]
}
},
{
"id": 9,
"priority": 1,
"action": {
"type": "block"
},
"condition": {
"urlFilter": "script.js",
"resourceTypes": ["script"]
}
},
{
"id": 10,
"priority": 2,
"action": {
"type": "modifyHeaders",
"responseHeaders": [{
"header": "h1",
"operation": "remove"
},
{
"header": "h2",
"operation": "set",
"value": "v2"
},
{
"header": "h3",
"operation": "append",
"value": "v3"
}
]
},
"condition": {
"urlFilter": "headers.com/123",
"resourceTypes": ["main_frame"]
}
},
{
"id": 11,
"priority": 1,
"action": {
"type": "modifyHeaders",
"responseHeaders": [{
"header": "h1",
"operation": "set",
"value": "v4"
},
{
"header": "h2",
"operation": "append",
"value": "v5"
},
{
"header": "h3",
"operation": "append",
"value": "v6"
}
]
},
"condition": {
"urlFilter": "headers.com/12345",
"resourceTypes": ["main_frame"]
}
}
]
动态规则集配置
动态规则集可以通过代码,调用chrome.declarativeNetRequest.updateDynamicRules方法,实现动态的规则更新;
chrome.declarativeNetRequest.updateDynamicRules({
addRules: [{
"id": 1,
"priority": 1,
"action": {
"type": "block"
},
"condition": {
"urlFilter": "abc",
"domains": ["foo.com"],
"resourceTypes": ["script"]
}
},
{
"id": 2,
"priority": 1,
"action": {
"type": "block"
},
"condition": {
"urlFilter": "abc",
"domains": ["foo.com"],
"resourceTypes": ["script"]
}
}, {
"id": 3,
"priority": 1,
"action": {
"type": "modifyHeaders",
"responseHeaders": [{
"header": "h1",
"operation": "set",
"value": "v4"
},
{
"header": "h2",
"operation": "append",
"value": "v5"
},
{
"header": "h3",
"operation": "append",
"value": "v6"
}
]
},
"condition": {
"urlFilter": "headers.com/12345",
"resourceTypes": ["main_frame"]
}
},
],
removeRuleIds: [11, 12, 13],
}, () => {})
3. 规则
一个规则以对象的格式定义,其中有四个属性:id、priority、action、condition
- id:每个规则需要对应一个id,不同的规则不能设置相同的id
- priority:优先级,填一个数字,数字越大,优先级越高
- action:规则匹配时,采取的操作。包括以下四个参数:
- type: 必填参数,操作的类型,参数为:"block"(拦截请求), "redirect"(重定向请求), "allow"(允许请求), "upgradeScheme"(升级请求), "modifyHeaders"(修改请求头), 或"allowAllRequests"(允许所有请求)
- redirect:选填,只有type为redirect时才可填,配置如何执行重定向
- extensionPath:相对于扩展目录的路径。应该以'/'开头
- regexSubstitution:指定正则表达式过滤器的规则的替换模式
- url:需要重定向到的网址,不可以重定向到javascript url
- transform:url转换
- requestHeaders:选填,只有操作类型为modifyHeaders时才可填,用于修改请求头
responseHeaders:选填,只有操作类型为modifyHeaders时才可填,用于修改响应头
condition:触发此规则的条件,有以下参数,皆是选填参数,常用参数(域名匹配、请求方法匹配、资源类型匹配、url匹配)
- domainType:指定网络请求是发起它的域的"firstParty"(第一方)还是"thirdParty"(第三方)。如果省略,则接受所有请求。
- domains:匹配域名列表,如省略则匹配所有域名的请求
- excludedDomains:和domains相反,指定一个排除匹配规则的域名列表,优先级高于domains
- requestMethods:http匹配请求方法("get"、"post"等)列表,如省略则匹配所有请求方法。
- excludedRequestMethods:与requestMethods相反,排除一个请求方法列表,优先级高于requestMethods
- resourceTypes:匹配资源类型("main_frame"、""xmlhttprequest"等)列表,如省略则匹配所有请求类型
- excludedResourceTypes:跟resourceTypes相反
- tabIds:匹配tabId列表,如省略则匹配所有tabs页
- excludedTabIds:和tabIds相反
- isUrlFilterCaseSensitive:是否区分大小写,默认是true,区分大小写
- regexFilter:匹配网络请求的正则表达式
- urlFilter:匹配网络请求的url
修改网络请求的MV3实现
我们的核心诉求就是修改请求头和跨域设置,具体功能就是在Action面板上配置请求头信息和跨域信息,然后保存到chrome.storage里面,发生请求时读取chrome.storage存储的配置信息,然后修改请求信息;
- 如果是用chrome.webRequest来实现,过程和实现非常简单,基本如下:
- 但是现在在MV3中,就只能使用chrome.declarativeNetRequest API来实现了,整个过程就变得不一样了,如下图:
其中Action面板和chrome.storage的实现就比较容易了,这里就不具体说明了,核心主要是如何实现动态的更新规则,这里面就涉及到什么时机更新规则,怎么更新规则,在哪里更新规则,用哪些方法等,具体如下:
- 在background的service work 中执行初始化及storage中规则数据变更时,执行规则更新代码;
- 主要用到chrome.declarativeNetRequest.updateDynamicRules方法,首先获取当前已存在的动态规则集,然后获取 已存在规则集的id数组,再通过storage中的配置生成规则,调用updateDynamicRules方法,传入addRules和removeIds2个参数,用于添加新的规则集和删除已有的规则集;
样例代码如下:
const createRules = (list) => {
const headers = list.map((item) => {
const {
name,
value
} = item;
const header = {
header: name,
value,
ooperation: 'set',
};
return header;
});
const rule = {
id,
priority: 1,
action: {
type: 'modifyHeaders',
requestHeaders: headers,
},
condition: {
urlFilter: '*',
resourceTypes: ['main_frame', 'sub_frame', 'xmlhttprequest'],
},
};
return rule;
};
const getNewRules = async () => {
try {
const list = await getHeaderList(); // 从storage内获取规则数据,由于不是网络请求修改的核心代码,故省略函数实现
const rule = createRules(list);
return [rule];
} catch (err) {
return null;
}
};
const getCurrentRules = async () => {
const rules = await chrome.declarativeNetRequest.getDynamicRules();
return rules;
};
const setRules = async () => {
try {
const newRules = await getNewRules();
const currentRules = await getCurrentRules();
const removeIds = currentRules?.map((rule) => rule.id);
const option = {
addRules: newRules,
removeRuleIds: removeIds,
};
chrome.declarativeNetRequest.updateDynamicRules(option, () => {
console.log(chrome.runtime.lastError);
});
} catch (err) {
console.log(err);
}
};
const initRules = async () => {
setRules();
};
const updateRules = () => {
chrome.storage.onChanged.addListener(async (changes, area) => {
if (changes && changes.updateRule && area === 'local') {
setRules();
}
});
};
const setRequestheader = () => {
initRules();
updateRules();
};
setRequestheader();
问题
declarativeNetRequest,对于官方来说,确实解决了性能和安全等问题,但是对于开发者来说,由于declarativeNetRequest目前的教程和实践比较少,现成的代码及实现也很少,因此很难找到合适的代码及相应的解决方案和问题处理,实现起来会遇到各种各样的问题,导致很简单的一段代码,调试了很久才成功;
其中“Rule with id xxx specifies an invalid request header to be appended. Only standard HTTP request headers that can specify multiple values for a single entry are supported”问题,特别困扰,隔了一个春节,才最终解决;
当前网上根本无法找到这个问题的相关内容及说明,只能苦苦调试、思索了;问题的大概意思就是同一个请求头,设置了多值,因为这个请求头不是标准的HTTP请求头,所以就报错了;通过不懈的调试,调试,调试。。。终于终于问题解决了!!!
问题的核心应该包括2个部分:
1、background脚本启动后,会执行2个部分:
- initRules函数(执行规则初始设定,获取storage内规则信息,然后执行动态规则更新,提供新的规则集,同时将要删除的规则id数据设置为空)
- updateRules函数(在storage触发数据变更时执行,并且也是获取storage内规则信息,然后执行动态规则更新,提供新的规则集,但是将要删除的规则id数据设置为当前的规则集id数组,确保规则集数据不重复)
但是这2个部分,因为代码里面存在多个异步过程,最终是initRules先执行规则更新,还是updateRules先执行规则更新,是不确定的;如果是updateRules先执行了,然后initRules后执行更新,那么因为initRules执行的时候,removeRuleIds为[],这样没有将之前的规则集删除,结果就导致请求头重复了,然后报错了;
2、生成规则的时候,当规则的action的type为modifyHeaders时,请求头的数据结构包括3个字段:header、value和operation,其中operation的取值包括3个值:append、set、remove;我理解remvoe是表示移除头,append应该是新增,set应该是修改;所以在代码实现上用的是append结果,结果就一直报错、报错,然后改成set就成了!
然后就最终成功了!!!