Chrome插件开发ReRes和request-interceptor源码赏析+复现+插件开发完整解决方案

Posted hans774882968

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Chrome插件开发ReRes和request-interceptor源码赏析+复现+插件开发完整解决方案相关的知识,希望对你有一定的参考价值。

文章目录

引言

这个项目主要目的是用前端工程化技术栈复现ReResrequest-interceptor,希望将两者的功能结合起来。request-interceptor是前端开发调试常用工具,提供了多种修改请求的功能,但无法将请求映射到本地的文件。ReRes是JS逆向工程师常用工具,可以用来更改页面请求响应的内容。可以把请求映射到其他的url,也可以映射到本机的文件或者目录。因为manifest version 3无法实现这两个插件的功能,所以这个项目仍然使用manifest version 2。本文假设你了解:

  • Chrome插件开发的manifest.json常见字段,尤其是browser_actionpopup页面)、options_pageoptions页面,扩展程序选项)和backgroundbackground.js)。

修改请求的代码都是在background.js中实现的。background.js实际上也在一个独立的页面运行。在chrome://extensions/点击插件的“背景页”链接即可对background.js进行调试。

亮点

  1. 赏析了若干源码:ReResrequest-interceptorhusky……
  2. 探讨了jest配置的若干问题。如:使用“鸭子类型”技巧解决模块不可测试的问题、配置路径别名……
  3. 编写构建脚本scripts/build.ts使得构建过程更为灵活。
  4. 使用react + vite展示了一套完整的Chrome插件开发的解决方案。包括:开发时预览、单元测试、构建。
  5. useLocalStorageStatehook源码进行了少量修改,并增加了配套的单元测试用例,以适应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地址。幸好没有特意进行混淆

  1. 安装插件。
  2. 以macOS为例,执行命令:open ~/Library/Application\\ Support/Google/Chrome/Default/Extensions,打开Chrome插件安装路径。
  3. 根据插件ID找到对应的文件夹。

如何获得request-interceptorbackground.js所使用的数据结构:阅读源码后知道,只需要在background.js控制台运行以下代码即可:

let dataSet1 = ;
let storageKey1 = '__redirect__chrome__extension__configuration__vk__';
chrome.storage.local.get(storageKey1, config => 
    dataSet1 = ;
    Object.assign(dataSet1, (config || )[storageKey1] || );
);

代码比较长就不完整贴出啦。带注释版源码地址,注释中包含对数据结构的讲解~

可以学到什么:

  1. 作者设计规则所执行的操作的时候,借鉴了http状态码设计的思想。add-request-headeradd-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"

所以我们需要:

  1. manifest.json
  2. background.ts
  3. popup.html和它引用的src/popup/popup.tsx
  4. options.html和它引用的src/options/options.tsx
  5. 一系列供tsx文件和background.ts共同使用的代码。
  6. 静态文件,放在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:sformat 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使用上有些坑:

  1. 当你有这样的结构:<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不产生影响。
  2. 打包体积增大。在只使用了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/_文件夹,下面有.gitignorehusky.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 $1h就是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插件专用的库

chrome插件开发-教程01(性能数据的获取和分析)

前端开发中一些好用的chrome插件汇总

chrome 浏览器插件开发—— 创建第一个chrome插件

chrome插件开发-教程00(如何开发插件)

查看页面性能的chrome插件开发