Skip to content

理解Manifest V3的影响

浏览器扩展领域正在经历重大变革。谷歌Chrome正引领一场改变扩展工作方式和功能的过渡,其他主要浏览器很可能会效仿。其中一些变化颇具争议,因为它们对许多非常流行的Chrome扩展有着重大影响。这些变化体现在如何重新编写manifest.json文件上(通过定义新的manifest_version 3),因此这些变化通常被称为“manifest v3”。

注意:这一过渡还远未结束,生态系统最终将呈现何种面貌的许多细节仍有待确定。本章主要涵盖确定的信息和示例。

Manifest V3的动机

Manifest v3中包含的所有重大变化都可以归结为以下几个动机:安全、性能、隐私和透明度以及收益。

安全

Manifest v2允许扩展执行从远程URL加载或由用户提供的JavaScript。这被认为是有问题的,因为具有访问扩展API和权限的恶意脚本可能会造成很大的损害。为了解决这个安全漏洞,Manifest v3限制扩展只能执行包含在扩展包本身中的脚本。

性能

Manifest v2中的几个功能可能会在浏览器中引入性能问题。

迁移到DeclarativeNetRequest

Manifest v2中的webRequest API允许开发者在网络请求生命周期的不同点运行JavaScript。当被极端使用时,管理所有页面上所有网络流量的扩展将因此为浏览器发出的每一个网络请求执行阻塞JavaScript。

Manifest v3通过将这些阻塞脚本转换为管理网络流量的静态声明性规则集(阻塞、重定向等)来解决这个问题。浏览器加载这些静态规则并根据需要原生执行它们,从而消除了为每个网络请求运行额外JavaScript的需求。

迁移到Service Workers

在Manifest v2中,后台脚本可以是“持久的”,这意味着脚本永远不会终止。这意味着每个具有持久后台脚本的扩展都会引入一个浏览器必须管理的额外JavaScript运行时。

Manifest v3通过将后台脚本迁移到作为Service Workers运行来解决这个问题。这些Service Workers会根据观察到的浏览器事件按需自动启动,并在没有活动时终止,以释放系统资源。

注意:移除持久后台脚本的做法极具争议,并为需要使用长生命周期实体(如WebSockets)的扩展带来了问题。请继续阅读本章,了解解决这些问题的策略。

隐私和透明度

与移动应用类似,浏览器扩展在允许执行除最基本任务之外的所有任务之前,都需要用户的许可。许多权限一旦被授予,就会提供对敏感信息的无限制访问,如页面内容、Cookie和浏览活动。Manifest v3更改了manifest权限结构,并增加了对扩展正在使用哪些权限的控制和可见性。

收益

众所周知,许多科技巨头严重依赖网络广告带来的收入。一些最受欢迎的浏览器扩展是“广告拦截器”,这些扩展非常擅长阻止用户看到广告,并消除任何潜在的广告收入。尽管移动网络流量占据主导地位,但桌面网络流量仍然是所有网络流量中的重要部分,而且其中很大一部分使用了广告拦截器。

因广告拦截器而损失广告收入的公司,正是那些支持浏览器扩展API和浏览器扩展市场(这些市场推动了广告拦截器的发展)的公司。为了解决这一冲突,Manifest v3瞄准了广告拦截器所依赖的扩展API。Manifest v3中的更改并没有完全消除阻止广告的能力,但这一能力已显著减弱。

后台Service Workers的影响

在Manifest v2中,后台脚本可以定义为持久脚本或事件页面。持久脚本在浏览器保持打开状态的整个过程中都会被初始化并保持活动状态。这允许后台脚本与页面并行运行,在脚本的顶层执行工作,打开如WebSockets这样的长生命周期网络连接,监听来自外部源的传入事件,并在短间隔内执行任务,而无需担心脚本被终止。不需要持久脚本状态的扩展可以选择将后台脚本作为事件页面运行。浏览器会运行后台脚本来初始化浏览器事件的监听器,然后在认为其处于空闲状态时挂起后台脚本。当触发带有处理程序的浏览器事件时,事件页面会唤醒并运行处理程序。

Manifest v3放弃了持久脚本和事件页面的二元性,转而采用Service Workers。在许多方面,后台Service Worker类似于事件页面。即便如此,Manifest v2和Manifest v3中的后台脚本仍有几个关键差异:

DOM

