Chrome 插件本质上就是一个特殊的 Web 页面,在这个基础上我们明确下文的称谓:
Extensions start with their manifest and every extension requires a manifest.
每个扩展都始于一份 manifest 描述文件并且每个扩展都需要它。
https://developer.chrome.com/docs/extensions/mv3/manifest/
它就类似前端项目中的 package.json 文件,用于描述整个插件的架构和权限,类似下面这种结构,全部字段可以看官网。
{
"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>"],
// 声明 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 scripts are files that run in the context of web pages. By using the standard Document Object Model (DOM), they are able to read details of the web pages the browser visits, make changes to them, and pass information to their parent extension.
内容脚本是在目标页面上下文中运行的文件。通过使用标准的文档对象模型(Document Object Model,DOM),它们能够读取浏览器访问的目标页面的详细信息,对它们进行更改,并将信息传递给它们的父扩展。
https://developer.chrome.com/docs/extensions/mv3/content_scripts/
content_script 本质上就是一个 js 文件,它可以使用插件特有的 API,同时可以操作目标页面上下文中的 DOM,当你需要操作目标页面的 DOM 时可以使用它,他的生命周期随着插件的打开和关闭而开始和结束。
但是请注意 content_script 有自己独立的上下文,这就意味着它运行在类似沙盒的环境中,此时在 content_script 中修改全局变量,例如 window.a = 1 不会反映到目标页面中,而是修改的沙盒中的 window 变量。
Events are browser triggers, such as navigating to a new page, removing a bookmark, or closing a tab. Extensions monitor these events using scripts in their background service worker, which then react with specified instructions.
浏览器触发事件,例如导航到一个新页面,删除一个书签,或关闭一个标签。扩展使用后台 service worker 监听这些事件,并在触发时执行回调。
https://developer.chrome.com/docs/extensions/mv3/service_workers/
service_worker 跑在一个单独的线程,可以使用插件特有的 API,它本质上也是一个 js 文件,和 content_script 的区别在于:
浏览器插件右上角的小图标点开时弹出的页面称为 popup,其视图本质上是一个 Web 页面。
A DevTools extension is structured like any other extension: it can have a background page, content scripts, and other items. In addition, each DevTools extension has a DevTools page, which has access to the DevTools APIs.
DevTools 扩展的结构与其他任何扩展一样:它可以有一个背景页面(service worker)、内容脚本和其他项目。此外,每个 DevTools 扩展都有一个 DevTools 页面,该页面可以访问 DevTools API。
https://developer.chrome.com/docs/extensions/mv3/devtools/
Devtools 也是插件的一种形式,不同于 popup,它有一组 chrome.devtools 独有的 API,当我们调用 chrome.devtools.panels.create 即可创建一个自定义的面板。
chrome.devtools.panels.create(
// 扩展面板显示名称
"DevPanel",
// 扩展面板icon,并不展示
"panel.png",
// 扩展面板页面
"index.html",
function (panel) {
console.log("自定义面板创建成功!");
}
);
像 Vue Devtools 和 React Devtools 都是这种形式,其视图本质上就是一个 Web 页面。
完整的通信推荐查看:https://juejin.cn/post/7021072232461893639#heading-9
由于 content_script 和 service worker 独立于插件页面,所以时常有消息传递的需求,基本方式:
// 发送方 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
{
"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 文件
}],
}
<!-- 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>Document</title>
</head>
<body>
<div id="root">
<input />
<button>confirm</button>
</div>
<script src="./index.js"></script>
</body>
</html>
// 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 面板。
Devtools 比较特殊,它需要在 manifest.json 中添加 devtools_page 字段指向一个入口 html 文件,在该文件中需要引入一段 js 来创建 devtools 面板,新的目录结构如下:
├── 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 面板:
所以我们需要使用熟悉的前端框架进行开发,这里以 create-react-app 举例(Vue 也差不多),创建一个 React 项目:
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 文件来描述当前应用的目标产物是 extensions 也就是插件产物:
// .eslintrc
{
"env": {
"webextensions": true
}
}
执行 npm run build 后将产物文件夹按上面「如何开发一个自己的插件」安装即可(Devtools 没出来关掉浏览器重试,比较玄学),后续就是正常的 Web 开发流程了,当然如果你想使用一些插件的 API 还是会报错的,这样只适合开发正常 Web 页面逻辑,需要调试插件独有的 API 还是需要 build 后安装再进行调试。
插件的调试稍微有点麻烦,分四个方面:
我们右键点击右上角插件图标 -> 审查弹出内容即可进入插件的 html 页面中进行调试:
F12 打开控制台,选择 Devtools 面板单独打开浏览器进行调试:
然后在此页面中 Mac 系统按住 Comand + Option + i(windows 笔者目前没有,可以自行尝试一下),即可打开 Devtools 的 Devtools(是的就是套娃),此时不仅可以调试自己的插件,还可以调试默认的 element、network 等面板,由此我们也可以得知类似 network 这种面板其实也是上述同样的开发模式并且默认集成在 Chrome 中。
由于 content_script 是注入到目标页面的,所以 F12 打开目标页面的控制台即可进行调试。
进入到插件面板点击 service worker 即可进行调试:
https://github.com/puppeteer/puppeteer
Puppeteer 是一款浏览器自动化工具,可以帮助我们自动控制浏览器的行为,例如打开指定 URL,点击某个 button 等行为。
正常 E2E 测试需要跳转到指定 URL 然后进行测试,Chrome 插件也不例外,所以我们无非是要获取到插件页面的 URL 跳转进去再进行正常 E2E 测试即可。
首先观察插件页面的 URL 组成如下:
`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 };
上述模式在有头模式下本地跑 E2E 测试是没问题的,因为本地有图形化界面,但是如果在 CI 环境比如 Linux 环境中并没有图形化界面,此时 headless:false 也就是有头模式会直接报错导致测试不通过,所以我们需要在没有图形化界面的环境中模拟一套图形化界面,这时我们就可以用到 xvfb。
Xvfb 在一个没有图像设备的机器上实现了 X11 显示服务的协议。它实现了其他图形界面都有的各种接口,但并没有真正的图形界面。所以当一个程序在 Xvfb 中调用图形界面相关的操作时,这些操作都会在虚拟内存里面运行,只不过你什么都看不到而已。
https://zhuanlan.zhihu.com/p/350944759
简单来说就是让浏览器以为自己跑在有图形界面的系统中,从而让有头模式正常运行。
首先就需要在 CI 环境中安装 xvfb,一般我们都是配置一份 pipeline 脚本来做,例如:
// 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 环境下也可以跑通用例了~
✅ 至此开发一个 Chrome 插件的整个生命周期基本都已覆盖。
本文代码:https://github.com/nyqykk/hello-extensions-react
本文由微信公众号字节前端 ByteFE原创,哈喽比特收录。
文章来源:https://mp.weixin.qq.com/s/qPeZZPwUGX2eRi4Q1AGfcw
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。