Chrome插件开发ReRes和request-interceptor源码赏析+复现+插件开发完整解决方案
Posted hans774882968
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Chrome插件开发ReRes和request-interceptor源码赏析+复现+插件开发完整解决方案相关的知识,希望对你有一定的参考价值。
文章目录
引言
这个项目主要目的是用前端工程化技术栈复现ReRes
和request-interceptor
,希望将两者的功能结合起来。request-interceptor
是前端开发调试常用工具,提供了多种修改请求的功能,但无法将请求映射到本地的文件。ReRes
是JS逆向工程师常用工具,可以用来更改页面请求响应的内容。可以把请求映射到其他的url,也可以映射到本机的文件或者目录。因为manifest version 3
无法实现这两个插件的功能,所以这个项目仍然使用manifest version 2
。本文假设你了解:
- Chrome插件开发的
manifest.json
常见字段,尤其是browser_action
(popup
页面)、options_page
(options
页面,扩展程序选项)和background
(background.js
)。
修改请求的代码都是在background.js
中实现的。background.js
实际上也在一个独立的页面运行。在chrome://extensions/
点击插件的“背景页”链接即可对background.js
进行调试。
亮点
- 赏析了若干源码:
ReRes
、request-interceptor
、husky
…… - 探讨了jest配置的若干问题。如:使用“鸭子类型”技巧解决模块不可测试的问题、配置路径别名……
- 编写构建脚本
scripts/build.ts
使得构建过程更为灵活。 - 使用
react + vite
展示了一套完整的Chrome插件开发的解决方案。包括:开发时预览、单元测试、构建。 - 对
useLocalStorageState
hook源码进行了少量修改,并增加了配套的单元测试用例,以适应Chrome插件开发的需求。
后续还会更新:仿request-interceptor
规则组、批量导入规则、react + vite
项目引入OB混淆……
作者:hans774882968以及hans774882968以及hans774882968
Chrome插件ReRes源码赏析
popup
页面和options
页面和background.js
唯一的联系就是,其他页面需要将数据写入背景页的localStorage
:
var bg = chrome.extension.getBackgroundPage();
//保存规则数据到localStorage
function saveData()
$scope.rules = groupBy($scope.maps, 'group');
bg.localStorage.ReResMap = angular.toJson($scope.maps);
background.js
注释版源码如下:
var ReResMap = [];
var typeMap =
"txt" : "text/plain",
"html" : "text/html",
"css" : "text/css",
"js" : "text/javascript",
"json" : "text/json",
"xml" : "text/xml",
"jpg" : "image/jpeg",
"gif" : "image/gif",
"png" : "image/png",
"webp" : "image/webp"
// 从背景页的localStorage读取ReResMap
function getLocalStorage()
ReResMap = window.localStorage.ReResMap ? JSON.parse(window.localStorage.ReResMap) : ReResMap;
// xhr请求本地文件的url,进行文本拼接,转为data url
function getLocalFileUrl(url)
var arr = url.split('.');
var type = arr[arr.length-1];
var xhr = new XMLHttpRequest();
xhr.open('get', url, false);
xhr.send(null);
var content = xhr.responseText || xhr.responseXML;
if (!content)
return false;
content = encodeURIComponent(
type === 'js' ?
content.replace(/[\\u0080-\\uffff]/g, function($0)
var str = $0.charCodeAt(0).toString(16);
return "\\\\u" + '00000'.substr(0, 4 - str.length) + str;
) : content
);
return ("data:" + (typeMap[type] || typeMap.txt) + ";charset=utf-8," + content);
// 看MDN即可,https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/onBeforeRequest
chrome.webRequest.onBeforeRequest.addListener(function (details)
// 这个url会在循环中被修改
var url = details.url;
for (var i = 0, len = ReResMap.length; i < len; i++)
var reg = new RegExp(ReResMap[i].req, 'gi');
if (ReResMap[i].checked && typeof ReResMap[i].res === 'string' && reg.test(url))
if (!/^file:\\/\\//.test(ReResMap[i].res))
// 普通url,只进行正则替换
do
url = url.replace(reg, ReResMap[i].res);
while (reg.test(url))
else
do
// file协议url,先正则替换,再转为data url
url = getLocalFileUrl(url.replace(reg, ReResMap[i].res));
while (reg.test(url))
return url === details.url ? : redirectUrl: url ;
,
urls: ["<all_urls>"],
["blocking"]
);
getLocalStorage();
window.addEventListener('storage', getLocalStorage, false);
Chrome插件request-interceptor background.js源码赏析
request-interceptor
作者说没有开源,但我们仍然能轻易找到其background.js
地址。幸好没有特意进行混淆
- 安装插件。
- 以macOS为例,执行命令:
open ~/Library/Application\\ Support/Google/Chrome/Default/Extensions
,打开Chrome插件安装路径。 - 根据插件ID找到对应的文件夹。
如何获得request-interceptor
的background.js
所使用的数据结构:阅读源码后知道,只需要在background.js
控制台运行以下代码即可:
let dataSet1 = ;
let storageKey1 = '__redirect__chrome__extension__configuration__vk__';
chrome.storage.local.get(storageKey1, config =>
dataSet1 = ;
Object.assign(dataSet1, (config || )[storageKey1] || );
);
代码比较长就不完整贴出啦。带注释版源码地址,注释中包含对数据结构的讲解~
可以学到什么:
- 作者设计规则所执行的操作的时候,借鉴了http状态码设计的思想。
add-request-header
、add-response-header
等操作的类型都是“add”,于是可以有下面的代码:
const modifyHeaders = (headers, action, name, value) =>
if (!headers || !action)
return;
if (action === 'add')
headers.set(name, value);
else if (action === 'modify')
if (headers.has(name))
headers.set(name, value);
else if (action === 'delete')
headers.delete(name);
;
// 调用
actionType = type.split('-')[0];
modifyHeaders(obj.responseHeaders, actionType, updatedName, updatedValue);
这一技巧可以减少一些重复的if-else
。
技术选型
React Hooks + vite + jest
。使用下面的命令来创建:
npm init @vitejs/app
如果对这条命令所做的事感兴趣,可以看参考链接4
但这条命令创建出的项目的文件结构是为构建单页应用而服务的,并不符合Chrome插件开发的需要,我们需要进行改造。我们期望的Chrome插件的manifest.json
如下:
"manifest_version": 2,
"name": "hans-reres",
"version": "0.0.0",
"description": "hans-reres旨在用前端工程化技术栈复现ReRes。ReRes是JS逆向工程师常用工具,可以用来更改页面请求响应的内容。通过指定规则,您可以把请求映射到其他的url,也可以映射到本机的文件或者目录。ReRes支持单个url映射,也支持目录映射。",
"browser_action":
"default_icon": "assets/icon.png",
"default_title": "hans-reres-popup",
"default_popup": "popup.html"
,
"icons":
"16": "assets/icon.png",
"48": "assets/icon48.png"
,
"options_page": "options.html",
"background":
"scripts": [
"background.js"
],
"persistent": true
,
"permissions": [
"tabs",
"webRequest",
"webRequestBlocking",
"<all_urls>",
"unlimitedStorage"
],
"homepage_url": "https://github.com/Hans774882968/hans-reres"
所以我们需要:
manifest.json
。background.ts
。popup.html
和它引用的src/popup/popup.tsx
。options.html
和它引用的src/options/options.tsx
。- 一系列供
tsx
文件和background.ts
共同使用的代码。 - 静态文件,放在
src/assets
文件夹下。
核心是希望构建流程用到这些文件,生成符合Chrome插件结构的产物,详见下文《构建流程》一节。
配置stylelint
根据参考链接1,首先
npm install stylelint stylelint-config-standard stylelint-order postcss-less -D
然后添加.stylelintrc.cjs
和.stylelintignore
,最后package.json
scripts
添加一条命令:
"lint:s": "stylelint \\"**/*.css,scss,less\\" --fix",
即可通过npm run lint:s
format less文件了。
更多stylelint
规则介绍见参考链接2
vscode配置保存自动修复
vscode打开设置,再打开settings.json
:
"editor.codeActionsOnSave":
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true,
,
若不生效,尝试重启vscode。
配置postcss、CSS Modules
react + vite
项目已经内置postcss,可以从package-lock.json
中看出:
"vite":
"requires":
"esbuild": "^0.16.14",
// 省略其他
"postcss": "^8.4.21",
,
"dependencies":
"rollup":
"requires":
"fsevents": "~2.3.2"
,
postcss-preset-env
装一下postcss-preset-env
插件,这个插件支持css变量、一些未来css语法以及自动补全:
npm i postcss-preset-env -D
添加postcss.config.cjs
:
const postcssPresetEnv = require('postcss-preset-env');
module.exports =
plugins: [postcssPresetEnv()]
;
配置postcss-preset-env
插件前:
._app_1afpm_1
padding: 20px;
user-select: none;
配置该插件后:
._app_1afpm_1
padding: 20px;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
flex-gap-polyfill
这个插件的配置步骤和上面的一样,不赘述。
代码:
.app
padding: 20px;
display: flex;
gap: 20px;
效果:
._app_13518_1
padding: 20px;
display: flex;
--fgp-gap: var(--has-fgp, 20px);
gap: 20px;
gap: var(--fgp-gap, 0px);
margin-top: var(--fgp-margin-top, var(--orig-margin-top));
margin-left: var(--fgp-margin-left, var(--orig-margin-left));
._app_13518_1
--has-fgp: ;
--element-has-fgp: ;
pointer-events: none;
pointer-events: var(--has-fgp) none;
--fgp-gap-row: 20px;
--fgp-gap-column: 20px;
._app_13518_1
--fgp-margin-top: var(--has-fgp) calc(var(--fgp-parent-gap-row, 0px) / (1 + var(--fgp--parent-gap-as-decimal, 0)) - var(--fgp-gap-row) + var(--orig-margin-top, 0px)) !important;
--fgp-margin-left: var(--has-fgp) calc(var(--fgp-parent-gap-column, 0px) / (1 + var(--fgp--parent-gap-as-decimal, 0)) - var(--fgp-gap-column) + var(--orig-margin-left, 0px)) !important;
flex-gap-polyfill踩坑
但要注意flex-gap-polyfill
使用上有些坑:
- 当你有这样的结构:
<div style="padding: 20px;"><div class="flex-and-gap"></div><div></div></div>
,那么.flex-and-gap
会因为使用了负margin,导致它右侧的div错位。解决方案:在.flex-and-gap
外面再套一层div,让.flex-and-gap
的负margin不产生影响。 - 打包体积增大。在只使用了3处
flex-gap
的情况下,css大小3.17kb -> 11.0kb
。
CSS Modules VSCode中点击查看样式
react + vite
项目使用less + CSS Modules
很简单。但使用VSCode时如何在不跳到less
文件的前提下方便地查看样式?根据参考链接12,安装VSCode CSS Modules插件后,用小驼峰命名styles.xxContainer
即可点击查看样式,但类名也要一起更改为小驼峰命名法。
另外,如果配置了stylelint
,还需要修改selector-class-pattern
:
'selector-class-pattern': '^[a-z]([A-Z]|[a-z]|[0-9]|-)+$'
配置husky + commitlint
根据参考链接8
(1)项目级安装commitlint
npm i -D @commitlint/config-conventional @commitlint/cli
(2)添加commitlint.config.cjs
(如果package.json
配置了"type": "module"
就需要.cjs
,否则git commit
时会报错)
module.exports =
extends: ['@commitlint/config-conventional'],
rules:
;
(3)安装husky:npm i -D husky
(4)对于husky版本>=5.0.0
,根据官方文档,首先安装git钩子:npx husky install
,运行后会生成.husky/_
文件夹,下面有.gitignore
和husky.sh
文件,都是被忽略的。接下来添加几个钩子:
npx husky add .husky/pre-commit "npm run lint"
npx husky add .husky/pre-commit "npm run lint:s"
npx husky add .husky/commit-msg 'npx commitlint --edit $1'
会生成.husky/commit-msg
和.husky/pre-commit
两个文件。不用命令,自己手动编辑也是可行的,分析过程见下文《husky add、install
命令解析》。
接下来可以尝试提交了。效果:
⧗ input: README添加husky + commitlint
✖ subject may not be empty [subject-empty]
✖ type may not be empty [type-empty]
husky add、install命令解析
vscode调试node cli程序
创建.vscode/launch.json
:
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
"type": "node-terminal",
"request": "launch",
"command": "npx husky add .husky/pre-commit 'npm run lint:s'",
"name": "npx husky add",
"skipFiles": [
"<node_internals>/**"
],
]
之后可以直接在“运行和调试”选择要执行的命令了。
husky add
命令举例:npx husky add .husky/commit-msg 'npx commitlint --edit $1'
cli的入口node_modules/husky/lib/bin.js
:
const [, , cmd, ...args] = process.argv;
const ln = args.length;
const [x, y] = args;
const hook = (fn) => () => !ln || ln > 2 ? help(2) : fn(x, y);
const cmds =
install: () => (ln > 1 ? help(2) : h.install(x)),
uninstall: h.uninstall,
set: hook(h.set),
add: hook(h.add),
['-v']: () => console.log(require(p.join(__dirname, '../package.json')).version),
;
try
cmds[cmd] ? cmds[cmd]() : help(0);
x, y
分别表示文件名.husky/commit-msg
和待添加的命令npx commitlint --edit $1
。h
就是node_modules/husky/lib/index.js
。找到相关函数:
function set(file, cmd)
const dir = p.dirname(file);
if (!fs.existsSync(dir))
throw new Error(`can't create hook, $dir directory doesn't exist (try running husky install)`);
fs.writeFileSync(file, `#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
$cmd以上是关于Chrome插件开发ReRes和request-interceptor源码赏析+复现+插件开发完整解决方案的主要内容,如果未能解决你的问题,请参考以下文章
chrome 浏览器插件开发—— 通信 获取页面 编写chrome插件专用的库