Chrome 插件开发指南和实践

看完这篇文章你会学到

  1. Chrome 插件可以做什么
  2. Chrome插件整体架构
  3. 如何开发 Chrome 插件(Popup 和 Devtools)
  4. 如何使用前端框架(React/Vue)进行开发
  5. 如何调试插件
  6. 如何使用 Puppeteer 对插件进行 E2E 测试(本地和 CI 环境)

Chrome 插件可以做什么

  1. 修改请求,包括修改请求和响应头,配置代理等,例如 模块头
  2. 在运行时将脚本注入页面,例如 Tampermonkey(油猴)
  3. 翻译、音乐等,例如 沙拉查询词 网易云音乐

总之,普通浏览器能做到的,插件基本都能做到

整体架构介绍

清晰的概念

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  
 <!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 面板。新的目录结构如下,绿色为新增

image.png

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面板

安装

  1. 点击Chrome右上角的“管理扩展”
  2. 右上角开启开发者模式
  3. 加载左上角的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 等配置  
 复制代码

明确构建产品

  1. manifest.json 描述插件的整体架构和权限
  2. 固定命名的 service worker、content_script 和 devtools.js 脚本(不带 hash),因为 manifest.json 中配置的名称是固定的(当然你可以写插件在构建过程中动态修改它们,这超出了本文的范围文章 )
  3. 可以看到,在上面的例子中,无论是service worker还是内容脚本实际上都没有在插件页面的入口js中 进口进口 (都只是 在 manifest.json 中描述,由插件本身注入 ),所以这部分不会包含在打包webpack等构建工具时生成的模块依赖图中,所以我们 它们需要单独打包为入口文件

删除多余部分后,目录结构如下,只关心绿色部分

image.png

修改设置

  1. 在public文件夹中添加一个manifest.json文件,格式同上面“如何开发自己的插件”

  2. 去掉 webpack.config.js 中生成 js 文件的 contenthash

  3. 使用 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 测试

github.com/puppeteer/p…

基本介绍

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 版权协议,转载请附上原文出处链接和本声明

本文链接:https://www.qanswer.top/38692/47322212

posted @ 2022-09-22 12:49  哈哈哈来了啊啊啊  阅读(1116)  评论(0编辑  收藏  举报