原文标题:从浏览器原理出发聊聊Chrome插件
原文作者:阿里云开发者
冷月清谈:
怜星夜思:
2、Manifest V3带来的主要变化及其对插件开发者的影响如何?
3、对于性能较低的硬件系统,Chrome多进程架构会带来哪些潜在问题?
原文内容
阿里妹导读
浏览器架构演进
单进程浏览器时代
-
不稳定:早期浏览器需要借助于插件来实现诸如 Web 视频、Web 游戏等各种强大的功能,但是插件是最容易出问题的模块,并且还运行在浏览器进程之中,所以一个插件的意外崩溃会引起整个浏览器的崩溃。除了插件之外,渲染引擎模块也是不稳定的,通常一些复杂的 JavaScript 代码就有可能引起渲染引擎模块的崩溃。和插件一样,渲染引擎的崩溃也会导致整个浏览器的崩溃。
-
不流畅:所有页面的渲染模块、JavaScript 执行环境以及插件都是运行在同一个线程中的,这就意味着同一时刻只能有一个模块可以执行。如果一个脚本非常耗时,它就会独占整个线程,这样导致其他运行在该线程中的页面没有机会去执行任务,导致整个浏览器失去响应,变卡顿。
-
不安全:当你在页面运行一个插件时,插件可以操作系统资源,如果是个恶意插件,那么它就可以释放病毒、窃取你的账号密码,引发安全性问题。
多进程浏览器时代
早期架构
-
解决不稳定:由于进程是相互隔离的,所以当一个页面或者插件崩溃时,影响到的仅仅是当前的页面进程或者插件进程,并不会影响到浏览器和其他页面。
-
解决不流畅:JavaScript运行在渲染进程中,所以即使JavaScript阻塞了渲染进程,也只会影响当前的渲染页面,并不会影响浏览器和其他页面,因为其他页面的脚本运行在它们自己的渲染进程中。
-
解决不安全:Chrome把插件进程和渲染进程锁在沙箱里面,沙箱里面的程序可以运行,但是不能在硬盘上写入任何数据,也不能在敏感位置读取任何数据,这样即使在渲染进程或者插件进程里面执行了恶意程序,恶意程序也无法突破沙箱去获取系统权限。
近期架构
-
浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。可以理解浏览器进程是一个统一的"调度大师"去调度其他进程,比如我们在地址栏输入url时,浏览器进程首先会调用网络进程。
-
渲染进程:核心任务是将HTML、CSS和JavaScript转换为用户可以交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,默认情况下,Chrome会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
-
GPU进程:其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
-
网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
-
插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
当前架构
插件运行机制
打开页面发生了什么
-
用户新增一个tab,此时系统浏览器进程、渲染进程、GPU 进程、网络进程会被创建好;
-
用户输入url,浏览器进程检查url,组装协议,构成完整的url;
-
浏览器进程通过进程间通信(IPC)把url请求发送给网络进程;
-
网络进程接收到url请求后检查本地缓存是否缓存了该请求资源,如果有则将该资源返回给浏览器进程;
-
如果没有,网络进程向web服务器发起http请求(网络请求);
-
网络进程解析响应流程;
-
检查状态码,非200执行状态码对应的处理逻辑;
-
200响应处理:检查响应类型Content-Type,如果是字节流类型,则将该请求提交给下载管理器,不再进行后续的渲染,如果是html则通知浏览器进程准备渲染进程进行渲染;
-
准备渲染进程
-
浏览器进程检查当前url是否和之前打开的渲染进程根域名是否相同,如果相同,则复用原来的进程,如果不同,则开启新的渲染进程;
-
传输数据、更新状态
-
渲染进程准备好后,浏览器向渲染进程发起“提交文档”的消息,渲染进程接收到消息和网络进程建立传输数据的“管道”;
-
渲染进程接收完数据后,向浏览器发送确认消息;
-
浏览器进程接收到确认消息后更新浏览器界面状态:安全、地址栏url、前进后退的历史状态、更新web页面;
打开插件发生了什么
-
我们打开浏览器,新增一个空白tab页
-
tab栏空白处右键,选择任务管理器,打开任务管理器面板
-
可以看到运行了6个进程,分别是浏览器进程、GPU进程、网络进程、存储进程、渲染进程和扩展进程。
-
扩展进程中运行Extension Page,主要包括backgrount.html和popup.html;
-
backgrount.html中没有任何内容,是通过background.js创建生成,当浏览器打开时,会自动加载插件的background.js文件,它独立于网页并且一直运行在后台,它主要通过调用浏览器提供的API和浏览器进行交互;
-
popup.html有内容的,跟我们普通的web页面一样,由html、css、Javascript组成,它是按需加载的,需要用户去点击地址栏的按钮去触发,才能弹出页面;
-
渲染进程主要运行Web Page,当打开页面时,会将content_script.js加载并注入到该网页的环境中,它和网页中引入的Javascript一样,可以操作该网页的DOM Tree,改变页面的展示效果;
-
GPU进程主要为插件界面的渲染提供硬件能力支持;
-
网络进程主要处理插件中的外部资源请求,比如nexydy插件依赖到一些外部js;
-
存储进程为插件提供本地存储能力,比如使用chrome.storage.local进行持久化存储;
-
浏览器进程在这里更多起到桥梁作用,作为中转可以实现Extension Page和content_script.js之间的消息通信。
插件基本介绍
版本发展
Manifest V2新特性
https://developer.chrome.com/docs/extensions/mv2/manifestVersion/#manifest-v1-changes
-
设置了默认的内容安全策略`script-src 'self'; object-src 'self';`。有关内容安全策略的详细配置,可以参考MDN文档;
-
默认情况下,插件包内的资源不再可供外部网站使用。需要通过清单web_accessible_resources属性将其显式列入白名单;
-
browser action API更改;
-
page action API更改;
-
chrome.extension 代替 chrome.self 来指向插件本身;
-
chrome.extension.getTabContentses和chrome.extension.getExtensionTabs废弃,使用extension.getViews替代;
-
Port.tab废弃,使用runtime.Port替代;
Manifest V3新特性
-
Service worker替换Background Page;
-
网络请求修改废弃webRequest API使用新的 declarativentrequest API 来处理;
-
不再允许执行远程托管的代码,只能执行扩展包内包含的JS;
-
Promises 已经被添加到许多方法中,但仍支持回调作为替代方法;
-
Browser Action API 和 Page Action API被统一为单独的Action API;
-
Web可访问的资源,可以只对指定的站点和扩展可用;
-
内容安全策略(CSP),现在可以为单个对象中的不同执行上下文指定单独的CSP;
-
executeScript的变化,不能再执行任意字符串,只能执行脚本文件和函数;
切换MV3会带来的问题
-
由于background不再支持page页面配置background.html,因此也无法调用window对象上的XMLHttpRequest来构建ajax请求,也就是说我们不能像V2版本一样,在background.html中使用XMLHttpRequest来发送请求了,而是需要使用fetch来获取接口数据;
-
由于service workers是短暂的,在不使用时会终止,这意味着它们在整个插件运行期间会不断的启动、运行和终止,也就是不稳定的;因此我们可能需要对V2中background.js的代码逻辑进行一些改造,以往我们会习惯将一些数据直接存储到全局变量,比如像下面这样:
// V2 background.js let saveUserName = "";
// 其他页面,比如content-script或者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中,需要用到的地方随用随取:
// V3 service worker chrome.runtime.onMessage.addListener(({ type, name }) => { if (type === "set-name") { chrome.storage.local.set({ name }); } });
chrome.action.onClicked.addListener(async (tab) => {
const { name } = await chrome.storage.local.get([“name”]);
chrome.tabs.sendMessage(tab.id, { name });
});
-
由webRequest API切换至declarativentrequest API,很多代码逻辑需要重构;
为什么切换MV3?
-
Service Worker 使扩展不再能常驻后台,让扩展所占用的资源可以被回收,降低了浏览器整体的开销;
-
限制规则的数量,相当于控制了单一扩展在规则计算方面的资源使用上限;
展示形式
browserAction(浏览器右上角)
pageAction(地址栏右侧)

