chrome 插件开发指南(Manifest V3) - 掘金
一、什么是 Chrome插件
1.1 概述
严格来讲,我们正在说的东西应该叫 Chrome 扩展(Chrome Extension),真正意义上的 Chrome 插件是更底层的浏览器功能扩展,需要对浏览器源码有一定掌握才有能力去开发。鉴于 Chrome 插件的叫法已经习惯,本文中也全部采用这种叫法。在百度指数里也没有收录“chrome 扩展”这个词,只有“chrome 插件”。
Chrome 插件是一个用 Web 技术开发、用来增强浏览器功能的软件,它其实就是一个由HTML、CSS、JS、图片等资源组成的一个.crx后缀的压缩包。
另外,其实不只是前端技术,Chrome 还可以配合 C++ 编写的 dll 动态链接库实现一些更底层的功能(NPAPI),比如全屏幕截图等。
由于安全原因,Chrome 浏览器42以上版本已经陆续不在支持 NPAPI 插件,取而代之的是更安全的 PPAPI。
1.2 Chrome 插件是如何工作的
扩展是基于诸如 HTML、 JavaScript 和 CSS 之类的 Web 技术构建的。它们运行在一个独立的沙箱执行环境中,并与 Chrome 浏览器进行交互。
从图中可以看出,存在三个进程: 扩展进程(Extension Process)、 页面渲染进程(Render Process)、 浏览器进程(Browser Process)。
1)扩展进程中运行Extension Page,Extension Page主要包括backgrount.html和popup.html:
- backgrount.html中没有任何内容,是通过background.js创建生成,当浏览器打开时,会自动加载插件的background.js文件,它独立于网页并且一直运行在后台,它主要通过调用浏览器提供的API和浏览器进行交互;
- popup.html则不同,它有内容,是一个实实在在的页面,和我们普通的web页面一样,由html、css、Javascript组成,它是按需加载的,需要用户去点击地址栏的按钮去触发,才能弹出页面。
2)渲染进程主要运行Web Page,当打开页面时,会将content_script.js加载并注入到该网页的环境中,它和网页中引入的Javascript一样,可以操作该网页的DOM Tree,改变页面的展示效果;
3)浏览器进程在这里更多起到桥梁作用,作为中转可以实现Extension Page和content_script.js之间的消息通信。
1.3 延伸阅读:插件和扩展的区别
扩展(Extension)指的是通过调用 Chrome 提供的 Chrome API 来扩展浏览器功能的一种组件,工作在浏览器层面,使用 HTML + Javascript 语言开发。比如著名的 AdBlock plus。
插件(Plug-in)指的是通过调用 Webkit 内核 NPAPI 来扩展内核功能的一种组件,工作在内核层面,理论上可以用任何一种生成本地二进制程序的语言开发,比如C/C++、Delphi等。比如Flash player插件,就属于这种类型。一般在网页中 object 或者 embed 标签声明的部分,就要靠插件来渲染。
从安全性上来看,由于插件一般实现的都是比较底层的功能,所以一旦出现问题,往往就会牵涉到整个操作系统,像 Flash 就属于经常被扒出高危漏洞的那一类。相比较之下,扩展出现问题,其危害性往往类似于浏览器漏洞。不过 Chrome Extension 在为用户带来便利的同时,也的确带来了不少安全问题,即便是在 Chrome 应用商店中的应用也不能保证绝对安全,Google 自己也下线过一些有安全隐患的扩展。
二、Chrome 插件能做什么
扩展允许你通过使用 API 修改浏览器行为和访问网页内容来“扩展”浏览器。
扩展 API 允许扩展的代码访问浏览器本身的特性: 激活选项卡、修改网络请求等等。
插件能力概述:
API | 备注 | ||
---|---|---|---|
自定义扩展用户界面 | 控制一个扩展的显示的图标 | Action | |
添加触发操作的键盘快捷键 | Commands | ||
添加页面右键菜单 | Context Menus | ||
向地址栏添加关键字功能 | Omnibox | ||
创建新标签卡、书签页或历史记录页 | Override Pages | ||
在工具栏中动态显示图标。 | Page Actions | ||
构建扩展工具 | 无障碍扩展服务 | Accessibility (a11y) | |
有趣的事情发生时,做出检测和反应 | Service Workers | ||
使用语言和语言环境 | Internationalization | ||
获得OAuth2访问令牌 | Identity | ||
管理已安装和正在运行的扩展插件 | Management | ||
通过 Content Script 与其父扩展进行通信 | Message Passing | ||
让用户自定义扩展 | Options Pages | ||
修改一个扩展的权限 | Permissions | ||
存储和检索数据 | Storage | ||
修改和监听 Chrome 浏览器 | 创建、组织和操作书签行为 | Bookmarks | |
从用户的本地配置文件中删除浏览数据 | Browsing Data | ||
以编程方式启动、监视、操作和搜索下载 | Downloads | ||
管理 Chrome 的字体设置 | Font Settings | ||
与浏览器访问页面的记录交互 | History | ||
控制 Chrome 的隐私特性 | Privacy | ||
管理 Chrome 的代理设置 | Proxy | ||
从浏览会话查询和还原选项卡和窗口 | Sessions | ||
在浏览器中创建、修改和重新排列选项卡 | Tabs | ||
访问用户访问次数最多的 URL | Top Sites | ||
更改浏览器的整体外观 | Themes | ||
在浏览器中创建、修改和重新排列窗口 | Windows | ||
修改和监听网页 | 扩展临时访问当前活动选项卡的权限 | Active Tab | |
自定义网站特性,如 cookies、 JavaScript 和插件 | Content Settings | ||
在网页上下文中运行 JavaScript 代码 | Content Scripts | ||
浏览和修改浏览器的 Cookie 系统 | Cookies | ||
使用 XMLHttpRequest 从远程服务器发送和接收数据 | Cross-Origin XHR | ||
在不需要许可的情况下对页面内容执行操作 | Declarative Content | ||
捕获屏幕、单个窗口或选项卡的内容 | Desktop Capture | ||
将选项卡的源信息保存为 MHTML | Page Capture | ||
与标签页互动媒体流交互 | Tab Capture | ||
接收 in-flight 导航请求状态的通知 | Web Navigation | ||
提供规则告诉 Chrome 如何拦截、阻止或修改 in-flight 的请求。 | Declarative Net Request | ||
打包、部署和更新 | 使用 Chrome Web Store 托管和更新扩展 | Chrome Web Store | |
在指定的网络或其他软件上分发扩展 | Other Deployment Options | ||
扩展 Chrome DevTools | 测试网络交互,调试 JavaScript,修改 DOM 和 CSS | Debugger | |
为 Chrome 开发工具添加功能 | Devtools |
几个插件例子:
Google 翻译 | Adblock Plus | Artemis and Britomartis | Octotree - GitHub code tree) |
一句话总结: Chrome扩展插件是用前端的技术栈,来定制浏览器的功能,改善用户体验。
三、主要构成
一个chrome插件通常由3类文件组成:
3.1 Manifest.json
每一个扩展都有一个json格式的清单文件,用于配置扩展的名称、版本号、图标、权限、脚本路径等信息;
文件内容如下所示:
{"manifest_version":3,"name":"MStars","description":"A chrome extension for sgfe","options_page":"options.html","background":{"service_worker":"background.bundle.js"},"action":{"default_popup":"popup.html","default_icon":"master-34.png"},"chrome_url_overrides":{"newtab":"newtab.html"},"icons":{"128":"master.png"},"content_scripts":[{"matches":["http://*/*","https://*/*","<all_urls>"],"js":["contentScript.bundle.js"],"css":["content.styles.css"]}],"web_accessible_resources":[{"resources":["injectScript.bundle.js","content.styles.css","master.png","master-34.png","vs/*"],"matches":["http://*/*","https://*/*","<all_urls>"]}],"permissions":["webRequest","storage","contextMenus","bookmarks"],"host_permissions":["<all_urls>"],"content_security_policy":{"extension_pages":"script-src 'self';object-src 'none'"}}复制代码
3.1.1 Manifest V2
自2022年1月17日起,Chrome 应用商店已经停止接受新的 Manifest V2扩展。
2023年6月 Chrome 115开始,关闭对 Manifest V2扩展的支持。
2024年1月,Chrome 应用商店将删除所有的 Manifest V2扩展。
(Manifest V2 support timeline)
3.1.2 Manifest V3
2020年年底推出V3版本,在安全性、隐私性和性能方面得到了增强;还可以使用更现代的开放 Web 技术,比如 service works和 promises。
Manifest V3 is available beginning with Chrome 88, and the Chrome Web Store begins accepting Manifest V3 extensions in January 2021.
详细文档: developer.chrome.com/docs/extens…
3.2 popup
3.2.1 概述
popup 是点击插件图标时打开的一个页面,点击 popup 之外的区域会收起,一般用来做一些临时性的交互。
(Web Vitals)
3.2.2 简单例子
1、 创建 manifest.json 文件:
{"manifest_version":3,"name":"Hello Extensions","description":"Base Level Extension","version":"1.0","action":{"default_popup":"hello.html","default_icon":"hello_extensions.png"}}复制代码
2、创建 html 文件:
<html><body><h1>Hello Extensions</h1></body></html>复制代码
3.3 content-scripts
3.3.1 概述
content-scripts 是在网页上下文中运行的文件。通过使用标准的文档对象模型(Document Object Model,DOM) ,它们能够读取浏览器访问的网页的详细信息,对它们进行更改。
如主题、样式、布局定制、广告拦截等等。
大部分浏览器插件都在围绕 content-scripts 做一些事情,background、popup、options等都为之服务。
配置示例:
"content_scripts":[{"matches":["http://*/*","https://*/*","<all_urls>"],"js":["contentScript.bundle.js"],"css":["content.styles.css"]}],复制代码
3.3.2 特性
1)和原始页面共享 DOM,但是不能共享 JS,不能访问页面中的 JS(比如变量)。
2)网络请求受到同源策略限制。
3)只能访问以下 Chrome API:
其他 API 都不能直接访问,但是可以通过通信让 background 来进行调用。
3.3.3 简单例子
在页面中增加一个按钮。
functioninJectBtn() {if(document.querySelector('#openInIDE'))return;document.querySelector('.repo-detail-base-info .btn-box')
.insertAdjacentHTML('beforeend','<button id="openInIDE" type="button" class="mtd-btn mtd-btn-warning"><span><div class="mtd-button-content"><span class="mtdicon mtdicon-link-o"></span><span>Open In WebIDE</span></div></span></button>');document.querySelector('#openInIDE').addEventListener('click',() =>{window.open('https://xxx.com/');
});
}functioninit() {varheartBeat =setInterval(() =>{varwantedEl =document.querySelector('.repo-detail-base-info .btn-box');if(wantedEl) {clearInterval(heartBeat);inJectBtn();
}
},1000);
}init();复制代码
3.3.4 延伸阅读:如何访问页面中的 JS
content-scripts 不能访问页面中的 js,它可以操作 DOM,但是 DOM 却不能调用它,所以就有了通过 DOM 操作的方式向页面动态注入 JS 的操作。
functioninjectJs(jsPath) {
jsPath = jsPath ||'injectScript.bundle.js';vartemp =document.createElement('script');
temp.setAttribute('type','text/javascript');// chrome-extension://mapfodeofmlldcgdgahpjiefememgeei/injectScript.bundle.jstemp.src= chrome.runtime.getURL(jsPath);
temp.onload=function() {// 放在页面不好看,执行完后移除掉this.parentNode?.removeChild(this);
};document.head.appendChild(temp);
}复制代码
injected 的内容需要在资源列表中进行声明:
"web_accessible_resources":[{"resources":["injectScript.bundle.js",],"matches":["http://*/*","https://*/*","<all_urls>"]}]复制代码
例子:拦截 xhr 请求。
// "injectScript.bundle.js"xhook.after(function(request, response) {if(request.url.match(/rest\/api.+files.+/)) {console.log(response?.data)
}
});复制代码
3.4 background
3.4.1 概述
插件是基于事件的用来修改或增强 Chrome 浏览体验的程序。事件是浏览器触发的,例如导航到新页、删除书签或关闭选项卡。插件在 background 中监视这些事件,然后根据指定的指令进行响应。
background 一旦加载完成,只要执行某个操作(比如发起网络请求或调用 Chrome API)就会一直运行,此外,在关闭所有可见视图和所有消息端口之前,不会被卸载。
总而言之,background 为所有的视图和消息端口服务。
background 在需要的时候被加载,空闲的时候被卸载。一些事件的例子包括:
- 插件首次安装或者更新版本;
- backgroud 正在监听一些事件,这些事件被触发;
- content script 或者其他扩展发送消息;
- 插件中的其他视图调用 runtime.getBackgroundPage
3.4.2 简单例子
创建一个右键菜单。
functioncreateContextMenus() {
chrome.contextMenus.create({type:'normal',id:'savePage',title:'保存页面',checked:false,
});
}
chrome.runtime.onInstalled.addListener(() =>{createContextMenus();
});复制代码
四、其他展现形式
4.1 homepage_url
插件主页,免费广告位!
{"homepage_url":"https://km.xxxx.com", }复制代码
4.2 Options页面
4.2.1 概述
4.2.2 配置
有两种配置方法,对应两种展现形式。
{// 方式1"options_page":"options.html",// 方式2 优先级高"options_ui":{"page":"options.html"},}复制代码
4.3 Devtools
开发者工具。
4.4 override(覆盖特定页面)
可以将 Chrome 默认的一些特定的页面改为使用扩展提供的页面:
页面名称 | url | 配置 | 备注 |
---|---|---|---|
新标签页 | chrome://newtab | "chrome_url_overrides": { "newtab": "newtab.html" } | |
历史记录 | chrome://history | "chrome_url_overrides": { "history": "history.html" } | |
书签 | chrome://bookmarks | "chrome_url_overrides": { "bookmarks": "bookmarks.html" } |
注意点:
- 不能替换隐身模式下的新标签页;
- 一个插件只能覆盖一个页面。
4.5 Omnibox
Chrome和其他浏览器相比一个最大的区别就是地址栏——其实不仅仅是地址栏,而是一个多功能的输入框,Google将其称为omnibox(中文为“多功能框”)。我们熟悉的一个功能就是用户可以直接在omnibox搜索关键字,Chrome也将omnibox开放给开发者,这使得omnibox更加强大。
五、通讯
由于 content script 运行在网页的上下文中,而不在扩展中,因此它们通常需要某种方式来与扩展的其余部分进行通信。
5.1 简单的一次性请求
5.1.1 从 content-scripts 发起请求
chrome.runtime.sendMessage({greeting:"hello"},function(response) {console.log(response.farewell);
});复制代码
5.1.2 发送消息到 content-scripts
chrome.tabs.query({active:true,currentWindow:true},function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {greeting:"hello"},function(response) {console.log(response.farewell);
});
});复制代码
5.1.3 接受消息
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {console.log(sender.tab?"from a content script:"+ sender.tab.url:"from the extension");if(request.greeting==="hello")sendResponse({farewell:"goodbye"});
}
);复制代码
5.2 长链接
可以使用 runtime.connect和 tabs.connect建立一个长链接进行通讯。
// port1varport = chrome.runtime.connect({name:"knockknock"});
port.postMessage({joke:"Knock knock"});
port.onMessage.addListener(function(msg) {if(msg.question==="Who's there?")
port.postMessage({answer:"Madame"});elseif(msg.question==="Madame who?")
port.postMessage({answer:"Madame... Bovary"});
});// port2chrome.runtime.onConnect.addListener(function(port) {console.assert(port.name==="knockknock");
port.onMessage.addListener(function(msg) {if(msg.joke==="Knock knock")
port.postMessage({question:"Who's there?"});elseif(msg.answer==="Madame")
port.postMessage({question:"Madame who?"});elseif(msg.answer==="Madame... Bovary")
port.postMessage({question:"I don't get it."});
});
});复制代码
5.3 跨插件通讯
除了在插件中的不同组件之间发送消息之外,还可以使用消息传递 API 与其他插件进行通信。
可以使用 runtime.onMessageExternal和 runtime.onConnectExternal来监听传入的请求和连接。
监听消息:
// For simple requests:chrome.runtime.onMessageExternal.addListener(function(request, sender, sendResponse) {if(sender.id=== blocklistedExtension)return;// don't allow this extension accesselseif(request.getTargetData)sendResponse({targetData: targetData});elseif(request.activateLasers) {varsuccess =activateLasers();sendResponse({activateLasers: success});
}
});// For long-lived connections:chrome.runtime.onConnectExternal.addListener(function(port) {
port.onMessage.addListener(function(msg) {// See other examples for sample onMessage handlers.});
});复制代码
发送消息:
// The ID of the extension we want to talk to.varlaserExtensionId ="abcdefghijklmnoabcdefhijklmnoabc";// Make a simple request:chrome.runtime.sendMessage(laserExtensionId, {getTargetData:true},function(response) {if(targetInRange(response.targetData))
chrome.runtime.sendMessage(laserExtensionId, {activateLasers:true});
}
);// Start a long-running conversation:varport = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);复制代码
5.4 从网页发送信息
插件可以接收和响应来自常规网页的消息。要使用这个特性,必须在 Manifent.json 中指定要与哪些网站通信。
通讯方式和跨插件通讯方式类似。
5.5 Native通讯
Chrome 插件可以与原生应用进行通讯,原生应用可以通过注册一个 Native Messaging Host,Chrome 以一个单独的进程启动 Host,并使用标准输入和标准输出流进行通信。
详情可参考: developer.chrome.com/docs/apps/n…
六、如何查看某个插件的源码
- 开源的, 如 github.com/adblockplus…
- 本地查看
七、最佳实践推荐
喜欢使用 React + TS:
github.com/chibat/chro…
github.com/lxieyang/ch…
喜欢使用 rxjs:
github.com/alibaba/bro…
不想要任何框架:
github.com/SimGus/chro…
八、适配其他浏览器
切换到 chromium 内核的浏览器适配工作还是比较小的,firefox 也支持 chrome API。
我们只需要对照各浏览器厂商提供的开发文档,关注我们用到的 API 是否有差异,有差异的部分,做一下兼容即可。
可参考各浏览器的 extension 开发文档:
firefox: developer.mozilla.org/en-US/docs/…
Edge: learn.microsoft.com/zh-cn/micro…
360: open.se.360.cn/open/extens…
九、发布
chrome开发者中心: chrome.google.com/webstore/de…
firefix发布文档: addons.mozilla.org/en-US/devel…
edge发布文档: learn.microsoft.com/zh-cn/micro…
十、未来
- content-sctipts + Mockjs 自动填充表单?
- UI检查,自动化测试?
- 性能、依赖检测,Lighthouse?
- 账号管理&一键登录?
可以在评论区说说你们用到的场景~~
参考
chrome 插件开发指南(字节跳动技术团队)
Chrome Extension 扩展程序迁移至 Manifest V3
Chromium扩展(Extension)的Content Script加载过程分析
一种开发 Chrome 扩展程序的新姿势
Message passing
Native Messaging