在Manifest v2中,后台脚本是作为无头浏览器页面创建的,具有对DOM的完全访问权限。然而,在Manifest v3中,Service Worker无法访问DOM或任何DOM API。但是,它仍然可以访问OffscreenCanvas API。

XMLHttpRequest

在Manifest v2中,后台脚本可以使用XMLHttpRequest进行网络请求。但在Manifest v3中,由于所有后台脚本都是Service Worker,因此网络请求必须通过fetch()方法进行。

Timer API

在Manifest v2中,持久脚本可以在脚本的顶层使用像setTimeout()或setInterval()这样的Timer API方法,并确保处理程序可靠地执行。以下是一个在具有持久后台的Manifest v2扩展中可靠运行的超时处理程序的示例:

在Manifest v2的持久后台脚本中可靠运行的定时器

javascript
// 5分钟后记录消息
setTimeout(() => console.log("5 minutes is up!"), 5 * 60 * 1000);

在Manifest v3中,由于没有选择持久性的选项,这些处理程序可能无法可靠地执行。浏览器在确定Service Worker是否空闲时,不会考虑这些定时器处理程序。当浏览器决定停止空闲的Service Worker时,这些定时器处理程序将被静默取消。在上面的示例中,如果Service Worker在处理程序执行之前被停止,则日志语句将永远不会打印。

建议的替代方案是使用扩展的Alarms API。这些方法类似于Timer API方法,可以触发定时事件,但这些事件将唤醒Service Worker并执行处理程序。以下是对上面示例的改写,以便在Manifest v3的Service Worker中正确运行:

在Manifest v3的后台Service Worker中可靠运行的闹钟

javascript
// 计划一个在5分钟后触发的闹钟事件
chrome.alarms.create({ delayInMinutes: 5 });
// 为闹钟事件设置处理程序
chrome.alarms.onAlarm.addListener(() => console.log("5 minutes is up!"));

在这个改写的示例中,Service Worker将为闹钟事件唤醒。因此,可以保证处理程序将按预期运行。在比较这两种策略时,您会发现Alarms API对于更高频率的事件来说并不是一个很好的替代方案。

注意:Alarms API在“扩展和浏览器API”章节中有详细介绍。有关处理高频计时器的策略,请参阅“后台脚本”章节。

事件处理程序

由于Service Worker预计会定期启动和停止,因此后台脚本必须以特定方式组织以确保正确行为。在编写Service Worker时,请牢记以下行为:

  • 当service worker停止时,其事件监听器也会随之终止。
  • 当service worker启动时,会添加事件监听器。
  • 当service worker因响应某个事件而启动时,该事件会在其启动后立即被分发给它。

在编写后台service worker时,应假设在设置事件处理程序后可能会立即触发事件。因此,事件处理程序必须在事件循环的第一轮中被注册。以下是一个未在事件循环第一轮中注册处理程序的示例,因此可能会错过service worker重启时分发的点击事件:

可能无法正确处理所有事件的事件处理程序注册

javascript
// 不正确做法
setTimeout(
   () => chrome.action.onClicked.addListener(() => console.log("click")),
  10
);

以下是一个在事件循环第一轮中注册了处理程序的示例,因此能够正确处理service worker重启时分发的事件:

能够正确处理所有事件的事件处理程序注册

javascript
// 正确做法
chrome.action.onClicked.addListener(
  () => console.log("click")
);

提示:通常规则是在后台脚本的顶层注册事件。

Service Worker持久性

在manifest v2中,持久后台脚本会在安装或浏览器启动时初始化,并且会一直运行,直到浏览器关闭或扩展被卸载。在manifest v3中,当浏览器检测到service worker处于空闲状态时,会停止它。

考虑以下扩展,它在后台service worker中设置了一个计时器,以测量其存活时间:

background.js:在service worker停止前持续记录到控制台

javascript
const t0 = performance.now();
setInterval(() => {
  const t1 = performance.now();
  console.log(`已存活 ${Math.round((t1 - t0) / 1e3)}秒`);
}, 1e3);

提示:在Google Chrome中,如果访问chrome://serviceworker-internals/?devtools,您将找到service worker内部界面,该界面允许您实时监控service worker的实时状态以及其控制台输出。与可能会阻止浏览器将service worker识别为空闲的浏览器检查器界面不同,此工具允许service worker进入空闲状态并被停止。