右键菜单
override(覆盖特定页面)
-
历史记录:从工具菜单上点击历史记录时访问的页面,或者从地址栏直接输入 chrome://history
-
新标签页:当创建新标签的时候访问的页面,或者从地址栏直接输入 chrome://newtab
-
书签:浏览器的书签,或者直接输入 chrome://bookmarks
devtools(开发者工具)
-
自定义一个和多个和Elements、Console、Sources等同级别的面板;
-
自定义侧边栏(sidebar),目前只能自定义Elements面板的侧边栏;
option(选项页)
omnibox
桌面通知
核心介绍
manifest.json
{
// 清单文件的版本,这里先使用2演示
"manifest_version": 2,
// 插件的名称
"name": "...",
// 插件的版本
"version": "1.0.0",
// 插件描述
"description": "...",
// 图标,一般偷懒全部用一个尺寸的也没问题
"icons": {
"16": "img/icon.png",
"48": "img/icon.png",
"128": "img/icon.png"
},
// 会一直常驻的后台JS或后台页面
"background": {
"scripts": ["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_scripts": [{
"matches": ["<all_urls>"],
"js": ["js/content-script.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://*/*" // 可以通过executeScript或者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-scripts
-
chrome.extension
-
chrome.i18n
-
chrome.runtime
-
chrome.storage
background
popup
injected-script
插件通信机制
权限对比
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
|
一些常见插件的实现思路
埋点日志检测
页面注入小工具
总结
-
随着浏览器不断的发展,Chrome逐渐把一些基础服务独立出来,类似于一个跨平台的线上操作系统。
-
Chrome插件提供的能力很丰富,比如代码注入、跨域请求、持久化方案、各种通信机制等,开发者可以发挥想象,组装不同能力以适应不同场景的需求,基本可以实现现代web所能支持的所有功能。
-
Chrome插件MV2版本将在24年1月全面废弃,需要尽快迁移至MV3版本。
参考资料:
《浏览器工作原理与实践》: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/
2023阿里云金秋云创季
阿里云开启金秋云创季,将算力普惠到底,点击阅读原文立享数百款爆品专属特惠。