背景

最近在开发过程中,总是面临各种环境问题:比如需要添加额外的请求头,才能请求到特定环境的接口;比如接口异常,出现跨域问题等;极大影响了开发调试效率,虽然能处理网络请求/响应头,解决跨域的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格式的规则,这些规则会匹配所有的网络请求,并遵从这些规则做相应处理。匹配规则有静态规则和动态规则两种。

静态规则配置

  1. 需要创建一个配置规则集的json文件,通过json文件存储一个数组,里面可以放置多个规则。
  2. 需要在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:规则匹配时,采取的操作。包括以下四个参数:
    1. type: 必填参数,操作的类型,参数为:"block"(拦截请求), "redirect"(重定向请求), "allow"(允许请求), "upgradeScheme"(升级请求), "modifyHeaders"(修改请求头), 或"allowAllRequests"(允许所有请求)
    2. redirect:选填,只有type为redirect时才可填,配置如何执行重定向
  • extensionPath:相对于扩展目录的路径。应该以'/'开头
  • regexSubstitution:指定正则表达式过滤器的规则的替换模式
  • url:需要重定向到的网址,不可以重定向到javascript url
  • transform:url转换
  • requestHeaders:选填,只有操作类型为modifyHeaders时才可填,用于修改请求头
  • responseHeaders:选填,只有操作类型为modifyHeaders时才可填,用于修改响应头

  • condition:触发此规则的条件,有以下参数,皆是选填参数,常用参数(域名匹配、请求方法匹配、资源类型匹配、url匹配)

    1. domainType:指定网络请求是发起它的域的"firstParty"(第一方)还是"thirdParty"(第三方)。如果省略,则接受所有请求。
    2. domains:匹配域名列表,如省略则匹配所有域名的请求
    3. excludedDomains:和domains相反,指定一个排除匹配规则的域名列表,优先级高于domains
    4. requestMethods:http匹配请求方法("get"、"post"等)列表,如省略则匹配所有请求方法。
    5. excludedRequestMethods:与requestMethods相反,排除一个请求方法列表,优先级高于requestMethods
    6. resourceTypes:匹配资源类型("main_frame"、""xmlhttprequest"等)列表,如省略则匹配所有请求类型
    7. excludedResourceTypes:跟resourceTypes相反
    8. tabIds:匹配tabId列表,如省略则匹配所有tabs页
    9. excludedTabIds:和tabIds相反
    10. isUrlFilterCaseSensitive:是否区分大小写,默认是true,区分大小写
    11. regexFilter:匹配网络请求的正则表达式
    12. urlFilter:匹配网络请求的url

修改网络请求的MV3实现

我们的核心诉求就是修改请求头和跨域设置,具体功能就是在Action面板上配置请求头信息和跨域信息,然后保存到chrome.storage里面,发生请求时读取chrome.storage存储的配置信息,然后修改请求信息;

  • 如果是用chrome.webRequest来实现,过程和实现非常简单,基本如下:

MV2流程.drawio.png

  • 但是现在在MV3中,就只能使用chrome.declarativeNetRequest API来实现了,整个过程就变得不一样了,如下图:

规则更新流程.drawio.png

其中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就成了!

然后就最终成功了!!!

results matching ""

    No results matching ""