闽公网安备 35020302035485号
一.整体架构介绍
明确概念
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 环境下也可以跑通用例了~