以下截图展示了此扩展的service worker在运行(RUNNING)和已停止(STOPPED)状态(图6-1和图6-2): 6-16-2

即使使用间隔计时器,浏览器也会认为此脚本处于空闲状态。通常,在浏览器停止服务工作者之前,您会看到计时器日志记录30秒。

打开长期连接(如扩展程序消息传递端口或WebSockets)将延迟服务工作者的关闭,但Chrome仍然会在5分钟后停止服务工作者并断开连接。因此,Manifest V3扩展程序的组织应避免依赖后台脚本中的长期连接。

注意:服务工作者的这一特定方面极具争议性。在许多情况下,后台脚本中的长期连接对于扩展程序的正常工作至关重要。 有一些技巧可以强制服务工作者保持运行状态。有关详细信息,请参阅“后台脚本”章节。

全局状态和存储

在使用持久脚本时,Manifest v2扩展程序通常依赖于后台脚本中的全局状态。以下是一个简单的后台脚本示例,该脚本用于计数操作按钮的点击次数:

示例6-1a. Manifest v2的manifest.json

json
{
 "name": "MVX",
 "version": "0.0.1",
 "manifest_version": 2,
 "background": {
 "scripts": ["background.js"],
 "persistent": true
},
 "browser_action": {}
}

示例6-1b. Manifest v2的background.js

javascript
let count = 0;
chrome.browserAction.onClicked.addListener(() => {
 console.log(`Clicked ${++count} times`);
});

然而,当将其转换为Manifest v3时,请考虑以下示例:

示例6-2a. Manifest v3的manifest.json

json
{
 "name": "MVX",
 "version": "0.0.1",
 "manifest_version": 3,
 "background": {
 "service_worker": "background.js"
 },
 "action": {}
}

示例6-2b. Manifest v3的background.js

javascript
let count = 0;
chrome.action.onClicked.addListener(() => {
 console.log(`Clicked ${++count} times`);
});

这段代码将一直正常工作,直到服务工作者停止。届时,全局变量将会重置,因为全局作用域中的任何变量都会丢失。推荐的解决方案是使用Storage API,该API可以在服务工作者停止和启动时保持数据持久性。以下代码展示了如何使用此API对示例进行重构:

示例6-3a. Manifest v3示例的重构manifest.json

json
{
 "name": "MVX",
 "version": "0.0.1",
 "manifest_version": 3,
 "background": {
 "service_worker": "background.js"
 },
 "action": {},
 "permissions": ["storage"]
}

示例6-3b. Manifest v3示例的重构background.js

javascript
chrome.action.onClicked.addListener(() => {
 chrome.storage.local.get(["count"], ({ count = 0 }) => {
 console.log(`Clicked ${++count} times`);
 chrome.storage.local.set({ count });
 });
});

注意:有关Storage的更多详细信息,请参阅“扩展程序和浏览器API”章节。

音频和视频

服务工作者无法在浏览器中播放或捕获媒体流。为了使用这些媒体API,必须使用Chrome扩展页面或内容脚本。

内容安全策略限制的影响

在Manifest v2中,扩展程序被允许从远程来源加载脚本、运行用户提供的脚本以及在浏览器中内联运行脚本。但在Manifest v3中,这些做法被明确禁止。尽管仍然可以在沙盒页面中利用这些脚本,但这些沙盒脚本无法访问扩展API。

以下弹出页面显示了三个脚本实例,当在Manifest v3扩展程序中加载时,它们将引发运行时错误:

  • 使用禁止的eval()popup.js

    javascript
    // eval()是不允许的
    eval(`console.log('foobar');`);
  • 带有禁止脚本的popup.html

    html
    <!DOCTYPE html>
    <html>
    <body>
    <h1>Popup</h1>
    <!-- 内联脚本不允许 -->
    <script>
    console.log("foobar");
    </script>
    <script src="popup.js"></script>
    <!-- 不允许从远程来源加载 -->
    <script
    src="https://code.jquery.com/jquery-3.6.0.js"
    integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk="
    crossorigin="anonymous"
    ></script>
    </body>
    </html>

对于Manifest v3扩展程序,原则很简单:扩展程序中运行的所有JavaScript都必须与扩展程序一起加载。如果您的扩展程序需要使用第三方库,则必须在扩展程序的有效载荷中包含这些库的静态副本。使用Webpack等工具构建扩展程序的开发人员不必担心这一点,因为库已经被打包并包含在内。

