Chrome 插件开发指南和实践
看完这篇文章你会学到
- Chrome 插件可以做什么
- Chrome插件整体架构
- 如何开发 Chrome 插件(Popup 和 Devtools)
- 如何使用前端框架(React/Vue)进行开发
- 如何调试插件
- 如何使用 Puppeteer 对插件进行 E2E 测试(本地和 CI 环境)
Chrome 插件可以做什么
- 修改请求,包括修改请求和响应头,配置代理等,例如 模块头
- 在运行时将脚本注入页面,例如 Tampermonkey(油猴)
- 翻译、音乐等,例如 沙拉查询词 网易云音乐
总之,普通浏览器能做到的,插件基本都能做到
整体架构介绍
清晰的概念
Chrome插件本质上是一个特殊的网页,在此基础上,我们澄清下标题
- 插件页面:指插件网页的内容
- 目标页面:指打开插件的页面,这个页面就是目标页面,比如在百度打开插件,百度就是此时的目标页面
- Popup:安装插件时,浏览器右上角会有一个插件图标,点击打开的页面是Popup(弹窗)
- Devtools:F12 打开开发者工具并将它们显示在顶部。比如 element 和 network 都是 Devtools。
- Tab:每一个底部都是一个单独的Tab,中间高亮部分是当前停留的页面,即 活动标签
清单.json
扩展从它们的清单开始,每个扩展都需要一个清单。
每个扩展都以清单描述文件开头,每个扩展都需要它。
类似于前端项目 包.json 文件,用于描述整个插件的结构和权限,类似如下结构,所有字段都可以在官网查看
{
"name" : "Hello Extensions" , // 名称
"description" : "入门教程" , // 描述
"version" : "1.0" , // 插件版本
"manifest_version" : 3 , // manifest 的版本,目前使用 V3
// action字段主要描述点击右上角图标弹出的页面
“行动” : {
"default_popup" : "index.html" //对应入口html文件(Popup后面会介绍)
"default_title" : "Garfish 模块" ,
“默认图标”:{
"16": "favicon.ico",
"48": "favicon.ico",
“128”:“favicon.ico”
}
} ,
// 当需要使用一些特殊的API时,需要在permissions中声明权限,会提示给用户
“权限”:[“存储”,“脚本”],
// 允许哪些域使用插件
“主机权限”:[“<all_urls> " ] ,
// 声明后台Service Worker的路径,后面会介绍
“背景” : {
“service_worker”:“background.js”
} ,
// 声明内容脚本的入口文件路径,允许的域名和执行时间
“内容脚本”:[{
“js”:[“content.js”]
“火柴” : [ ”<all_urls> " ] ,
// 共有三个值“document_start” “document_idle” “document_end”
“run_at”:“document_idle”,
}
复制代码
content_script(内容脚本)
内容脚本是在网页上下文中运行的文件。通过使用标准 文档对象模型 (DOM),它们能够读取浏览器访问的网页的详细信息,对其进行更改,并将信息传递给它们的父扩展。
内容脚本在 目标页面 要在上下文中运行的文件。通过使用标准的文档对象模型 (DOM),他们可以阅读 目标页面 详细信息,对其进行更改,并将信息传递给其父扩展。
content_script 本质上是一个 js文件 , 可以使用 特定于插件的 API ,并且可以在目标页面的上下文中操作DOM,在需要操作目标页面的DOM时可以使用, 它的生命周期随着插件的打开和关闭而开始和结束 .
但请注意 content_script 有自己的 独立上下文 ,这意味着它在类似沙盒的环境中运行。此时修改content_script中的全局变量,如window.a = 1,将不会体现在 目标页面 而不是修改沙箱中的 window 变量。
service_worker(后台)
事件是浏览器触发器,例如导航到新页面、删除书签或关闭选项卡。扩展使用后台服务工作者中的脚本监控这些事件,然后根据指定的指令做出反应。
浏览器触发事件,例如导航到新页面、删除书签或关闭选项卡。该扩展使用后台服务工作者来监听这些事件并在触发时执行回调。
service_worker 在单独的线程中运行,您可以使用 特定于插件的 API ,本质上也是一个js文件,和content_script的区别是:
- service_worker 的生命周期较长,从打开浏览器开始,到关闭浏览器结束。 content_script 的生命周期遵循插件的打开和关闭。通常使用 service_worker 监听一些用户操作来执行回调
- service_worker 无法访问目标页面的 DOM,而 content_script 可以
弹出
点击浏览器插件右上角的小图标时弹出的页面称为Popup,它的视图本质上是一个Web页面
Devtools(调试工具)
DevTools 扩展的结构与任何其他扩展类似:它可以有背景页面、内容脚本和其他项目。此外,每个 DevTools 扩展都有一个 DevTools 页面,可以访问 DevTools API。
DevTools 扩展的结构与任何其他扩展类似:它可以有一个后台页面(服务工作者)、内容脚本和其他项目。此外,每个 DevTools 扩展都有一个 DevTools 页面,提供对 DevTools API 的访问。
Devtools 也是一种插件形式。与 Popup 不同,它有一组 chrome.devtools 独有的 API。当我们调用 chrome.devtools.panels.create 时,我们可以创建一个自定义面板
铬合金。开发工具。面板。创造(
// 扩展面板显示名称
"开发面板",
// 扩展面板图标,不显示
"面板.png",
// 扩展面板页面
"index.html",
功能(面板){
安慰。 log("自定义面板创建成功!");
}
);
复制代码
像 Vue Devtools 和 React Devtools 都是这种形式,视图本质上是一个网页
沟通
完整的通讯推荐查看: juejin.cn/post/702107…
由于 content_script 和 service worker 独立于插件页面,所以经常需要消息传递。基本方法
// 发送者服务工作者 ||内容脚本
铬 .runtime .sendMessage(数据)
// 接收者内容脚本 ||服务人员
铬 .runtime .onMessage .addListener(() => {})
复制代码
但是,通常有多个 content_scripts 并且只有一个 service_worker。上面的方法会通知所有的 content_scripts,导致问题。建议指定发送到某个 Tab 的 content_script。
// 获取当前活动Tab(活动Tab的概念可以看“定义概念”部分)
铬合金。标签。查询({活动:真},(标签)=> {
铬合金。标签。 sendMessage(tabs[0].id, 响应 =>{
安慰。 log("背景 -> 内容脚本信息已发送"); }
}
复制代码
如何开发自己的插件
目录结构
你好扩展
├── 背景
│ └── index.js
├── index.html
├── index.js
├── manifest.json
├── package-lock.json
├── package.json
└── 脚本
└── index.js
复制代码
配置 manifest.json
{
“名称”:“你好扩展”,
“描述”:“基本级别扩展”,
“版本”:“1.0”,
“manifest_version”:3,
“行动” : {
"default_title" : "你好扩展" ,
"default_popup" : "index.html" // 指向入口 html 文件
} ,
“背景” : {
"service_worker" : "background/index.js" // 指向一个 js 文件
} ,
“内容脚本”:[{
“火柴” : [ ”<all_urls> " ] ,
“run_at”:“document_idle”,
"js" : [ "scripts/index.js" ] // 指向一个 js 文件
} ] ,
}
复制代码
弹出入口html
// index.html
<!DOCTYPE html >
< html lang = "en" >
<头>
<元字符集=“UTF-8”>
< meta http-equiv = "X-UA-Compatible" 内容 = "IE=edge" >
<元名称=“视口”内容=“宽度=设备宽度,初始比例=1.0”>
<title>文档</ title >
</ head >
<身体>
< div id = "根" >
<输入/>
<按钮>确认</ button >
</ div >
< 脚本 src = "./index.js" ></ script >
</ body >
</ html >
复制代码
js文件
// 你好扩展/index.js
安慰。 log('我是 html 中的 index.js');
// hello-extensions/background/index.js
安慰。 log('我是服务人员');
// 你好扩展/脚本/index.js
安慰。 log('我是内容脚本');
复制代码
至此,最简单的Popup插件就完成了。单击右上角的图标以打开弹出面板。
开发开发工具
Devtools 很特别。它需要在 manifest.json 中添加 devtools_page 字段以指向一个入口 html 文件。在这个文件中,需要引入一段 js 来创建 Devtools 面板。新的目录结构如下,绿色为新增
Devtools的入口html文件devtools.html需要引入一个js脚本来创建Devtools面板。其实这个devtools.html个人感觉有点多余,直接指向这个js脚本创建面板,没有这个html文件(个人意见)
// devtools.html
<!DOCTYPE html >
< html lang = "en" >
<头>
<元字符集=“UTF-8”>
< meta http-equiv = "X-UA-Compatible" 内容 = "IE=edge" >
<元名称=“视口”内容=“宽度=设备宽度,初始比例=1.0”>
<title>文档</ title >
</ head >
<身体>
< 脚本 src = "./devtools/index.js" ></ script >
</ body >
</ html >
复制代码 // 开发工具/index.js
// 创建扩展面板
铬合金。开发工具。面板。创造(
// 扩展面板显示名称
"开发面板",
// 扩展面板图标,不显示
"面板.png",
// 扩展面板页面
"../index.html",
功能(面板){
安慰。 log("自定义面板创建成功!");
}
);
复制代码
然后安装插件并打开F12看到Devtools面板
安装
- 点击Chrome右上角的“管理扩展”
- 右上角开启开发者模式
- 加载左上角的hello-extensions文件夹
如何使用前端框架进行开发
以上模式开发有什么问题
- Dev模式下没有热更新,每次修改都需要手动刷新页面
- 构建工具的代码压缩、分包等一系列优化在构建过程中无法享受
- 该语法不支持降级,在低版本浏览器中可能会报错等。
所以我们需要使用熟悉的前端框架进行开发。这里我们以create-react-app为例(Vue类似)创建一个React项目
npx create-react-app hello-extensions-react
cd hello-extensions-react
npm 安装
npm run eject // 弹出 create-react-app 创建的模版项目的 webpack config 等配置
复制代码
明确构建产品
- manifest.json 描述插件的整体架构和权限
- 固定命名的 service worker、content_script 和 devtools.js 脚本(不带 hash),因为 manifest.json 中配置的名称是固定的(当然你可以写插件在构建过程中动态修改它们,这超出了本文的范围文章 )
- 可以看到,在上面的例子中,无论是service worker还是内容脚本实际上都没有在插件页面的入口js中 进口进口 (都只是 在 manifest.json 中描述,由插件本身注入 ),所以这部分不会包含在打包webpack等构建工具时生成的模块依赖图中,所以我们 它们需要单独打包为入口文件
删除多余部分后,目录结构如下,只关心绿色部分
修改设置
-
在public文件夹中添加一个manifest.json文件,格式同上面“如何开发自己的插件”
-
去掉 webpack.config.js 中生成 js 文件的 contenthash
-
使用 service worker、content_scripts 和 devtools 进入 多入口包装 ,并且由于 Devtools 需要一个默认的 html 页面来导入打包好的 devtools.js(参考“如何开发自己的插件”), 所以这时候还需要配置HtmlWebpackPlugin多生成一个html文件,并将devtools.js生成的chunk引入其中。
// 路径.js
devtoolsHtml: resolveApp('public/devtools.html'),
devtools:resolveModule(resolveApp,'src/devtools/index'),背景:resolveModule(resolveApp,'src/background/index'),
content_script: resolveModule(resolveApp, 'src/content_scripts/index'),// webpack.config.js
// 在 dev 环境中打包 service worker 是没有用的,因为在普通网页中是不会用到的
条目: isEnvProduction ?
{
主要:paths.appIndexJs,
开发工具:paths.devtools,
背景:paths.background,
content_script:paths.content_script,
} :
{
主要:paths.appIndexJs,
},// 配置 HtmlWebpackPlugin
新的 HtmlWebpackPlugin(
对象.assign(
{},
{
注入:真,
文件名:'index.html',
模板:paths.appHtml,
块:['主'],
},
)
),
新的 HtmlWebpackPlugin(
对象.assign(
{},
{
注入:真,
文件名:'devtools.html',
模板:paths.devtoolsHtml,
块:['devtools'],
},
)
),
复制代码
此时包会报eslint错误
我们需要在根目录下添加.eslintrc文件来描述当前应用的目标产品是extensions,即插件产品
// .eslintrc
{
“环境”:{
“网络扩展”:真
}
}
复制代码
执行npm run build后,按照上面“如何开发自己的插件”安装product文件夹(Devtools没有出来,关闭浏览器重试,比较玄学),如下—— up是正常的Web开发流程,当然,如果你想要一些插件的API还是会报错,只适合开发正常的网页逻辑。需要调试插件独有的API,或者在构建后安装后再调试。
如何调试插件
插件的调试有点麻烦,分为四个方面:
- 调试弹出窗口
- 使用 Devtools 进行调试
- content_script 调试
- 服务工作者调试
弹出页面的调试
我们右击右上角的插件图标->查看弹出内容进入插件的html页面进行调试
使用 Devtools 进行调试
F12打开控制台,选择Devtools面板单独打开浏览器进行调试
然后在这个页面Mac系统长按 命令 + 选项 + i (我目前没有windows,大家可以自己试试,盲猜shift、ctrl、alt i的组合),可以打开 用于开发工具的开发工具 (没错,就是套娃),这个时候你不仅可以调试自己的插件, 您还可以调试默认元素、网络和其他面板 ,我们也可以看到类似的 网络面板其实是和上面一样的开发模式,默认集成在Chrome中
content_script 调试
由于content_script被注入到目标页面,F12打开目标页面的控制台进行调试
服务工作者调试
进入插件面板,点击 service worker 进行调试
如何使用 Puppeteer 进行 E2E 测试
基本介绍
Puppeteer 是一个浏览器自动化工具,可以帮助我们 自动控制浏览器行为 ,比如打开指定的URL、点击某个按钮等。
如何测试
正常的E2E测试需要跳转到指定的URL再进行测试,Chrome插件也不例外,所以我们只需要获取插件页面的URL并跳转进去就可以进行正常的E2E测试.
首先观察插件页面的URL组成如下
`chrome-extension:// ${plugin id} `
// 例如
'铬扩展://dmlpmahdbmhcfonakcknmkeobmopidgl'
复制代码
所以我们的目标是获取插件的id,Puppeteer支持插件的自动安装。问题是如何在安装后获取 id。代码如下
const puppeteer = require('puppeteer');
异步函数引导(选项){
常量 { appUrl } = 选项;
常量扩展路径 = 'xxx'; // 插件路径
const browser = 等待木偶师。发射({
headless: false, // 需要配置headed模式,headless模式下找不到service worker
参数:[
// 除了 extensionPath 之外的所有插件都被禁用以避免测试受到影响
`--disable-extensions-except= ${extensionPath} `,
// 安装插件
`--load-extension= ${extensionPath} `,
],
});
const appPage = 等待浏览器。新页面();
等待应用页面。 goto(appUrl, { waitUntil: 'load' });
const 目标 = 等待浏览器。目标();
// 找到 service worker 获取目标插件
const extensionTarget = 目标。查找((目标)=> {
返回目标。类型() === 'service_worker'
});
// 解析目标插件url获取插件id
const partialExtensionUrl = 扩展目标。网址()|| '';
const [, , extensionId] = partialExtensionUrl。分裂( '/');
const extPage = 等待浏览器。新页面();
常量 extensionUrl = `chrome-extension:// ${extensionId} /index.html`;
等待分页。 goto(extensionUrl, { waitUntil: 'load' });
返回 {
应用页面,
浏览器,
扩展网址,
分页,
};
}
模块。出口 = { 引导 };
// 例如
// 引导程序({
// appUrl: 'https://www.baidu.com'
// })
复制代码
问题
上面的模式在headed模式下本地运行E2E测试是没有问题的,因为 本地图形用户界面 , 但如果在 CI 环境中 比如Linux环境下没有图形界面 ,此时headless:false,即headless模式会直接报错,导致测试失败,所以我们需要在没有图形界面的环境下进行 模拟一套图形界面 ,那么我们可以使用 xvfb
Xvfb 在没有图形设备的机器上实现 X11 显示服务协议。它实现了其他图形界面有的各种界面,但没有真正的图形界面。所以当一个程序在Xvfb中调用GUI相关的操作时,这些操作会在虚拟内存中运行,但是你什么都看不到。
链接: zhuanlan.zhihu.com/p/350944759
简单来说就是让浏览器认为自己运行在一个有图形界面的系统中,这样headed模式才能正常运行
首先,需要在CI环境中安装xvfb。一般我们配置一个流水线脚本来做,例如
// xxx-pipeline.yaml
脚步:
- 名称:配置 xvfb
命令:
- sudo apt-get 更新
- sudo apt-get 安装 xvfb
// 也可以手动完成
sudo apt-get 更新
sudo apt-get install xvfb
复制代码
然后调整逻辑启动测试脚本
// 前
节点测试/index.js
// 后
xvfb-运行节点测试/index.js
复制代码
然后我们可以愉快地发现,在Linux环境下我们也可以运行通用的例子,例如~
✅ 至此,已经基本覆盖了开发一个Chrome插件的整个生命周期
本文代码: github.com/nyqykk/地狱…
版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议。转载请附上原文出处链接和本声明。
这篇文章的链接: https://homecpp.art/1322/10153/1147
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明