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