【引子】前端可能是一个日新月异的领域,我们很难了解其中的方方面面。但是,前端系统一般都以浏览器作为运行环境, 对浏览器的进一步理解有助于我们更好地开发前端应用。这也是本文的由来之一,也作为对runtime的一次实例分析。
浏览器架构经历了从单进程浏览器到多进程浏览器的转变。在强调稳定性、平滑性和安全性的同时,进程开始分解为渲染、 GPU、网络和插件等,提高了架构的整洁性。回顾浏览器的架构,需要进一步了解打开页面的过程、页面渲染的过程以及浏览器插件机制。通过整理 Chrome 插件版本的时间表,特别是从 Manifest V1到 Manifest V3的转换,可以对浏览器随时间变化有一个相对全面的理解。
2007年之前的浏览器架构一般是这样的:
单进程浏览器架构在单个操作系统进程中运行整个 Web 浏览器,将网络处理、插件、 JavaScript 运行时、呈现引擎、页面管理和用户界面元素等任务集中在一个执行空间中。在简化资源管理的同时,这种架构设计主要问题有:
单进程浏览器的优势是在一个进程中操作的所有浏览器组件简化了资源管理和协调。单进程浏览器通常表现出较低的内存使用率,有利于资源效率的提升。在一个统一的过程中,任务在同一个过程中按顺序运行。
2008年发布的 Chrome 进程架构是一个多进程浏览器,如下图所示:
早期的浏览器架构将功能划分为三个主要进程程: 浏览器、插件和渲染。每个页面及其插件在专用的渲染和插件进程中独立运行,通过 IPC 进行通信。
进程间通信(IPC)是一种机制,使进程能够在计算机上进行通信和同步操作。它促进了不同程序之间有效的数据交换和协调。关键的 IPC 机制包括共享内存,允许进程通过信号量来同步访问共享的公共内存区域。命名和非命名管道提供了单向通信,Linux 中的 IPC 通常涉及通过共享文件或带信号量的内存共享存储。消息队列支持异步通信,有助于分离发送方和接收方进程。此外,进程可以通过信号进行通信,相互通知特定的事件或请求。socket利用网络协议将 IPC 扩展到不同的机器。
多进程浏览器增强了稳定性,隔离进程可以防止崩溃影响到整个浏览器。页面或插件崩溃只会影响其特定的进程,从而确保了其他页面和浏览器的稳定性。同时,在呈渲染进程中运行 JavaScript 也可以隔离其影响。如果脚本阻塞呈现进程,它只影响当前页,浏览器和其他页不受影响,因为每个页都在其专用渲染进程中运行脚本。另外,Chrome 将插件和渲染进程放在沙箱环境中,限制了数据的读写访问。即使恶意程序在渲染或插件进程中执行,它也不能破坏沙箱以获得系统权限。这是舱壁架构模式的一个具体体现。
沙箱是一个测试环境,它允许用户在不影响整个系统的情况下运行程序或打开文件。在网络安全领域,沙箱分析并执行潜在的恶意代码,检测并减轻威胁。
在近期的 Chrome 浏览器中,其架构由关键进程组成,如下图所示:
浏览器进程管理界面显示、用户交互、子进程协调和提供存储功能。它充当协调其他进程的“调度器”,例如在输入 URL 时调用网络进程。渲染过程将 HTML、 CSS 和 JavaScript 转换为交互式网页,运行 V8引擎。为了安全起见,Chrome 在沙箱模式下为每个选项卡创建了一个单独的渲染进程。
GPU 进程最初是为了3D CSS 效果,后来扩展到绘制网页和 Chrome UI 界面。在 Chrome 的多进程架构中引入,以满足常见的浏览器需求。网络进程独立加载页面网络资源,最初是浏览器进程中的一个模块,现在作为独立进程运行。插件进程ーー管理插件以防止由于插件固有的不稳定性而导致的崩溃影响浏览器和页面。
现代浏览器架构如下图所示:
Chrome 已经采用了面向服务的浏览器架构 ,旨在基于不同的硬件能力提高灵活性和优化性能。主要目标是将与浏览器(Chrome)相关的组件划分为不同的服务,这些功能可以在单独的进程中运行,也可以合并为单个进程。
这种转变背后的主要动机是根据不同硬件的性能来定制 Chrome 的性能。在强大的硬件上,与浏览器进程相关联的服务在单独的进程中运行。在功能不太强大的硬件上,这些服务在相同的进程中运行,有效地减少了内存使用。
现代浏览器使用了延迟加载和缓存等策略来优先考虑性能。浏览器通过渲染进程来显示 Web 内容。关键阶段包括 HTML 解析、 CSS 样式设计、布局创建和绘制,具体步骤如下:
浏览器一个字符一个字符地读取 HTML,标识元素、属性和文本,然后构建表示网页结构的 DOM 树,并确保正确显示 HTML 代码。
CSS 对象模型表达了应用于 HTML 元素的样式,类似于 DOM 树的结构化层次结构,并考虑了样式的特殊性和级联性,允许访问、操作和计算样式。
布局管理器结合 DOM 和 CSS 对象模型形成渲染树,根据内容、填充等确定Box的尺寸,使用各种方法构建具体位置。同时,使用堆叠上下文和 Z 索引处理重叠元素,使用批处理等技术来优化布局变更。最后,在屏幕上绘制元素,在用户交互期间不断更新。
当使用插件时,浏览器的操作比普通网页还要简单。渲染过程负责运行网页,打开页面时,contentscript.js被加载并注入到网页环境中,操作类似于 JavaScript,操作 DOM 树并改变显示。GPU 进程支持渲染插件接口的硬件功能,网络进程管理插件中的外部资源请求,例如,插件依赖于外部 的JS 资源。同时,存储进程为插件提供了本地存储功能,使用chrome.storage.local在chrome扩展中本地存储和检索数据。浏览器进程起到了桥梁的作用,促进了Extension Page和contentscript.js之间的通信。
插件机制的发展过程如下:
总体而言,Chrome 插件(也被称为扩展)已经经历了3个主要版本的版本开发: Manifest V1、 Manifest V2和 Manifest V3。
Manifest V1 (MV1)是 Chrome 扩展清单的初始版本,已经被放弃。Manifest V2 (MV2)是当前 Chrome 扩展中广泛使用的主流版本,它提供了一个健壮的框架,用于构建具有增强浏览器功能的特性和功能的扩展。Manifest V3是最新的版本,正在逐步取代 MV2。引入 MV3是为了解决安全性和性能方面的问题,它强化了更强的安全措施,并促进了扩展开发中的更好性能。从 Chrome 127开始(2024年6月) ,谷歌开始在预稳定版本的 Chrome 中禁用 Manifest V2扩展,鼓励开发者转向 MV3。
Manifest V2 的功能特性:
Manifest V3 的功能特性:
Manifest V3代表了从 V1和 V2的重大转变,受到 Chrome 致力于提高隐私、安全性和扩展的整体性能的驱动。与之前的版本不同,Manifest V3优先考虑资源利用率,解决了人们对 Chrome 历史性的高资源利用率的担忧。其核心目标是通过扩展来限制系统资源消耗,以优化浏览器性能。在施加额外限制的同时,Manifest V3引入了显著的好处。ServiceWorker 功能允许扩展操作,而无需一直驻留在后台。这样可以回收扩展资源,有效地减少总体浏览器开销。对规则计算的限制作为一种控制机制,确保单个扩展不会过度消耗资源。这些改变共同促进了 Chrome 浏览器更加流畅的体验,符合用户对提高浏览器效率的期望。
在从V2迁移到V3的时候,由于缺少用于配置页面背景的 background. html,与 V2版本不同的是,windows 对象上的 XMLHttpRequest 不再适用于 background. html 来构造 AJAX 请求。相反,必须利用提取方法来获取接口数据。
另外,由于service workers 的生命周期很短,并且在非活动期间终止,因此他们在整个插件生命周期中偶尔启动、运行和终止,从而引入不稳定性。在 MV2中,全局变量被用来直接存储数据。为了适应这种情况,需要对 backound.js 中的逻辑进行修改,以提高稳定性和功能性。而且,从 webRequest API 过渡到 statativeNetRequest API 需要大量的代码重构。
manifest.json 文件对于位于根目录中的 Chrome 插件非常重要。它用于配置所有插件设置,其基本参数为 Manif_ version、 name 和 version。
Manifest V2 的一个示例如下:
{
"manifest_version": 2,
// Plugin name
"name": "...",
// Plugin version
"version": "1.0.0",
// Plugin description
"description": "...",
"icons": {
"16": "img/icon16.png",
"48": "img/icon48.png",
"128": "img/icon128.png"
},
// Persistent background JS or background page
"background": {
"scripts": ["js/background.js"]
},
// Browser icon settings :browser_action, page_action, app
"browser_action": {
"default_icon": "img/icon.png",
"default_title": "...",
"default_popup": "popup.html"
},
// Icon displayed only when specific pages are open
"page_action": {
"default_icon": "img/icon.png",
"default_title": "...",
"default_popup": "popup.html"
},
// JS directly injected into pages
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["js/content-script.js"],
"css": ["css/custom.css"],
// Code injection timing, default is document_idle
"run_at": "document_start"
}],
// Permissions requested
"permissions": [
"contextMenus", // Right-click menu
"tabs", // Tabs
"notifications", // Notifications
"webRequest", // Web requests
"webRequestBlocking",
"storage", // Plugin local storage
"https://*/*" // Websites accessible via executeScript or insertCSS
],
// List of plugin resources directly accessible by normal pages "web_accessible_resources": ["js/inject.js"],
"homepage_url": "...", // Plugin homepage
"chrome_url_overrides": { // Override browser default pages
"newtab": "newtab.html"
},
"options_ui": { // Plugin options page
"page": "options.html",
"chrome_style": true
},
"omnibox": { "keyword" : "..." }, // Register a keyword in the address bar for search suggestions, only one keyword can be set
"default_locale": "en", // Default language
"devtools_page": "devtools.html", // Devtools page entry, can only point to an HTML file "content_security_policy": "...", // Security policy
"web_accessible_resources": [ // Loadable resources
"RESOURCE_PATHS"
]
}
Manifest V3的一个示例如下:
{
"manifest_version": 3,
"name": "...",
"version": "1.0.0",
"description": "...",
"icons": {
"16": "img/icon16.png",
"48": "img/icon48.png",
"128": "img/icon128.png"
},
"background": {
"service_worker": "js/background.js"
},
"action": {
"default_icon": "img/icon.png",
"default_title": "...",
"default_popup": "popup.html"
},
"content_security_policy": {
"extension_pages": "...",
"sandbox": "..."
},
"web_accessible_resources": [
{
"resources": ["RESOURCE_PATHS"]
}
],
"permissions": [
"contextMenus",
"tabs",
"notifications",
"webRequest",
"webRequestBlocking",
"storage",
"https://*/*"
],
"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",
"content_security_policy": "...",
"web_accessible_resources": ["RESOURCE_PATHS"]
}
Chrome 插件中的内容脚本通过配置将 JS 和 CSS 注入到指定的页面中。它们与原始页面共享 DOM,但不与 JS 共享。访问页面 JS 变量需要注入 JS。内容脚本无法访问大多数 Chrome API,除了: * chrome.extension * chrome.i18n * chrome.runtime * chrome.storage
对于其他 API,需要与后台或service worker进行通信。
Chrome 扩展中的后台脚本具有最长的生命周期,并且在浏览器打开时连续运行。它拥有广泛的权限,允许访问大多数 Chrome 扩展 API 和跨源请求,而不受 CORS 限制。在 Manifest V3中,后台页被具有较短生命周期和基于事件的执行的服务工作者所替代,这使得它们不适合存储全局变量。
弹出窗口是一个小窗口的网页,出现在点击右上角的图标。当用户在网页之外进行互动时,它会迅速关闭。通常用于临时交互,其权限级别类似于背景,但具有较短的生命周期。
开发者在 Chrome 插件开发过程中创造了“注入脚本”这个术语。它表示通过 DOM 操作注入到页面中的 JavaScript。内容脚本虽然能够操作 DOM,但由于访问限制,DOM 不能直接调用它。这种限制在事件绑定中是显而易见的。为了满足在 Web 页面中添加一个按钮来触发插件的常见需求,大家采用了插入脚本。
在 Chrome 插件中,通信依赖于五种类型的脚本:
每个脚本拥有不同的权限,强调了它们之间通信的重要性。这种交互对于启用广泛的插件功能非常重要。
温故而知新,浏览器架构作为现代互联网的基石,历经多次迭代与创新,始终承载着用户与网页内容之间的桥梁作用。回顾其发展历程,从早期的单一渲染引擎到如今的多进程、多线程架构,每一次变革都带来了更为流畅、安全的浏览体验。展望未来,浏览器架构将继续深化其性能优化与安全性提升,为用户带来更加出色的网络浏览体验。