阿里妹导读
本文从浏览器架构演进、插件运行机制、插件基本介绍和一些常见的插件实现思路几个方向聊聊Chrome插件。
浏览器架构演进
单进程浏览器时代
单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、JAVA 运行环境、渲染引擎和页面等。在 2007 年之前,市面上浏览器都是单进程的。
单进程浏览器的架构
很多功能模块运行在一个进程里,是导致单进程浏览器不稳定、不流畅和不安全的一个主要因素。
多进程浏览器时代
早期架构
2008 年 Chrome 发布时的进程架构
从图中可以看出,早期的架构已经对浏览器的能力进行了拆分,主要拆分为三类:浏览器进程、插件进程和渲染进程。每个页面是运行在单独的渲染进程中的,同时页面里的插件也是运行在单独的插件进程之中,进程之间是通过 IPC 机制进行通信。
这就解决了单进程时代浏览器的各种问题:
解决不安全:Chrome把插件进程和渲染进程锁在沙箱里面,沙箱里面的程序可以运行,但是不能在硬盘上写入任何数据,也不能在敏感位置读取任何数据,这样即使在渲染进程或者插件进程里面执行了恶意程序,恶意程序也无法突破沙箱去获取系统权限。
相较之前,近期的架构又有了很多新的变化。
近期Chrome进程架构
从图中可以看出,最新的 Chrome 浏览器包括:1 个浏览器主进程、1 个 GPU 进程、1 个网络进程、多个渲染进程和多个插件进程。
插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
目前Chrome浏览器的架构正在发生一些改变,称为面向服务的架构(SOA),目的是将和浏览器本身(Chrome)相关的部分拆分为一个个不同的服务,服务化之后,这些功能既可以放在不同的进程里面运行也可以合并为一个单独的进程运行。
这样做的主要原因是让Chrome在不同性能的硬件上有不同的表现。当Chrome运行在一些性能比较好的硬件时,浏览器进程相关的服务会被放在不同的进程运行以提高系统的稳定性。相反如果硬件性能不好,这些服务就会被放在同一个进程里面执行来减少内存的占用。
面向服务的架构
插件运行机制
在运行机制前,我们先来回顾一下打开页面会发生什么:
打开页面发生了什么
浏览器进程接收到确认消息后更新浏览器界面状态:安全、地址栏url、前进后退的历史状态、更新web页面;
打开插件发生了什么
插件的运行相较于页面会有简化
浏览器进程在这里更多起到桥梁作用,作为中转可以实现Extension Page和content_.js之间的消息通信。
插件基本介绍
版本发展
chrome插件存在三个版本,分别是Manifest V1、Manifest V2和Manifest V3。
其中MV1版本已经被废弃了,目前市面上存在MV2和MV3版本,以MV2为主流,在被MV3慢慢取代。
时间线:
Manifest V2新特性
https://developer.chrome.com/docs/extensions/mv2/manifestVersion/#manifest-v1-changes
execute的变化,不能再执行任意字符串,只能执行脚本文件和函数;
// 其他页面,比如content-或者popup中存储数据chrome.runtime.onMessage.addListener(({ type, name }) =>{if(type=== "set-name") {saveUserName = name;}});
// 点击popup时展示数据chrome.action.onClicked.addListener((tab) =>{// 这里saveUserName可能为空字符串console.log(saveUserName, "saveUserName");});
因此在V3中,需要对这种全局变量数据进行改造,改造的方式也很简单,就是将数据持久化保存到storage中,需要用到的地方随用随取:
chrome.action.onClicked.addListener(async(tab) => {const{ name } = awAItchrome.storage.local.get(["name"]);chrome.tabs.sendMessage(tab.id, { name });});
从Manifest V1到Manifest V2,可以看到Chrome想提高插件的隐私和安全,同时也优化了不少API。
而Manifest V3除了安全性更完善外,还在性能上下了功夫。Manifest V3 的核心非常明确,就是限制扩展对系统资源的使用。一直以来高资源占用都是 Chrome 为人诟病的痛点,而且扩展由于在后台运行,如果出现问题,更是难以定位和管理。
虽然增加了诸多限制,但Manifest V3还是有优点的:
这些变化可以让 Chrome 变得更加流畅,对于用户来说是好事。
展示形式
Chrome插件有以下常见的8中展现形式:
browserAction(浏览器右上角)
在浏览器右上角扩展程序一栏显示,包含一个图标、名称和popup
山海关插件popup
pageAction(地址栏右侧)
pageAction指的是在当某些特定页面打开才显示的图标。在早些版本的Chrome是将pageAction放在地址栏的最右边,左键单击弹出popup,右键单击则弹出相关默认的选项菜单。而新版的Chrome更改了这一策略,pageAction和普通的browserAction一样也是放在浏览器右上角,只不过没有点亮时是灰色的,点亮了才是彩色的,灰色时无论左键还是右键单击都是弹出选项。
右键菜单
通过开发Chrome插件可以自定义浏览器的右键菜单,主要是通过chrome.contextMenus API实现,右键菜单可以出现在不同的上下文,比如普通页面、选中的文字、图片、链接,等等。
掘金插件右键菜单
override(覆盖特定页面)
使用override可以将Chrome默认的一些特定页面替换掉,改为使用扩展提供的页面。
扩展可以替代如下页面:
书签:浏览器的书签,或者直接输入 chrome://bookmarks
掘金插件替换了新标签页
devtools(开发者工具)
Chrome允许插件在开发者工具(devtools)上开发,主要表现在:
React Developer Tools
option(选项页)
插件的设置页面,可以在右上角入口右键,有一个选项标签。
omnibox
omnibox是向用户提供搜索建议的一种方式,可以在搜索栏输入特定的标识然后按Tab进入搜索。
JSON Viewer插件
桌面通知
Chrome提供了一个chrome.notificationsAPI以便插件推送桌面通知,暂未找到chrome.notifications和HTML5自带的Notification的显著区别及优势。
在后台JS中,无论是使用chrome.notifications还是Notification都不需要申请权限(HTML5方式需要申请权限),直接使用即可。
核心介绍
manifest.json
这是一个Chrome插件最重要也是必不可少的文件,用来配置所有和插件相关的配置,必须放在根目录。其中,manifest_version、name、version3个是必不可少的。
Manifest V2
{// 清单文件的版本,这里先使用2演示"manifest_version": 2,// 插件的名称"name": "...",// 插件的版本"version": "1.0.0",// 插件描述"deion": "...",// 图标,一般偷懒全部用一个尺寸的也没问题"icons": {"16": "img/icon.png","48": "img/icon.png","128": "img/icon.png"},// 会一直常驻的后台JS或后台页面"background": {"s": ["js/background.js"]},// 浏览器右上角图标设置,browser_action、page_action、App必须三选一"browser_action": {"default_icon": "img/icon.png","default_title": "...","default_popup": "popup.html"},// 当某些特定页面打开才显示的图标"page_action": {"default_icon": "img/icon.png","default_title": "...","default_popup": "popup.html"},// 需要直接注入页面的JS"content_s": [{"matches": ["<all_urls>"],"js": ["js/content-.js"],"css": ["css/custom.css"],// 代码注入的时机,document_start, document_end, document_idle,默认document_idle"run_at": "document_start"},],// 权限申请"permissions": ["contextMenus", // 右键菜单"tabs", // 标签"notifications", // 通知"webRequest", // web请求"webRequestBlocking","storage", // 插件本地存储"https://*/*"// 可以通过execute或者insertCSS访问的网站],// 普通页面能够直接访问的插件资源列表,如果不设置是无法直接访问的"web_accessible_resources": ["js/inject.js"],"homepage_url": "...", // 插件主页"chrome_url_overrides": { // 覆盖浏览器默认页面"newtab": "newtab.html"},"options_ui": { // 插件选项页"page": "options.html","chrome_style": true},"omnibox": { "keyword": "..."}, // 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字"default_locale": "zh_CN", // 默认语言"devtools_page": "devtools.html", // devtools页面入口,注意只能指向一个HTML文件,不能是JS文件"content_security_policy": "...", // 安全策略"web_accessible_resources": [ // 可以加载的资源RESOURCE_PATHS]}Manifest V3(仅展示与V2版本的不同点)
{"manifest_version": 3,"background": {"service_worker": js/background.js"},"action": { //browser_action 和 page_action,统一为 Action"default_icon": "img/icon.png","default_title": "这是一个示例Chrome插件","default_popup": "popup.html"}"content_security_policy": {"extension_pages": "...","sandbox": "..."},"web_accessible_resources": [{"resources": [RESOURCE_PATHS]}]}content-s
是Chrome插件中向页面注入脚本的一种形式(虽然名为,其实还可以包括css的),借助content-s我们可以实现通过配置的方式轻松向指定页面注入JS和CSS。
content-s和原始页面共享DOM,但不共享JS。如要访问页面JS(例如某个JS变量),只能通过injected js来实现。content-s不能访问绝大部分chrome API,除了下面这4种:
chrome.extension
chrome.i18n
chrome.runtime
这些API绝大部分时候都够用了,有需要调用其它API的话,可以通过通信让background或service worker来帮忙调用
background
后台是一个常驻的页面,它的生命周期是插件中所有类型页面中最长的,它随着浏览器的打开而打开,随着浏览器的关闭而关闭,所以通常把需要一直运行的、启动就运行的、全局的代码放在background里面。
background的权限非常高,几乎可以调用所有的Chrome扩展API(除了devtools),而且它可以无限制跨域,可以跨域访问任何网站而无需要求对方设置CORS。
background的概念在MV3版本中变为了service worker,区别在于生命周期变短了,service worker是短暂的基于事件的脚本,所以不适合用来保存全局变量。
popup
popup是点击右上角图标时打开的一个小窗口网页,焦点离开网页就立即关闭,一般用来做一些临时性的交互。权限级别和background差不多,就是生命周期比较短。
injected-
chrome插件中其实没有injected-这一概念,这是开发者们在开发过程中衍生出来的一种概念,指的是通过DOM操作的方式向页面注入的一种JS。
因为content-无法访问页面中的JS,虽然可以操作DOM,但是DOM却不能调用它,也就是无法在DOM中通过绑定事件的方式调用content-中的代码。但是在网页中增加一个按钮来调用插件的能力是一个比较常见的需求,所以诞生了injected-。
插件通信机制
讲通信机制之前,先回顾一下插件中存在的脚本类型。
Chrome插件的JS主要可以分为这5类:injected 、content-、popup js、background js和devtools js。
权限对比
JS种类 |
可访问的API |
DOM访问情况 |
JS访问情况 |
直接跨域 |
injected |
和普通JS无任何差别,不能访问任何扩展API |
可以访问 |
可以访问 |
不可以 |
content |
只能访问 extension、runtime等部分API |
可以访问 |
不可以 |
不可以 |
popup |
可访问绝大部分API,除了devtools系列 |
不可直接访问 |
不可以 |
可以 |
background |
可访问绝大部分API,除了devtools系列 |
不可直接访问 |
不可以 |
可以 |
devtools |
只能访问 devtools、extension、runtime等部分API |
可以 |
可以 |
不可以 |
通过权限对比可以看到,每一种脚本在权限上都不相同,所以各种脚本间的相互通信就非常重要,这也是插件能够实现众多功能的基础。
通信概览
injected |
content |
popup |
background |
|
injected |
- |
window.postMessage |
- |
- |
content |
window.postMessage |
- |
chrome.runtime.sendMessage chrome.runtime.connect |
chrome.runtime.sendMessage chrome.runtime.connect |
popup |
- |
chrome.tabs.sendMessage chrome.tabs.connect |
- |
chrome.extension. getBackgroundPage |
background |
- |
chrome.tabs.sendMessage chrome.tabs.connect |
chrome.extension.getViews |
- |
devtools |
chrome.devtools. inspectedWindow.eval |
- |
chrome.runtime.sendMessage |
chrome.runtime.sendMessage |
一些常见插件的实现思路
埋点日志检测
一般业务中都会进行一些埋点上报,埋点的本质就是发送一些带特定参数的请求,前端本地调试的时候想实时查看埋点信息通常需要去查看上报接口的入参,或者去对应的埋点平台查看,这样非常不方便。
基于这个,我们可以使用插件来帮助我们快速的可视化查看埋点信息:
页面注入小工具
插件的另一个常见用法就是往页面注入一些工具代码,比如去除页面广告工具。
总结
《浏览器工作原理与实践》:https://time.geekbang.org/column/intro/100033601?tab=catalog
《Inside look at modern web browser》:https://developer.chrome.com/blog/inside-browser-part1/
《图解浏览器的基本工作原理》:
https://zhuanlan.zhihu.com/p/47407398
《Welcome to Manifest V3》:https://developer.chrome.com/docs/extensions/mv3/intro/
MDN文档:
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSP
web_accessible_resources:https://developer.chrome.com/docs/extensions/mv2/manifest/web_accessible_resources/
END