一.整体架构介绍
明确概念
Chrome 插件本质上就是一个特殊的 Web 页面,在这个基础上我们明确下文的称谓:{ "name": "Hello Extensions", // 名称 "description": "An introductory tutorial", // 描述 "version": "1.0", // 插件的版本 "manifest_version": 3, // 清单的版本,目前都是使用 V3 // action 字段主要描述点击右上角图标弹出的页面 "action": { "default_popup": "index.html", // 对应的入口 html 文件(Popup 在后面介绍) "default_title": "Garfish Module", "default_icon": { "16": "favicon.ico", "48": "favicon.ico", "128": "favicon.ico" } }, // 当需要使用一些特殊 API 时需要在 permissions 声明权限,会提示给用户 "permissions": ["storage", "scripting"], // 哪些域名允许使用插件 "host_permissions": ["<all_urls>"], // 堆代码 duidaima.com // 声明 background service worker 的路径,在后面介绍 "background": { "service_worker": "background.js" }, // 声明 content script 的入口文件路径、允许使用的域名以及执行时机 "content_scripts": [{ "js": ["content.js"], "matches": ["<all_urls>"], // 有 "document_start" "document_idle" "document_end" 三个值 "run_at": "document_idle" }] }content_script(内容脚本)
chrome.devtools.panels.create( // 扩展面板显示名称 "DevPanel", // 扩展面板icon,并不展示 "panel.png", // 扩展面板页面 "index.html", function (panel) { console.log("自定义面板创建成功!"); } );像 Vue Devtools 和 React Devtools 都是这种形式,其视图本质上就是一个 Web 页面。
// 发送方 service worker || content_script chrome.runtime.sendMessage(data) // 接收方 content_script || service worker chrome.runtime.onMessage.addListener(() => {})但时常 content_script 会有多个,service_worker 只有一个,上述方式会通知所有的 content_script,导致出现问题,推荐指定发送到某一个 Tab 下的 content_script。
// 获取当前活跃 Tab(活跃 Tab 概念可以看「明确概念」部分) chrome.tabs.query({active: true}, (tabs) => { chrome.tabs.sendMessage(tabs[0].id, response =>{ console.log("background -> content script infos have been sended"); } }二.如何开发一个自己的插件
hello-extensions ├── background │ └── index.js ├── index.html ├── index.js ├── manifest.json ├── package-lock.json ├── package.json └── scripts └── index.js配置 manifest.json
{ "name": "Hello Extensions", "description" : "Base Level Extension", "version": "1.0", "manifest_version": 3, "action": { "default_title": "Hello Extensions", "default_popup": "index.html" // 指向入口 html 文件 }, "background": { "service_worker": "background/index.js" // 指向一个 js 文件 }, "content_scripts": [{ "matches": ["<all_urls>"], "run_at": "document_idle", "js": ["scripts/index.js"] // 指向一个 js 文件 }], }popup 入口 html
<!-- index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>堆代码 duidaima.com</title> </head> <body> <div id="root"> <input /> <button>confirm</button> </div> <script src="./index.js"></script> </body> </html>js 文件
// hello-extensions/index.js console.log('i am index.js in html'); // hello-extensions/background/index.js console.log('i am service worker'); // hello-extensions/scripts/index.js console.log('i am content script');至此一个最简单的 popup 插件就已经完成,点击右上角图标即可打开 popup 面板。
├── background │ └── index.js ├── devtools │ └── index.js ├── devtools.html ├── index.html ├── index.js ├── manifest.json ├── package.json └── scripts └── index.js// manifest.json
"devtools_page": "devtools.html"Devtools 入口 html 文件 devtools.html 中需要引入一段 js 脚本来创建 devtools 面板。其实这个 devtools.html 个人觉得有些多余,直接指向这个 js 脚本来创建面板就可以了,而不需要这个 html 文件。
<!-- devtools.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script src="./devtools/index.js"></script> </body> </html> // devtools/index.js // 创建扩展面板 chrome.devtools.panels.create( // 扩展面板显示名称 "DevPanel", // 扩展面板icon,并不展示 "panel.png", // 扩展面板页面 "../index.html", function (panel) { console.log("自定义面板创建成功!"); } );然后安装插件打开 F12 即可看到 Devtools 面板:
npx create-react-app hello-extensions-react cd hello-extensions-react npm install npm run eject // 弹出 create-react-app 创建的模版项目的 webpack config 等配置明确构建产物
├── README.md ├── config │ ├── env.js │ ├── getHttpsConfig.js │ ├── jest │ │ ├── babelTransform.js │ │ ├── cssTransform.js │ │ └── fileTransform.js │ ├── modules.js │ ├── paths.js // 一些路径配置 │ ├── webpack │ │ └── persistentCache │ │ └── createEnvironmentHash.js │ ├── webpack.config.js // webpack 配置 │ └── webpackDevServer.config.js ├── package.json ├── public │ ├── devtools.html │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── scripts │ ├── build.js │ ├── start.js │ └── test.js └── src ├── App.js ├── background │ └── index.js ├── content_scripts │ └── index.js ├── devtools │ └── index.js └── index.js修改配置
// paths.js + devtoolsHtml: resolveApp('public/devtools.html'), + devtools: resolveModule(resolveApp, 'src/devtools/index'), + background: resolveModule(resolveApp, 'src/background/index'), + content_script: resolveModule(resolveApp, 'src/content_scripts/index'), // webpack.config.js // dev 环境打包 service worker 等也没用,因为正常 web 页面中不会使用到 entry: isEnvProduction ? { main: paths.appIndexJs, devtools: paths.devtools, background: paths.background, content_script: paths.content_script } : { main: paths.appIndexJs }, // 配置 HtmlWebpackPlugin new HtmlWebpackPlugin( Object.assign( {}, { inject: true, filename: 'index.html', template: paths.appHtml, chunks: ['main'] } ) ), new HtmlWebpackPlugin( Object.assign( {}, { inject: true, filename: 'devtools.html', template: paths.devtoolsHtml, chunks: ['devtools'] } ) ),此时打包会报 eslint 的错误:
// .eslintrc { "env": { "webextensions": true } }执行 npm run build 后将产物文件夹按上面「如何开发一个自己的插件」安装即可(Devtools 没出来关掉浏览器重试,比较玄学),后续就是正常的 Web 开发流程了,当然如果你想使用一些插件的 API 还是会报错的,这样只适合开发正常 Web 页面逻辑,需要调试插件独有的 API 还是需要 build 后安装再进行调试。
`chrome-extension://${插件id}` // 例如 'chrome-extension://dmlpmahdbmhcfonakcknmkeobmopidgl'所以我们的目标变成了获取插件的 id,而 Puppeteer 是支持自动安装上插件的,问题在于安装上之后如何获取 id,此时代码如下:
const puppeteer = require('puppeteer'); async function bootstrap(options) { const { appUrl } = options; const extensionPath = 'xxx'; // 插件路径 const browser = await puppeteer.launch({ headless: false, // 需要配置有头模式,无头模式找不到 service worker args: [ // 除了 extensionPath 的插件都禁用掉,避免测试被影响 `--disable-extensions-except=${extensionPath}`, // 安装插件 `--load-extension=${extensionPath}` ] }); const appPage = await browser.newPage(); await appPage.goto(appUrl, { waitUntil: 'load' }); const targets = await browser.targets(); // 找到 sercice worker 即可获取到目标插件 const extensionTarget = targets.find((target) => { return target.type() === 'service_worker' }); // 解析目标插件 url 获得插件 id const partialExtensionUrl = extensionTarget.url() || ''; const [, , extensionId] = partialExtensionUrl.split('/'); const extPage = await browser.newPage(); const extensionUrl = `chrome-extension://${extensionId}/index.html`; await extPage.goto(extensionUrl, { waitUntil: 'load' }); return { appPage, browser, extensionUrl, extPage }; } bootstrap({ appUrl: 'https://www.baidu.com' }) module.exports = { bootstrap };其中的问题
// xxx-pipeline.yaml steps: - name: Configuration xvfb commands: - sudo apt-get update - sudo apt-get install xvfb // 手动也可以 sudo apt-get update sudo apt-get install xvfb然后调整启动测试脚本的逻辑
// before node test/index.js // after xvfb-run node test/index.js然后我们就可以愉快的发现在比如 Linux 环境下也可以跑通用例了~