注意:由于禁用了所有这些脚本技术,在Manifest v3中,扩展程序用户不再能够在任何扩展程序的运行时提供自己的脚本。因此,这一更改破坏了所有像reasemonkey和Tampermonkey这样的用户脚本扩展。

DeclarativeNetRequest的影响

webRequestDeclarativeNetRequest的变更虽然不严格属于Manifest v3的一部分,但这一转变正在同时进行,其影响同样重大。在使用webRequest时,浏览器会拦截并路由网络流量到扩展程序,然后扩展程序通过JavaScript以编程方式对每个请求进行操作。而DeclarativeNetRequest API则颠倒了这一范式:扩展程序定义一组规则来指示浏览器如何处理每个网络请求,然后浏览器根据这些规则执行请求操作。

例如,以下是一个Manifest v2扩展程序的示例,它使用简单的URL匹配器来阻止加载Google徽标:

示例6-4a. Manifest v2 Google徽标拦截器的manifest.json

json
{
 "name": "MVX",
 "version": "0.0.1",
 "manifest_version": 2,
 "background": {
 "scripts": ["background.js"]
 },
 "permissions": [
 "webRequest",
 "webRequestBlocking",
 "<all_urls>"
 ]
}

示例6-4b. Manifest v2 Google徽标拦截器的background.js

javascript
chrome.webRequest.onBeforeRequest.addListener(
 () => {
 return { cancel: true };
 },
 { urls: ["*://*.google.com/logos/*"] },
 ["blocking"]
);

此扩展将阻止所有匹配 *://.google.com/logos/ 的网络请求。加载扩展并访问 google.com 进行测试。

在 manifest v3 中,此行为可以按如下方式复制:

示例 6-5a. manifest v3 Google 徽标拦截器的 manifest.json

{
 "name": "MVX",
 "version": "0.0.1",
 "manifest_version": 3,
 "permissions": ["declarativeNetRequest"],
 "host_permissions": ["<all_urls>"],
 "declarative_net_request": {
 "rule_resources": [
 {
 "id": "ruleset_1",
 "enabled": true,
 "path": "rules.json"
 }
 ]
 }
}

示例 6-5b. manifest v3 Google 徽标拦截器的 rules.json

[
 {
 "id": 1,
 "priority": 1,
 "action": { "type": "block" },
 "condition": {
 "urlFilter": "*.google.com/logos/*",
 "resourceTypes": ["image"]
 }
 }
]

有关网络的详细信息,请参阅“网络”章节。

再次加载 google.com 以测试徽标拦截器(如图6-3所示)。 6-3

DeclarativeNetRequest API 对广告拦截器施加了一些有问题的限制。Google Chrome 强制执行全局静态规则限制,这意味着浏览器中安装的所有扩展都会共同贡献一个规则总和。在本书撰写之时,该总和为 150,000 条,尽管这个数字可能会根据社区反馈而继续波动。这个数字看起来可能很慷慨,但考虑到仅 uBlock Origin 就定义了大约 100,000 条规则,这个限制很快就会达到。

此外,与 webRequest 所能提供的功能相比,declarativeNetRequest 的请求匹配能力受到了极大的限制:它无法基于有效载荷大小来阻止请求,无法通过注入 CSP 指令来禁用 JavaScript 执行,也无法从发出的请求中剥离 cookie 头。由于这些限制,一些浏览器(如 Firefox)似乎正在定位自己,以在 manifest v3 中保留 webRequest 的功能。

有关 manifest v3 如何影响广告拦截器的更多信息,请参阅以下线程: https://github.com/uBlockOrigin/uBlock-issues/issues/338https://bugs.chromium.org/p/chromium/issues/detail?id=896897

总结

从 manifest v2 到 manifest v3 的过渡对浏览器扩展生态系统有着重大影响。其中,最显著的变化是 service workers 和 declarativeNetRequest,这两者对扩展的行为方式都有着广泛的影响。随着这些变化,扩展变得更加安全和高效,但与此同时,似乎也有一些有价值的功能被完全废弃。

下一章将探讨背景脚本(background scripts)。它将深入介绍背景脚本的工作原理、如何最佳地构建它们,以及它们如何作为浏览器扩展的神经中枢来发挥作用。