Browser Extension v3 必备概念

extension
Created 9/17/2022
Updated 4/17/2023

Browser Extension v3 必备概念

扩展程序由多个部分组成,以下列出了开发相关的必备概念

通用技术

Service Workers

在 Manifest V2 版本中,后台页面 background pages 是扩展程序中的一个独立页面,一般设置事件监听,以响应用户的操作,但是它会长期驻留后台影响性能。在 Manifest V3 版本中,后台脚本迁移到 Service Workers 中运行以提供性能,其中有两个特点:

  • Service Workers 在执行事件处理函数后终止,并在新的事件触发下重新运行
  • 它是一种 JavaScript Worker无法直接访问 DOM。
提示

Service Worker 是浏览器完全独立于网页运行的脚本。除了以上的特点,还要注意以下的相关事项:

  • 它是一种可编程网络代理,让您能够控制页面所发送网络请求的处理方式
  • 它广泛地利用了 promise 进行异步操作

如果需要使用 service workers,则需要先在配置清单 manifest.json 的选项 background.service_worker 中声明/注册,该选项的属性值指定需要执行的一个 JavaScript 文档的路径(它必须在项目的根目录下)。

manifest.json
json
{
  // ...
  "background": {
    "service_worker": "background.js"
  },
}

后台脚本在 Service Workers 中运行基于监听事件-响应模型来执行操作,因此逻辑代码的编写也应该遵循该模型以优化性能:

  • 在事件循环的第一轮中完成事件监听的注册,即将事件监听程序写在后台脚本最顶层的作用域中,而不应该内嵌在其他的逻辑代码中(因为 Service Workers 执行完代码会终止而不会长期驻留,当有事件需要分派时它才再次运行,如果未能在第一次事件轮询中注册监听器,这就无法响应事件)。
    background.js
    js
    chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
      chrome.action.setBadgeText({ text: badgeText });
    });
    
    // Listener is registered on startup
    chrome.action.onClicked.addListener(handleActionClick);
    
    function handleActionClick() {
        // ...
    }
    
  • 由于 Service Workers 生命周期是短期的,如果需要持久化的数据(例如需要将用户输入的内容作为变量),可以使用 Chrome 为扩展程序提供的 Storage API
    background.js
    js
    chrome.runtime.onMessage.addListener(({ type, name }) => {
      if (type === "set-name") {
        chrome.storage.local.set({ name });
      }
    });
    
    chrome.action.onClicked.addListener((tab) => {
      chrome.storage.local.get(["name"], ({ name }) => {
        chrome.tabs.sendMessage(tab.id, { name });
      });
    });
    
  • 在 Manifest V2 版本中使用 setTimeoutsetInterval 实现延迟或定期操作,在 Manifest V3 版本的 Service Workers 中并可行,因为 Service Worker 并不会长期驻留后台,当它终止时调度程序会注销计时器。
    应该使用 Alarms API 来替代,它用以安排代码定期运行或在未来的指定时间运行。它也需要在后台脚本最顶层的作用域中注册。
    background.js
    js
    // set an alarm, which will dispatch onAlarm event after 3 mins
    chrome.alarms.create({ delayInMinutes: 3 });
    
    // listen the onAlarm event and react to it
    chrome.alarms.onAlarm.addListener(() => {
      chrome.action.setIcon({
        path: getRandomIconPath(),
      });
    });
    

Service Worker 实际上是一个 web worker,在浏览器中可以独立于网页运行,一般网页的执行上下文中都有全局变量 window,可以通过它访问一些浏览器提供的 API,例如 IndexedDB、cookie、localStorage 等,但在 Service Worker 中没有该对象,因此有诸多限制,例如在该环境中无法访问 DOM,无法发起 XMLHttpRequest(但支持 fetch 功能),以下是应对限制的一些解决方法:

  • 由于 Service Workers 无法访问 DOMParser API 以解析 HTML,我们可以通过 chrome.windows.create()chrome.tabs.create() API 创建一个标签页,以提供一个具有 window 对象的环境,或借助一些库(如 jsdomundom)弥补这个缺失。
  • 在 Service Workers 中无法播放或捕获多媒体资源,可以通过 chrome.windows.create()chrome.tabs.create() API 创建一个标签页,以提供一个具有 window 对象的环境,然后可以通过消息传递 message passing 在 service worker 中控制页面的多媒体播放。
  • 虽然在 Service Worker 中无法访问 DOM,但对于 <canvas> 画布元素,可以通过 OffscreenCanvas API 创建一个 canvas。关于 OffscreenCanvas 相关信息可以参考这里
    background.js
    js
    // for MV3 service workers
    function buildCanvas(width, height) {
      const canvas = new OffscreenCanvas(width, height);
      return canvas;
    }
    

Message Passing

参考

注入到页面的脚本 content scripts 是在网页运行的,它「脱离」了扩展程序,但可以使用信息传递 message passing (让运行在 web page 的页面脚本 content script 和扩展程序之间)进行沟通。

Chrome 除了提供简单的 API 进行一次性的请求-响应通讯,也提供复杂的 API 进行长连接通讯,还可以基于 ID 进行跨扩展程序的通讯,甚至可以与本机系统进行通讯

说明

任何合法的 JSON 格式的数据都可以传递。

注意

信息传递过程中需要考虑安全问题:

  • 内容脚本 content scripts 更容易遭受恶意网页攻击,应该限制通过 content script 传递过来的信息,可以触发的操作范围
  • 当扩展程序与外部资源进行通讯时,应该采取必要的行为避免发生跨站脚本攻击
    js
    chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
      // WARNING! Might be injecting a malicious script!
      document.getElementById("resp").innerHTML = response.farewell;
    });
    
    chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
      // innerText does not let the attacker inject HTML elements.
      document.getElementById("resp").innerText = response.farewell;
    });
    
    chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
      // JSON.parse does not evaluate the attacker's scripts.
      var resp = JSON.parse(response.farewell);
    });
    

一次性请求

使用 chrome.runtime.sendMessage() 方法chrome.tabs.sendMessage() 方法进行单次请求,发送信息

这两个方法都可以设置回调函数(默认接收返回的响应数据作为参数),以接收返回的信息进行后续处理

js
// 在页面脚本 content script 发送信息
chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
  // 接收并处理返回的信息
  console.log(response.farewell);
});
js
// 在扩展程序发送信息
// 在后台代码 background script 中编写代码
// 需要先使用 query 获取 tabId,以指定该请求发送给哪个特定的 tab 标签页面
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  // 该示例将信息发送到当前激活的标签页面
  chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response) {
    // 接收并处理返回的信息
    console.log(response.farewell);
  });
});

在信息的接收端,无论是网页页面(逻辑写在植入页面的代码 content script 中)或是扩展程序(逻辑写在后台代码 background script 中)都一样,需要使用方法 chrome.runtime.onMessage.addListener() 监听信息事件,以捕获请求/信息

js
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
  console.log(sender.tab ?
    "from a content script:" + sender.tab.url :
    "from the extension");
  if (message.greeting === "hello")
    sendResponse({farewell: "goodbye"});
  }
);

信息事件的处理函数接收三个参数:

  • 第一参数 message 是接收到的信息
  • 第二个参数 sender 是一个对象,它包含这次 message 的发送方的相关信息
  • 第三个参数 sendResponse 是一个方法,通过该函数可以向对方返回响应内容
说明

在信息事件的回到函数中,发送响应是同步执行的,即接收到 message 触发事件处理函数后,执行到 sendResponse 函数时就会立即发送响应

如果希望异步发送响应(例如需要先进行异步请求获取其他的数据,再整合到响应内容中),则可以在 onMessage 事件的处理函数的最后先显性返回 return true 以告知扩展程序响应会异步执行/发送,这样信息通道 message channel 还会保持打开,直到 sendResponse 方法被调用

注意

即使在不同页面设置了多个 onMessage 事件监听器,但是为了保证只有一次响应,扩展程序对于每次 message 请求整体都最多只会执行一次 sendResponse() 方法,其他事件处理函数中的响应函数都会被忽略

长连接

可以使用 chrome.runtime.connect() 方法chrome.tabs.connect() 方法 在内容脚本 content script 和扩展程序之间建立一个长连接

使用以上方法创建通道后,会返回一个 runtime.Port 端口对象,其中包括了关于信息通道的相关方法和属性。

然后就可以调用该通道对象的方法 portObj.postMessage() 发送信息;而调用方法 portObj.onMessage.addListener() 则是监听信息事件,以便接收信息

js
/**
 * 发起端
 */
// 该代码片段编写在页面脚本 content script
// 以建立长连接的信息通道
// 传递参数,为信息通道 channel 设置名称,以区别其他的通道
// (也就是说扩展程序可以同时建立多个信息通道)
var port = chrome.runtime.connect({name: "knock-knock"});
// 通过该端口发送信息
port.postMessage({joke: "Knock knock"});
// 设置事件监听器,通过该端口接收信息
// 接收到的信息会作为回调函数的入参
port.onMessage.addListener(function(msg) {
  if (msg.question === "Who's there?")
    port.postMessage({answer: "Madame"});
  else if (msg.question === "Madame who?")
    port.postMessage({answer: "Madame... Bovary"});
});
说明

类似地,如果在扩展程序中建立长连接(代码写在后台代码 background script 中)发送消息时,使用方法 chrome.tabs.connect() 以指定请求是发送给哪个特定的 tab 标签页

信息通道是双向的,因此除了发起端创建端口,还需要在接收端使用方法 chrome.runtime.onConnect() 响应通道连接的请求(在内容脚本 content script 和扩展程序的后台脚本 background script 中的设置方法是一样的)

当通道的发起端口调用 connect 方法时,接收端的监听器就会调用回调函数,它将通道的发起端口 runtime.Port 端口对象作为入参,然后接收端也可以使用这一相同的端口,在通道中发送和接收消息,这样通道两端的接口就可以相互接收和发送信息

js
/**
 * 接收端
 */
chrome.runtime.onConnect.addListener(function(port) {
  console.assert(port.name === "knock-knock");
  port.onMessage.addListener(function(msg) {
    if (msg.joke === "Knock knock")
      port.postMessage({question: "Who's there?"});
    else if (msg.answer === "Madame")
      port.postMessage({question: "Madame who?"});
    else if (msg.answer === "Madame... Bovary")
      port.postMessage({question: "I don't get it."});
  });
});

端口生命周期

信息通道连接是长期的,但也可能因为以下原因断开:

  • 在接收端没有设置监听器 chrome.runtime.onConnect() 无法成功建立通道
  • 使用 chrome.tabs.connect() 创建通道时,期望连接的端口所在的页面不存在
  • All frames that received the port (via runtime.onConnect) have unloaded
  • 端口对象调用了方法 chrome.runtime.Port.disconnect() 结束连接
    提示

    如果发起端口调用 connect 后,与多个接收端口之间创建了多个信息通道,而在其中一个接收端口调用 disconnect 时,则 onDisconnect 事件只会在发起端口触发,其他端口并不会触发

可以使用方法 port.onDisconnect() 监听该端口的断连事件(其中 port 是端口对象),该断连事件的回调函数的入参是该端口对象

跨扩展程序的通讯

除了在扩展程序内进行信息传递,还可以使用类似的 messaging API 在不同扩展程序间进行通讯。

对于发送方:

  • 使用方法 chrome.runtime.sendMessage(id, message) 发送一次性的请求传送信息
  • 使用方法 chrome.runtime.connect(id) 发起通道连接请求,并返回端口对象
js
// The ID of the extension we want to talk to.
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// Make a simple request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      // 发送请求信息时,必须提供扩展程序的 ID,以便其他扩展程序可以进行判断,是否作出响应
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  }
);

// Start a long-running conversation:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

对于接收方:

  • 对于一次性的请求,使用方法 chrome.runtime.onMessageExternal() 进行监听并作出响应
  • 对于长连接,使用方法 chrome.runtime.onConnectExternal() 响应连接,并使用端口对象接收和发送信息
js
// For simple requests:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id === blocklistedExtension)
      return;  // don't allow this extension access
    else if (request.getTargetData)
      sendResponse({targetData: targetData});
    else if (request.activateLasers) {
      var success = activateLasers();
      sendResponse({activateLasers: success});
    }
  });

// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    // See other examples for sample onMessage handlers.
  });
});

网页通讯

类似于跨扩展程序间通讯,一般的网页可以发送信息给扩展程序。

而扩展程序需要在配置清单 manifest.json 的选项 externally_connectable.matches 中声明/注册,希望与哪些外部网页进行连接(可以使用正则表达式,以支持一系列符合一定规则的网页,但至少包含二级域

json
{
  // ...
  "externally_connectable": {
    "matches": ["https://*.example.com/*"]
  }
}

在网页使用方法 chrome.runtime.sendMessage() 或方法 chrome.runtime.connect() 发送信息(通过 ID 来指定与哪一个扩展程序进行通讯)

js
// The ID of the extension we want to talk to.
var editorExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// Make a simple request:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
  function(response) {
    if (!response.success) handleError(url);
});

在扩展程序使用方法 chrome.runtime.onMessageExternal() 或方法 chrome.runtime.onConnectExternal() 监听信息

js
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.url === blocklistedWebsite)
      return;  // don't allow this web page access
    if (request.openUrlInEditor) openUrl(request.openUrlInEditor);
});
注意

当扩展程序与一般的网页通讯时,长连接只能通过网页端发起

Options Page

扩展程序的设置页面 Options Page 一般是让用户设置扩展程序的参数,以调整扩展程序的行为方式,以更符合自己的使用习惯。

如果要为扩展程序添加设置页面,需要先在配置清单 manifest.json 的中进行声明/注册,有两种类型的设置页面:

  • Full page options 整页的设置页面,需要打开一个标签页
    在配置清单的选项 options_page 中声明/注册作为设置页面的 HTML 文档的路径
    json
    {
      // ...
      "options_page": "options.html",
    }
    

    然后用户可以在浏览器工具栏的扩展程序图标右键点击选择「选项」打开设置页面
    打开设置页面-1
    打开设置页面-1

    或者在扩展程序的「详情」页面中选择「扩展程序选项」打开设置页面
    打开设置页面-2
    打开设置页面-2
  • Embedded Options 内嵌的设置页面,以弹出式 modal 模态框的形式展示设置页面
    提示

    模态框的大小会根据页面内容自动调整,但有时候可能并不奏效,此时可以为设置页面先提供一个最小的尺寸,以保证布局正常显示。


    在配置清单的选项 options_ui.page 中指向相应的 HTML 文档路径,并将选项 option_ui.open_in_tab 设置为 false,表示采用内嵌式的设置页面(如果设置为 true 就会打开新的标签页来展示设置页面)。
    json
    {
      // ...
      "options_ui": {
        "page": "options.html",
        "open_in_tab": false
      },
    }
    

    然后用户就可以在在浏览器工具栏的扩展程序图标右键点击选择「选项」,会跳转到相应扩展程序的详情页,同时打开模态框展示设置页面;或者在扩展程序的「详情」页面中选择「扩展程序选项」,打开模态框展示设置页面。
    打开设置页
    打开设置页
提示

除了通过用户点击相应的按钮打开扩展程序的设置页面,还可以在扩展程序的其他页面/脚本中(例如 popup 页面),通过调用方法 chrome.runtime.openOptionsPage() 以编程式的方法打开设置页面

html
<!-- 点击按钮打开扩展程序的设置页面 -->
<button id="go-to-options">Go to options</button>
js
document.querySelector('#go-to-options').addEventListener('click', function() {
  // 先判断浏览器是否支持该方法
  if (chrome.runtime.openOptionsPage) {
    chrome.runtime.openOptionsPage();
  } else {
    // 如果不支持就手动基于页面路径打开设置页面
    window.open(chrome.runtime.getURL('options.html'));
  }
});

虽然两种方式都可以供用户设置参数,但是有一些不同:

  • 对于内嵌型的设置页面,不支持部分 Tabs 相关的 API
    • 无法使用方法 chrome.tabs.query() 通过设置页面的 URL 找到对应的 tab
    • 打开设置页面时,标签创建事件 chrome.tabs.onCreated.addListener() 无法被监听到
    • 当设置页面刷新/重载时,更新事件 chrome.tabs.onUpdated.addListener() 无法被监听到
  • 无法使用 chrome.tabs.connect() 方法chrome.tabs.sendMessage() 方法,❓ 即无法从扩展程序向设置页面发送信息
    chrome.runtime.connect() 方法chrome.runtime.sendMessage() 方法并不受限,即可从设置页面向扩展程序发送信息
    说明

    如果设置页面使用方法 chrome.runtime.connect()方法 chrome.runtime.sendMessage() 传递信息,接收者并不能读取发送者 sender 的 tab 信息,只能读取到发送者的 url 信息(设置页面文档的路径)

提示

为了可以让用户的设置数据持久化及跨设备同步,可以使用 Chrome 提供的 storage.sync API,如果要使用该 API 记得先在配置清单 manifest.json 中声明/注册 storage 权限

Permissions

参考

限制扩展程序的权限,不仅可以降低扩展程序被恶意程序利用的可能性;还可以让用户有主动选择权,决定应该给扩展程序授予哪些权限

扩展程序为了可以使用部分 Chrome APIs 或访问外部网页,需要先在配置清单 manifest.json 中进行显式声明,在配置清单 manifest 中有三种不同的选项以声明不同类型的权限

  • permissions 选项:以数组的形式列出所需的权限,用于设置必要权限许可 required permissions 为了实现扩展程序的基本功能,会在安装时询问用户获取授权
    提示

    所有可以在 permissions 选项中声明的权限可以查看官方文档

  • optional_permissions 选项:以数组的形式列出所需的权限,用于设置可选权限 optional permissions 为了实现某些可选的功能(一般是访问特定的数据或资源),在扩展程序运行时才询问用户,以获取权限许可
    注意

    permissions 选项中声明的权限大部分也适用于optional_permissions 选项,部分权限除外,即它们并不能作为 optional_permissions 可选权限

    • debugger
    • declarativeNetRequest
    • devtools
    • experimental
    • geolocation
    • mdns
    • proxy
    • tts
    • ttsEngine
    • wallpaper
    提示

    除了可以在配置清单 manifest.json 中设置所需的可选权限,还可以在扩展程序的逻辑代码中使用方法 chrome.permissions.request() 动态申请,这样就可用基于用户主动交互,例如在按钮的点击事件的处理函数中,按需获取权限

    如果申请的权限会触发警告提示,则会弹出一个权限提示框询问用户许可,并等待结果返回再执行后续的代码

    js
    document.querySelector('#my-button').addEventListener('click', (event) => {
      // Permissions must be requested from inside a user gesture, like a button's
      // click handler.
      chrome.permissions.request({
        permissions: ['tabs'],
        origins: ['https://www.google.com/']
      }, (granted) => {
        // The callback argument will be true if the user granted the permissions.
        if (granted) {
          doSomething();
        } else {
          doSomethingElse();
        }
      });
    });
    
  • host_permissions 选项:以数组的形式列出一系列正则表达式,它们是用于匹配 url 的,用于声明扩展程序可以去访问哪些主机

然后浏览器会在扩展程序安装时或在运行时,询问用户是否允许它获取相应的权限、访问特定的资源,让用户可以有主动权去保护自己的数据

manifest.json
json
{
  // ...
  "permissions": [
    "tabs",
    "bookmarks",
    "unlimitedStorage"
  ],
  "optional_permissions": [
    "unlimitedStorage"
  ],
  "host_permissions": [
    "http://www.blogger.com/",
    "http://*.google.com/"
  ],
}
提示

可以使用方法 chrome.permissions.contains() 查看扩展程序当前是否拥有某项权限,使用方法 chrome.permissions.getAll() 获取扩展程序当前具有的所有权限

js
chrome.permissions.contains({
  permissions: ['tabs'],
  origins: ['https://www.google.com/']
}, (result) => {
  if (result) {
    // The extension has the permissions.
  } else {
    // The extension doesn't have the permissions.
  }
});

删除权限

如果不再需要某项权限时,可以使用方法 chrome.permissions.remove() 删除该权限,如果之后再使用方法 chrome.permissions.request() 动态添加相应的权限,此时扩展程序并不会再告知/询问用户

js
chrome.permissions.remove({
  permissions: ['tabs'],
  origins: ['https://www.google.com/']
}, (removed) => {
  if (removed) {
    // The permissions have been removed.
  } else {
    // The permissions have not been removed (e.g., you tried to remove
    // required permissions).
  }
});

最佳实践

有一些权限在获取时会弹出警告提示,询问用户以获取授权

权限警告
权限警告

最佳实践

具体哪些权限会通知用户,或是询问让用户主动授权,可以查看官方文档中最佳实践的相关内容

在安装扩展程序时,如果弹出过多的警告会影响用户体验,还可能降低安装量,所以在配置清单 manifest.json 的选项 permissions 中应该只声明核心功能所必需的权限

而那些需要弹出警告提示来获取的权限,则应该在选项 optional_permissions 中声明,然后使用交互控件,如按钮或 checkbox,让用户主动激活可选功能,此时才弹出权限警告框,让用户主动选择是否授权,这样的交互体验会更佳

另外如果更新扩展程序时,新增 permissions 可能会导致扩展程序临时无法激活,需要用户再起允许授权才可以,如果将这些新增的 permissions 放在选项 optional_permissions 则可以避免这个不好的体验

推荐使用 activeTab 权限(以替代 <all_urls> 权限),该权限支持临时获取(作为可选权限),以访问当前激活的标签页,并在当前页面使用 tabs 相关的 API(当导航到其他 url 或关闭当前页面后,该路径的可访问性就失效),不会在安装扩展程序时弹出警告

说明

对于在「开发者模式」下「加载已解压的扩展程序」,并会展示权限警告提示。如果希望查看警告提示的效果,可以相对开发的扩展程序进行打包,具体步骤请参考官方指南

Storage

扩展程序可以在浏览器中存储和检索数据。

Chrome 浏览器为拓展出现提供数据存储 API chrome.storeage ,该 API 提供类似 localStorage 的功能,但也有一些不同:

  • 使用 chrome.storage.sync 相关方法,就可以利用 Chrome 的同步功能,实现同一账户下的扩展程序数据在多个设备之间同步
    说明

    如果已登录账户的 Chrome 处于离线状态,则需要同步存储的数据会先进行本地存储,等待浏览器上线后再进行同步。

    如果用户在 Chrome 设置中取消了数据同步功能,那么 chrome.storage.sync 相关方法的作用和 chrome.storage.local 一样

  • 批量的读取和写入数据操作是异步执行的,因此与 localStorage(该方法是同步执行的,会引起的阻塞和串行)相比,该 API 的操作更快
  • 存储的数据类型可以是对象,而 localStorage 只允许存储字符串
  • Enterprise policies configured by the administrator for the extension can be read (using storage.managed with a schema). The storage.managed storage is read-only

有三种不同的存储域 ❓ StorageArea

  • sync 同步存储的数据
  • local 本地存储的数据
  • session 会话存储的数据(存储在内存里)
  • managed 管理员设置的数据,只读

如果要使用该 API 需要先在配置清单 manifest.json 的选项 permissions 中声明/注册相应的权限

manifest.json
json
{
  // ...
  "permissions": [
    "storage"
  ],
}

该 API 所允许存储的数据并不大

  • 对于 sync 同步存储的数据,允许总大小为 100KB,最多存储 512 个数据项,每项大小为 8KB
  • 对于 local 本地存储的数据,允许总大小为 5MB(类似于 localStorage 的存储限制),因此它们一般用作存储、同步扩展程序的设置
  • 对于 session 会话存储的数据,允许总大小为 10MB,但是该类型的数据只会保留在内存中,并不会持久化地写入硬盘里
注意

与用户相关机密数据请不要使用该 API 进行存储,因为使用该 API 所存储的数据并不会加密

存储和读取数据

以键值对的形式存储数据(即数据格式是对象)

  • 使用方法 chrome.storage.local.set({key: value}, callback()) 将数据存储在本地,使用 chrome.storage.local.get(key) 获取给定 key 所对应的数据
  • 使用方法 chrome.storage.sync.set({key: value}, callback()) 进行同步存储,使用 chrome.storage.sync.get(key) 获取给定 key 所对应的数据
js
// sync
chrome.storage.sync.set({key: value}, function() {
  console.log('Value is set to ' + value);
});

chrome.storage.sync.get(['key'], function(result) {
  console.log('Value currently is ' + result.key);
});
js
// local
chrome.storage.local.set({key: value}, function() {
  console.log('Value is set to ' + value);
});

chrome.storage.local.get(['key'], function(result) {
  console.log('Value currently is ' + result.key);
});
说明

获取数据时,不仅可以传入 key 字符串,也可以传入 keys 数组,以返回对应的一系列数据

如果获取数据时,传入的 key 为 null 则会返回所有存储的数据

删除数据

如果想删除某个数据,可以使用方法 chrome.storage.remove(key, callback()) 删除给定 key 所对应的数据

如果要清空所有存储的数据,可以使用方法 chrome.storage.clear(callback())

这两个方法都可以设置回调函数,以便在删除数据后执行后续操作

监听数据更改

当扩展程序所存储的数据发生改变时,会触发 onChange 事件,可以对其进行监听以作出响应

background.js
js
chrome.storage.onChanged.addListener(function (changes, namespace) {
  for (let [key, { oldValue, newValue }] of Object.entries(changes)) {
    console.log(
      `Storage key "${key}" in namespace "${namespace}" changed.`,
      `Old value was "${oldValue}", new value is "${newValue}".`
    );
  }
});

Accessibility

可及性,又称为 a11y,是指一系列的设计指南,以便残障人士更易于使用扩展程序,例如快捷键命令可以让视觉障碍人士更方便地使用扩展程序,字幕和翻译功能对视觉障碍人士和外语者有帮助。

开发者可以遵循以下建议,以提高扩展程序的可及性 accessible:

  • 使用正确的语义化 HTML 元素搭建 UI 控件,以便保障可及性。这些控件保证了可以通过键盘(Tab 键)访问操作,且它们的语义性让屏幕阅读器更容易理解。
    语义化 HTML 元素
    语义化 HTML 元素
  • 如果需要使用非语义化的 HTML 元素或 JavaScript 构建 UI 控件时,可以通过 WAI-ARIA,Web Accessibility Initiative - Accessible Rich Internet Applications,一个由 W3C 制定的规范,它通过给 DOM 元素添加一些规定的属性,让我们的开发的网页应用更具语义化。
    提示

    WAI-ARIA 属性不会对 web 页面有任何影响,只是让浏览器暴露更多的信息给无障碍 API,例如屏幕阅读器。


    该规范中三个主要的特性:
    • 声明元素的角色和功能,例如 role="navigation" 表示该元素的作用是导航
    • 为元素定义额外的属性,例如 aria-require=true 告知用户该表单是必添的
    • 设定元素的状态,例如 aria-disable="true" 表示该表单禁止输入
    Tip

    状态和属性的差异之处就是:属性在应用的生命周期中不会改变,而状态可以,用编程的方法改变。

    html
    <div role="toolbar" tabindex="0" aria-activedescendant="button1">
      <img src="buttoncut.png" role="button" alt="cut" id="button1">
      <img src="buttoncopy.png" role="button" alt="copy" id="button2">
      <img src="buttonpaste.png" role="button" alt="paste" id="button3">
    </div>
    

    以上示例中,属性 tabindex="0" 让该元素可以聚焦,并且可以通过键盘来导航,其聚焦顺序由 DOM 结构决定;属性 aria-activedescendant 指定该元素聚焦时,其子代哪一个元素先进行聚焦。
  • 元素可聚焦让用户仅通过键盘也能操作扩展程序页面,应该保证按钮、菜单栏等元素可以聚焦。默认情况下,只有锚标签、按钮、表单控件可以通过键盘聚焦,但是我们可以通过为元素添加属性 tabIndex="0" 让任意元素可以聚焦;也可以通过添加属性 tabIndex="-1" 让该元素无法通过键盘聚焦(但仍可以通过 JavaScript 编程聚焦 elem.focus())。
  • 与通过 Tab 键聚焦元素相比,快捷键可以让用户更方便地操作扩展程序页面的元素,可以参考官方示例如何用快捷键进行导航。如果在扩展程序的页面内提供了快捷键,应该在设置页面 options page 中告知用户。
  • 以下设置可以完善内容的可访问性
    • 文本:选择合适的字体和字号大小以确保文本内容的可读性,而且应该考虑视觉障碍者的使用体验,他们可能需要设置更大的字体,避免将自定义的快捷键与页面缩放的快捷键相冲突。为了确保版型的稳定,应该测试缩放至 200%,查看页面是否还显示正常。应该避免将文字嵌入到图片中,这样不仅无法调整字体,还无法让屏幕阅读器识别;如果必须将文字嵌入图片中,应该在图片的可替代文字(属性 alt)中包含这些文字内容,相关指南可以参考这里
    • 颜色:背景和内容的颜色应该有足够的对比度,可以使用 Colour Contrast Check 工具检查。对于颜色丰富的图表,可以使用色弱模拟器 Coblis 来查看它在不同程度的色弱症人士眼里的效果,调整图表的配色,以使扩展程序对于这些用户更友好。可以考虑为提供不同的色彩主题,或允许自定义配色方案,以便用户有更好的体验。
    • 声音:如果扩展程序需要声音和视频传达信息,应该提供字幕和翻译功能,关于多媒体字幕的相关指南参考这里
    • 图片:为图片提供可替代文字,即属性 alt(文字内容应该用于阐明图像的目的,而不是描述图片的细节),而对于占位图和纯装饰作用的图片,应该将该属性值留空 "",或使用 CSS 来实现(而不是用 DOM)
      html
      <img src="img.jpg" alt="The logo for the extension">
      

更多相关资源

Internationalization

参考

国际化 I18N 是指在开发扩展程序时,可以让用户按需切换语言

多语言版本的开发

关于软件多语言版本的开发

  • 国际化 Internationalization,L18N:软件国际化是在软件设计和文档开发过程中,使得功能和代码设计能处理多种语言和文化传统,使创建不同语言版本时,不需要重新设计源程序代码的软件工程方法。
  • 本地化 localization ,L10N:能够使网站、Web 应用或任何其他形式的内容适用于特定语言的范围和文化圈。

为了让扩展程序更方便地实现国际化,需要遵循以下建议:

  • 将所有用户可见的内容(文本)放在一个 messages.json 文档中,并将该文档放在 _locales/_localeCode_ 相应的目录下(其中 _localeCode_ 是一个语言编码,例如英语就应该使用 en 表示)。
    提示

    如果项目中有 _locales 目录,需要在配置清单 manifest.json 的选项 default_locale 中声明默认语言,可以在这里查看具体的语言代码


    messages.json 文档中,它以键值对的形式来定义内容,每一个键都包含一个需要国际化的项(大小写敏感),值是一个对象,其中常用的属性如下:
    • 属性 message 是需要显示的内容
    • (可选)属性 description 是对该项的描述,便于告知翻译者该项的意义。
    messages.json
    json
    {
      "search_string": {
        "message": "hello%20world",
        "description": "The string we search for. Put %20 between words that go together."
      },
      //  ...
    }
    
    提示

    键名不能以 @@ 开头,这一类的键名是保留预定义的项,常用的预定义 message 项如下:

    Message 键名解释
    @@extension_id扩展程序的 ID,对于非国际化的扩展程序该 message 项也可用,💡 但不能用于 manifest.json
    @@ui_locale当前使用的语言
    @@bidi_dir当前的文字方向,ltr 表示从左往右书写的文字,rtl 表示从右往左书写的文字

    例如在 CSS 中使用扩展程序中的静态图片作为背景

    css
    body {
      background-image:url('chrome-extension://__MSG_@@extension_id__/background.png');
    }
    
  • 然后就可以在项目的其他文档中,通过 message 项来使用相应的数据:
    • manifest.json 或 CSS 文档中,通过 __MEG_messagename__ 来使用 message 相应的项 messagename
    • 在扩展程序页面或 JavaScript 脚本中,通过方法 chrome.i18n.getMessage("messagename") 获取相应的项 messagename

    i18n-1
    i18n-1
    Tip

    每次新增一种语言,就需要新增一个 message 文件到 _locales/_localeCode_ 目录里。每一种语言中,不必包含与默认语言中定义的全部 message 项。


    i18n file structure
    i18n file structure

    以上示例是支持英语、西班牙语、韩语的扩展程序项目文件结构
    Tip

    浏览器会基于用户的语言设置和扩展程序的 default_locale 设置来自动选择采用哪一个 messages.json 文档

Identity

OAuth 2.0

关于该授权机制的介绍可以参考这里

扩展程序可以基于 OAuth 2.0 获取授权,访问 Chrome 用户信息,具体参考 chrome.identity API

Management

管理已安装和正在运行的扩展程序,具体参考 chrome.management API

用户交互

Action

参考

扩展程序的图标
扩展程序的图标

该交互控件是指在浏览器工具栏的扩展程序图标,供用户点击,可以执行预定的操作。

它也可以作为唤起其他交互控件的入口,例如通过适当的配置,在悬停时可以显示提示框 tooltip,在点击时可以弹出弹出框 popup

说明

安装扩展程序后,Action 图标默认「隐藏」在浏览器工具栏的扩展程序(拼图)图标下,可以将它 pin 到工具栏上

提示

在 MV2 版本中有 API chrome.browserAction(浏览器级别的按钮,对所有页面都可以响应)和 API chrome.pageAction(针对特定匹配条件的页面才会响应)

而在 MV3 版本中新增了 chrome.action API,它的功能有点像是 MV2 的 Browser Action,可以在大部分的页面响应用户点击

默认在所有标签页时 action 图标都是可以响应点击的 clickable,如果希望实现类似于 Page Action 的功能,即只在特定的页面才会响应用户的点击操作,可以参考官方示例,先使用 chrome.action.disable() 将 Action 图标设置为默认不响应点击,再通过 new chrome.declarativeContent.PageStateMatcher() 设置当 Page URL 匹配时(只有在特定页面)才调用方法 chrome.action.enable() 让 Action 图标可响应点击,

但是这样的设置会影响 popup 的显示或 chrome.action.onClicked 所监听的相应事件的分发

如果要使用该控件,需要先在配置清单 manifest.json 的选项 action 中进行声明/注册,例如指定所使用的图标文件的路径,如果与该控件交互时,使用了 popup 弹出页面,则还需要要指定弹出页面相对应的 HTML 文件

manifest.json
json
{
  // ...
  "action": {
    // 图标文件
    "default_icon": {              // optional
      "16": "images/icon16.png",   // optional
      "24": "images/icon24.png",   // optional
      "32": "images/icon32.png"    // optional
    },
    // 提示框
    "default_title": "Click Me",   // optional, shown in tooltip
    // 弹出框
    "default_popup": "popup.html"  // optional
  },
}
提示

即使扩展程序的配置清单中没有设置 action 选项,该扩展程序在浏览器工具栏上也会显示一个默认的图标,一般以背景色为灰底,且包含扩展程序的首字母

图标

icon 图标的默认高和宽都是 16 DIPs(device-independent pixels),推荐预设多个尺寸的图标,浏览器根据不同的场景自动选择使用恰当的尺寸图片。

图标文件的格式需要是 Blink 渲染引擎所支持的,例如 PNG、JPEG、BMP、ICO 等,而 SVG 是支持的。如果是解压的扩展程序,则只支持 PNG 格式。

图标尺寸用途
16x16作为扩展程序相关页面的 favicon 图标,以及在右键菜单项中显示
32x32一般在 Windows 系统中需要使用该尺寸
48x48在浏览器的扩展程序管理页面中显示
128x128在 Chrome 网上应用店显示

除了在配置清单 manifest.json 的选项 action.default_icon 中提供固定的文件,还有使用方法 chrome.action.setIcon() 通过编程的方式设置图标,可以根据不同的条件设置不同的图片(路径),也可以使用 canvas 创建图片。⚠️ 该方法是用于设置静态图片,而不应该用于设置动图。

js
const canvas = new OffscreenCanvas(16, 16);
const context = canvas.getContext('2d');
context.clearRect(0, 0, 16, 16);
context.fillStyle = '#00FF00';  // Green
context.fillRect(0, 0, 16, 16);
const imageData = context.getImageData(0, 0, 16, 16);
chrome.action.setIcon({imageData: imageData}, () => { /* ... */ });

提示框

tooltip 是在用户悬停在图标时显示的提示框,可以设置一段简短的文字,用于显示该扩展程序的名称

除了在配置清单 manifest.json 的选项 action.default_title 中设置文字,还可以使用方法 chrome.action.setTitle() 通过编程的方式进行设置

js
chrome.action.setTitle({title: 'this is tooltip content'})
提示

当图标按钮被聚焦时,提示框的内容可以被屏幕阅读软件识别,可以增强扩展程序的可及性

弹出页面

popup 是在用户点击 Action 图标时显示的一个弹出框,它实际上是一个大小受到限制的 HTML 页面,默认大小是基于其包含的内容布局的

popup 弹出页面的高和宽的最小值是 25px,高的最大值是 600px,宽的最大值是 800px

除了在配置清单 manifest.json 的选项 action.default_popup 中设置 popup 页面,还可以使用方法 chrome.action.setPopup() 以编程的方式动态地更新弹出框所指向的 HTML 文件的路径

js
chrome.storage.local.get('signed_in', (data) => {
  if (data.signed_in) {
    chrome.action.setPopup({popup: 'popup.html'});
  } else {
    chrome.action.setPopup({popup: 'popup_sign_in.html'});
  }
});
提示

popup 和普通的 HTML 文档一样,可以引入样式文件和脚本文件,但不支持使用行内脚本 inline JavaScript

标记

badge 是一个添加到图标上的文字,并允许设置背景色,一般用以显示扩展程序的状态,例如更新了新版本可以显示 new,如果扩展程序带统计功能则可以显示数值等

注意

由于 badge 需要添加到图标上,所以在设置 badge 前,扩展程序需要先在配置清单 manifest.json 的选项 action 中设置图标

由于标记的空间有限,所以一般只能显示 4 个或以下的字符

通过方法 chrome.action.setBadgeText(settingObj, callbackFn) 设置标记文字,该方法接收一个配置对象 settingObj 和回调函数 callbackFn

配置对象可设置以下属性

  • tabId 属性:数值,表示特定的标签页。当用户选中该标签页时,会显示相应的标记内容
  • text 属性:字符串,标记内容
说明

如果方法 setBadgeText 第一个参数的选项 tabId 省略了,则该标记会作为全局设置,而提供了 tabId 则针对特定的标签页,优先级更高,会覆盖全局所设置的标记文字

通过方法 chrome.action.setBadgeBackgroundColor(settingObj, callbackFn) 设置标记的背景色

js
// 设置 badge 文字内容
chrome.action.setBadgeText(
  // 配置对象
  {
    // 标记内容
    text: 'NEW'
  }
);

// 通过多种方式设置 badge 背景颜色
chrome.action.setBadgeBackgroundColor(
  // 配置对象
  {
    // 支持以数组表示 RGBA 颜色
    color: [0, 255, 0, 0] // Green
  },
  // callbackFn 回调函数
  () => { /* ... */ },
);


chrome.action.setBadgeBackgroundColor(
  {
    // 支持 HEX 表示颜色
    color: '#00FF00', // Also green
  }
  () => { /* ... */ },
);

chrome.action.setBadgeBackgroundColor(
  {
    // 支持以颜色名称指定颜色
    color: 'green' // Also, also green
  },
  () => { /* ... */ }, // callbackFn
);

action 在每一个标签页都可以有不同的状态,例如可以针对不同的标签页,设置不同 badge 内容

js
function getTabId() { /* ... */}
function getTabBadge() { /* ... */}

chrome.action.setBadgeText(
  {
    text: getTabBadge(tabId),
    tabId: getTabId(),
  },
  () => { ... }
);

Context Menu

参考

菜单选项
菜单选项

该交互控件是指浏览器的右键菜单,可以通过扩展程序往其中添加选项,除了可以针对整个页面,还可以针对特定的 DOM 元素,或 action 图标,添加右键菜单项。

如果要使用该控件,需要先在配置清单 manifest.json 的选项 permissions 中进行声明/注册相关的权限。而且为了便于将菜单选项(通过图标指示)与扩展程序相匹配,需要在配置清单的选项 icons 中指定图标文件,最好提供多种尺寸的图片文件

manifest.json
json
{
  // ...
  "permissions": [
    "contextMenus"
  ],
  "icons": {
    "16": "icon-bitty.png",
    "48": "icon-small.png",
    "128": "icon-large.png"
  },
}

创建菜单项

使用方法 chrome.contextMenus.create(settingObj, callbackFn) 为扩展程序创建专属的菜单项

说明

该方法应在后台脚本 background script 中设置,代码逻辑应该编写在钩子函数 chrome.runtime.onInstalled.addListener()(该钩子函数会在安装扩展程序完成时调用),以便安装完成时,右键菜单选项即可用

js
// background.js
chrome.runtime.onInstalled.addListener(function() {
  chrome.contextMenus.create({
    id: key,
    title: 'context menu demo',
    type: 'normal',
    contexts: ['selection'],
  });
});

chrome.contextMenus.create(settingObj, callbackFn) 方法可以接收两个入参

  • 第一个参数是配置对象 settingObj 有多个属性,常用如下
    • id 属性:为当前菜单选项分配一个唯一 ID
    • title属性:必须(除非该菜单选项的类型是分割线 type: "separator")设置,作为菜单项的内容显示出来
    • type 属性:菜单选项的类型,默认值是 normal,就是正常的菜单选项,还可以是 checkboxradioseparator
    • contexts 属性:一个数组,限制菜单选项的出现,当用户在对页面的哪个元素进行右键点击时,默认值是 ['page'],即在整个网页任何地方右键点击时,该菜单选项都显示在菜单中
    • parentId 属性:当前菜单选项的父级菜单的 ID,可用于构架复杂的多级菜单
    • onclick 属性:监听菜单选项的点击事件,并设置事件的处理函数。当菜单选项被点击时执行该事件处理函数。该处理函数有两个入参 info(该菜单选项的信息)和 tab(当前标签页的信息)传入
  • 第二个(可选)参数是回调函数 callbackFn 是在用户在执行右键点击,且该菜单选项被创建(显示在菜单)时所执行的

最后该方法的返回值是该菜单选项的唯一 ID 值

说明

可以通过上述方法,为扩展程序创建多个菜单选项,但是如果选项多于一个,浏览器会自动将它们收纳到一个次级菜单

更新菜单项

使用方法 chrome.contextMenus.update(menuItemId, settingObj, callbackFn) 更新给定的 menuItemId 菜单选项

该方法接收 3 个参数:

  • 第一个参数 menuItemId 是菜单选项的唯一 ID 值
  • 第二个参数 settingObj 是配置对象,和方法 chrome.contextMenus.create() 的配置对象可使用的属性一样
  • 第三个(可选)参数 callbackFn 是回调函数,在更新完菜单选项后执行

提示

浏览器的右键菜单默认是全局的,即可以出现在任何 URL 所对应的页面中,甚至是 file://chrome://URLs 的页面

如果希望控制菜单选项出现在指定的页面,可以在使用上述方法创建或更新菜单选项时,通过配置对象 settingObj 的属性 documentUrlPatterns 来限制,只在特定的 URL 所对应的页面或 <iframe> 中显示菜单项

删除菜单项

使用方法 chrome.contextMenus.remove(menuItemId, callbackFn) 动态删除指定的 menuItemId 已创建的菜单选项。第二个(可选)参数是一个回调函数,它会在删除指定的菜单选项后执行。

如果想删除所有该扩展程序所创建的菜单选项,则可以使用方法 chrome.contextMenus.removeAll(callbackFn)

Omnibox

参考

Omnibox
Omnibox

该交互控件是指地址栏上的搜索关键词,当用户在地址栏输入相应的关键词,就会触发 Omnibox(唤起相应的扩展程序),接下来用户在地址栏中输入内容是直接与该扩展程序进行交互。一般会在扩展程序中预先设置一系列的搜索建议,当用户输入的内容模糊匹配成功时,就会在一个 dropdown 中显示相应的搜索建议。

如果要使用该控件,需要先在配置清单 manifest.json 的选项 omnibox 中进行声明注册。当 Omnibox 被触发时,扩展程序图标(以灰阶的形式展示)和名称会显示在地址栏的左侧,了便于识别所以需要在配置清单的选项 icons 中指定图标文件,最好提供多种尺寸的图片文件(默认使用高和宽为 16px 的图标)

处理该控件的交互逻辑的代码则是在后台脚本 background script的 service worker 中设置,基于事件监听-响应的原理

manifest.json
json
{
  // ...
  "background": {
    "service_worker": "background.js"
  },
  "omnibox": { "keyword" : "demo" },
  "icons": {
    "16": "icon-bitty.png",
    "48": "icon-small.png",
    "128": "icon-large.png"
  },
}

以上示例将扩展程序的 Omnibox 触发关键词设定为 demo,那么当用户在地址栏输入 demo 时,就会在下拉框显示一个扩展程序名称的选项,可以点击该选项、或按 Tab 键、或键入空格 Space,即可触发该扩展程序的 Omnibox

Omnibox 触发
Omnibox 触发

Omnibox 交互控件一般是通过监听相关的事件实现的

监听用户输入

使用 onInputChanged 可监听用户的输入。在地址栏进入 Omnibox 模式后,当用户在地址栏中输入内容时会触发相应的回调函数

background.js
js
chrome.omnibox.onInputChanged.addListener((text, suggest) => {
  if(!text) return;
  suggest([
      {
          content: text,
          description: `search for ${text}`
      }
  ])
});

输入事件的处理函数会接收两个入参

  • 第一个参数 text 是用户输入的内容(字符串)
  • 第二个参数 suggest 是一个方法,用于设置搜索建议
    该方法接收一个数组作为参数,它包含一系列的建议结果 SuggestResult(每一个搜索建议都是一个对象),该方法会将这些建议选项传递回浏览器,显示在地址栏的下拉框中
    说明

    建议结果 SuggestResult 是一个对象,以供用户选择,包括以下属性

    • content 属性:当用户选中该建议选项时,实际上会输入到地址栏的内容,同时这个内容会返回/传递给扩展程序
    • deletable 属性:该建议选项是否可以让用户删除
    • description 属性:描述内容,显示在地址栏的下拉框中,可以包含 XML 风格的样式修饰。但是不能包含 5 种 XML 转义字符
提示

可以使用方法 chrome.omnibox.setDefaultSuggestion(suggestion) 设置默认的建议选项

该方法接收的入参是一个不完整 suggestionResult 对象,没有 content 属性,其作用类似于输入框中的 placeholder

当触发了 Omnibox 时,在用户还没输入内容时,该默认建议就会出现在地址栏的下拉框中第一条的位置

注意

🎉 该 Bug 已经修复了

根据一个 Bug 报告,由于 Omnibox 的搜索建议内容支持 XML,所以需要调用 DomParser,但是后台脚本在 MV3 版本迁移改用了 service worker,该运行环境并没有 DomParser,所以会导致报错,且无法正常显示搜索建议选项

监听确认选择

onInputEntered 在用户选择执行一个建议选项后,触发回调函数

background.js
js
chrome.omnibox.onInputEntered.addListener((text, disposition) => {
  if(!text) return;
  console.log('inputEntered: ' + text);
  // Encode user input for special characters , / ? : @ & = + $ #
  var newURL = 'https://www.google.com/search?q=' + encodeURIComponent(text);
  chrome.tabs.create({ url: newURL });
  console.log(disposition)
});

事件处理函数接收两个参数

  • 第一个参数 text 是选中的搜索建议后,实际输入到地址栏的内容
  • 第二个参数 disposition 是进行搜寻时窗口的设置,有三种可能的结果:
    • currentTab 在当前标签页进行搜寻
    • newForegroundTab 新建一个标签页进行搜寻,同时切换到该标签页
    • newBackgroundTab 新建一个标签页进行搜寻,但不进行标签页的切换

以上示例调用了 chrome.tabs.create() 方法,在当前标签页进行搜索,所以终端打印的值是 currentTab

Override Pages

参考

覆写新建标签页
覆写新建标签页

该交互控件是通过覆写页面实现的,扩展程序可以覆写三个 Chrome 的特殊页面:

  • Bookmark Manager:书签管理页面 chrome://bookmarks
  • History:历史记录页面 chrome://history
  • New Tab:新建标签页 chrome://newtab
说明

每一个扩展程序只能覆写以上三个特殊 Chrome 页面之一,而每一种特殊 Chrome 页面,只能选择被一个扩展程序进行覆写

此外隐身模式下,新建标签页不能被覆写

如果要使用该控件,需要先在配置清单 manifest.json 的选项 chrome_url_overrides 中进行声明/注册,指定需要覆写的页面(bookmarkshistorynewtab 三者之一)

manifest.json
json
{
  // ...
  "chrome_url_overrides" : {
    "newTab": "newPage.html" // 覆写新建标签页
  },
  ...
}

为了提供更好的用户体验,用于替换页面的 HTML 文档应该遵循以下指引:

  • 页面文件大小应该较小,便于快速加载显示,避免使用同步访问网络或数据库资源,导致渲染阻塞
  • 包含明确的信息,告知用户当前浏览的是 Chrome 的特殊页面
  • 不要在新建标签页使用输入框聚焦功能,因为新建页面时,标签页的地址栏会首先获取焦点
提示

用以替换的页面可以引入样式文件或脚本文件,但支持行内脚本 inline JavaScript

Commands

参考

该交互控件是指使用快捷键操作扩展程序,可以通过快捷键激活 action 或执行特定的命令

提示

所有扩展程序的快捷键都在 chrome://extensions/shortcuts 中显示,用户可以在其中修改快捷键的组合值,或快捷键的局部与全局适用性

修改扩展程序的快捷键
修改扩展程序的快捷键

如果要使用该控件,需要先将这些快捷键在配置清单 manifest.json 的选项 commands 中进行声明/注册。

该选项的值是一个对象,每一个属性对应于一个命令,其属性名称是一个用于描述命令的名称,属性值则是一个对象用于描述该命令的相关信息

说明

其中 _execute_action 是保留属性,用以定义激活 action 的快捷键(对于 MV2 版本,则有 _execute_browser_action_execute_page_action 保留属性,分别设定激活 browser action 和 page action 的快捷键)

但是对于激活 action 的快捷键,它是无法触发 command.onCommand 事件的,所以无法后台脚本 background script中进行监听

可以在弹出框 popup 的脚本文件中,监听 DOMContentLoaded 事件来替代

命令描述对象包含了关于快捷键定义的信息,该对象一般有两个选项

  • suggested_key(可选)声明默认的快捷键
    其值可以是一个表示(跨平台适用的)快捷键的字符串,或是一个对象(以便针对不同的系统平台设定不同的快捷键,其中系统平台支持 defaultchromeoslinusmacwindows
    如果该选项省略,则该命令默认没有相应的触发快捷键,相应等待用户来设定后再生效
  • description 一段告知用户该快捷键功能作用的字符串,它会显示在扩展程序的快捷键管理界面中 chrome://extensions/shortcuts,对于标准快捷键 standard commands 它是必须的,对于 action commands 是可选的。
manifest.json
json
{
  // ...
  "commands": {
    "run-foo": {
      "suggested_key": {
        "default": "Ctrl+Shift+Y",
        "mac": "Command+Shift+Y"
      },
      "description": "Run \"foo\" on the current page."
    },
    "_execute_action": {
      "suggested_key": {
        "windows": "Ctrl+Shift+Y",
        "mac": "Command+Shift+Y",
        "chromeos": "Ctrl+Shift+U",
        "linux": "Ctrl+Shift+J"
      }
    },
  },
}

响应快捷键的逻辑代码一般编写在后台脚本 background script中,使用 chrome.commands.onCommand.addListener() API 监听快捷键,并触发相应的回调函数

manifest.json
json
{
  "name": "Tab Flipper",
  // ...
  "commands": {
    "flip-tabs-forward": {
      "suggested_key": {
        "default": "Ctrl+Shift+Right",
        "mac": "Command+Shift+Right"
      },
      "description": "Flip tabs forward"
    },
    "flip-tabs-backwards": {
      "suggested_key": {
        "default": "Ctrl+Shift+Left",
        "mac": "Command+Shift+Left"
      },
      "description": "Flip tabs backwards"
    }
  }
  // ...
}
background.js
js
chrome.commands.onCommand.addListener(command => {
  // 如果用户触发了扩展程序所注册的快捷键,则监听器会触发回调函数
  // 回调函数传入的参数 command 是命令的名称
  // command will be "flip-tabs-forward" or "flip-tabs-backwards"
  chrome.tabs.query({currentWindow: true}, tabs => {
    // Sort tabs according to their index in the window.
    tabs.sort((a, b) => a.index - b.index);
    const activeIndex = tabs.findIndex((tab) => tab.active);
    const lastTab = tabs.length - 1;
    let newIndex = -1;
    if (command === 'flip-tabs-forward') {
      newIndex = activeIndex === 0 ? lastTab : activeIndex - 1;
    } else {  // 'flip-tabs-backwards'
      newIndex = activeIndex === lastTab ? 0 : activeIndex + 1;
    }
    chrome.tabs.update(tabs[newIndex].id, {active: true, highlighted: true});
  });
});
快捷键冲突

当新安装的扩展程序所采用的快捷键的默认值,与已安装的其他扩展程序的快捷键冲突时,对于后来安装的扩展程序,浏览器就不会再注册这些快捷键,避免覆盖之前已存在的快捷键。

为了避免让用户觉得快捷键「无故失灵」的现象,我们应该采用以下更健壮的方法,在安装扩展程序时 chrome.runtime.onInstalled.addListener 先进行检查快捷键冲突,并告知用户

background.js
js
// Only use this function during the initial install phase.
// After installation the user may have intentionally unassigned commands.
// 在扩展程序安装完成后执行
chrome.runtime.onInstalled.addListener((reason) => {
  if (reason === chrome.runtime.OnInstalledReason.INSTALL) {
    checkCommandShortcuts();
  }
});

// 检查是否存在快捷键冲突
function checkCommandShortcuts() {
  // 获取当前的扩展程序所有命令
  chrome.commands.getAll((commands) => {
    let missingShortcuts = [];

    for (let {name, shortcut} of commands) {
      // 如果存在快捷键冲突
      // 则有些命令的快捷键的默认值就无法设置,即 shortcut 值为空字符串
      if (shortcut === '') {
        missingShortcuts.push(name);
      }
    }

    // 如果该扩展程序与其他扩展程序真的存在快捷键冲突
    if (missingShortcuts.length > 0) {
      // Update the extension UI to inform the user that one or more
      // commands are currently unassigned.
      // 以可视化的元素,如弹出通知,来告知用户
    }
  });
}

在为扩展程序设置(默认的)快捷键时,应该注意以下几点

  • 快捷键必须包含 CtrlAlt 两者之一,对大小写敏感,支持使用以下按键组合为快捷键:
    • 字母键 A-Z
    • 数字键 0~9
    • 标准的功能键
      • 通用键 Comma, Period, Home, End, PageUp, PageDown, Space, Insert, Delete
      • 方向键 Up, Down, Left, Right
    • 修饰键 Ctrl, Alt, Shift, MacCtrl (macOS only), Command (macOS only), Search (Chrome OS only)
    注意

    不支持 Tab 键,媒体键能与修饰键组合

  • 为了响应用户按下快捷键,需要在后台脚本 background script中使用 chrome.commands.onCommand.addListener API 进行监听。但是和标准的快捷键 standard commands 不同,对于激活 action 的快捷键,无法通过以上方法进行监听。
    可以在弹出框 popup 的脚本文件中,监听 DOMContentLoaded 事件来替代
  • 默认情况下,注册的快捷键是 Chrome 浏览器的局部快捷键,即只有当浏览器是当前系统的激活应用时(浏览器被选中的状态下),按下快捷键,扩展程序才响应
    也可以在快捷键定义对象中,通过属性 global: true 将该命令所对应的快捷键设定为全局快捷键
    建议

    建议注册全局快捷键限制在 Ctrl+Shift+[0..9] 这样的快捷键组合中,以避免覆盖掉其他系统级别的快捷键

    manifest.json
    json
    {
      // ...
      "commands": {
        "toggle-feature-foo": {
          "suggested_key": {
            "default": "Ctrl+Shift+5"
          },
          "description": "Toggle feature foo",
          "global": true
        }
      },
    }
    

    但是 Chrome OS 不支持扩展程序设定全局快捷键

定制浏览器体验

扩展程序可以使用很多相关的 API 以定制浏览器体验:

Bookmarks

参考

可以使用扩展程序定制浏览器的书签管理行为。除了可以使用覆写页面 Override Page 的方式定制书签管理页面,还可以使用 Chrome.bookmarks 一些列 API 操作书签。

如果要使用 Chrome.bookmarks API,需要先在配置清单 manifest.json 的选项 permissions 中进行相应的权限声明/注册

manifest.json
json
{
  // ...
  "permissions": ["bookmarks"]
}

书签数据是以树形结构组织的,每一个节点对象 bookmarks.BookmarkTreeNode 是一个书签或一个文件夹(也称为组),它具有多种属性,以下列出常用的几个:

  • id (必须)该节点的唯一标识符
  • title (必须)节点所展示的名称
  • children 所包含的子节点(数组)
  • index 该节点在父级文件夹中的索引(从 0 开始)
  • parentId 父级文件夹的唯一标识符(如果是根节点,则会省略该属性)
  • url 书签的链接,如果节点表示的是文件夹,则可以省略该属性
注意

根节点是一个文件夹,不能删除,且只能有一个。

有一些不能被重命名的特殊的节点(文件夹)

  • 📁 书签栏 Bookmarks Bar
  • 📁 其他书签 Other Bookmarks

使用方法 chrome.bookmarks.create() 创建一个书签节点

该方法接收两个参数

  • 第一个是节点的属性对象
  • 第二个(可选)参数是回调函数
js
// 创建一个文件夹
// 文件夹名称为 Extension bookmarks
chrome.bookmarks.create(
  {
    'parentId': bookmarkBar.id,
    'title': 'Extension bookmarks'
  },
  function(newFolder) {
    console.log("added folder: " + newFolder.title);
  },
);

// 创建一个书签
// 书签名称为 Extensions doc,指向的链接为 https://developer.chrome.com/docs/extensions
chrome.bookmarks.create({
  'parentId': extensionsFolderId,
  'title': 'Extensions doc',
  'url': 'https://developer.chrome.com/docs/extensions',
});

Browsing Data

参考

扩展程序可以删除特定时间段的用户浏览数据。

如果要使用 Chrome.browsingData API,需要先在配置清单 manifest.json 的选项 permissions 中进行相应权限声明/注册

manifest.json
json
{
  // ...
  "permissions": ["browsingData"]
}

使用方法 chrome.browsingData.remove() 删除用户浏览数据。

js
// 删除一周以来的数据
const millisecondsPerWeek = 1000 * 60 * 60 * 24 * 7;
const oneWeekAgo = (new Date()).getTime() - millisecondsPerWeek;

// 删除所有类型的数据
chrome.browsingData.removeCookies({
  "since": oneWeekAgo
}, callback);

const callback = function () {
  // Do something clever here once data has been removed.
};

// 或删除指定类型的的浏览数据
chrome.browsingData.remove({
  "since": oneWeekAgo
}, {
  "appcache": true,
  "cache": true,
  "cacheStorage": true,
  "cookies": true,
  "downloads": true,
  "fileSystems": true,
  "formData": true,
  "history": true,
  "indexedDB": true,
  "localStorage": true,
  "passwords": true,
  "serviceWorkers": true,
  "webSQL": true
}, callback);
Tip

除了传递第二个参数,以指定删除哪些类型的的数据,还可以调用专门的方法来删除相应类型的用户数据

还可以在该方法的第一个参数中设置属性 originsexcludeOrigins 以删除指定网页(或不包含指定网页)下的用户数据(适用于cookies、cache、storage (CacheStorage, FileSystems, IndexedDB, LocalStorage, ServiceWorkers, and WebSQL) 类型的数据)

js
chrome.browsingData.remove({
  "origins": ["https://www.example.com"]
}, {
  "cacheStorage": true,
  "cookies": true,
  "fileSystems": true,
  "indexedDB": true,
  "localStorage": true,
  "serviceWorkers": true,
  "webSQL": true
}, callback);
说明

由于 cookies 是基于域名的,所以删除特定网页的 cookies 时会删除该域名下的所有 cookies,例如删除 https://www.example.com 会删除 .example.com 域名下所有的 cookie

由于删除用户数据可能需要较长的时间(基于用户产生了多少操作历史数据),因此推荐设置回调函数(它在清除完成后执行),以可视化的信息提示,告知用户最新的状态。

注意

方法 chrome.browsingData.remove() 可能在清除完 cookie 后自动设置它们,以便让账户的数据同步功能正常运作,这样才可以让服务器中已同步的相应数据也覆盖清除掉

而使用方法 chrome.browsingData.removeCookies 清除 cookie 时,会停止同步功能。

Downloads

参考

扩展程序可以对浏览器中的下载功能进行操作,通过 chrome.downloads API 可以启动、监控、操作和搜索下载项。

如果要使用 Chrome.downloads API,需要先在配置清单 manifest.json 的选项 permissions 中进行相应权限声明/注册

manifest.json
json
{
  // ...
  "permissions": ["downloads"]
}

History

参考

扩展程序可以操作浏览器的历史记录。除了可以使用覆写页面 Override Page 的方式定制历史记录页面,还可以使用 Chrome.history 一些列 API 操作历史记录。

如果要使用 Chrome.history API,需要先在配置清单 manifest.json 的选项 permissions 中进行相应权限声明/注册

manifest.json
json
{
  // ...
  "permissions": ["history"]
}
提示

浏览的历史记录是通过不同类型的页面跳转 transition type 所产生的,例如用户从一个网页中点击一个链接跳转到另一个网页,这时候产生的历史记录的类型是 link

Tabs

参考

扩展程序可以创建、修改、重排标签页。

虽然使用 Chrome.tabs 部分的 API 可以不进行权限声明就直接使用,但是仍推荐在配置清单 manifest.json 的选项 permissions 中进行相应的权限声明/注册,因为需要访问标签的属性 urlpendingUrltitlefavIconUrl 时,仍然需要权限许可的。

manifest.json
json
{
  // ...
  "permissions": ["tabs"]
}

以下是一些常见的应用场景

  • 使用方法 chrome.tabs.create() 在新的标签页打开一个网页
    background.js
    js
    // 可以在安装成功后,打开扩展程序的 onboard 介绍页面
    chrome.runtime.onInstalled.addListener((reason) => {
      if (reason === chrome.runtime.OnInstalledReason.INSTALL) {
        chrome.tabs.create({
          url: 'onboarding.html'
        });
      }
    });
    
    注意

    在内容脚本 content scripts 中不能使用 chrome.tabs.create() 方法

  • 使用方法 chrome.tabs.query({ active: true, currentWindow: true }) 查询/获取符合条件的标签页
    background.js
    js
    async function getCurrentTab() {
      // 查询的条件 queryOptions 是当前的窗口正激活/选中的标签页
      let queryOptions = { active: true, currentWindow: true };
      // 查询并返回符合条件的标签页对象列表
      // 由于这里查询返回的页面只有一个,所以可以直接用解构来获取该标签页对象
      let [tab] = await chrome.tabs.query(queryOptions);
      return tab;
    }
    
    注意

    在内容脚本 content scripts 中不能使用 chrome.tabs.query() 方法

  • 使用方法 chrome.tabs.update() 更新标签页面的状态
    background.js
    js
    function toggleMuteState(tabId) {
      chrome.tabs.get(tabId, async (tab) => {
        // 获取标签页的静音状态,并进行切换
        let muted = !tab.mutedInfo.muted;
        // 更新标签页的静音状态
        await chrome.tabs.update(tabId, { muted });
        console.log(`Tab ${tab.id} is ${ muted ? 'muted' : 'unmuted' }`);
      });
    }
    
    注意

    在内容脚本 content scripts 中不能使用 chrome.tabs.get() 方法和 chrome.tabs.update() 方法

  • 使用方法 chrome.tabs.move() 重排标签页
    background.js
    js
    // 监听标签页激活事件(点击浏览器最顶部一栏进行激活标签页切换时会触发该事件)
    chrome.tabs.onActivated.addListener(activeInfo => move(activeInfo));
    
    // 将激活的标签页移到第一的位置
    async function move(activeInfo) {
      try {
        await chrome.tabs.move(activeInfo.tabId, {index: 0});
        console.log('Success.');
      } catch (error) {
        // 如果用户在拖拽标签页时
        if (error == 'Error: Tabs cannot be edited right now (user may be dragging a tab).') {
          setTimeout(() => move(activeInfo), 50);
        }
      }
    }
    

Windows

参考

扩展程序可以创建、修改、重排浏览器窗口

由于使用 Chrome.windows API 时,会获取标签页的相关信息 ,所以需要先在配置清单 manifest.json 的选项 permissions 中进行 tabs 的权限声明

manifest.json
json
{
  // ...
  "permissions": ["tabs"],
}
提示

当前窗口是指扩展程序目前执行代码所在的窗口,而不一定是浏览器的置顶窗口或当前聚焦窗口

例如扩展程序在一个窗口中打开了一个 HTML 文档,在文档中调用了 chrome.tabs.query() 方法,则当前窗口是指该 HTML 文档所在的窗口。

有些情况下对于后台脚本 background script 没有相应的当前窗口

定制网页体验

扩展程序可以使用很多相关的 API 以定制网页的体验

Active Tab

参考

在配置清单 manifest.json 的选项 permissions 中声明 activeTab 权限,可以临时获取许可,以访问当前激活的标签页,并在当前页面使用 tabs 相关的 API

manifest.json
json
{
  // ...
  "permissions": ["activeTab"]
}

由于扩展程序的很多使用场景都只需临时访问当前激活的标签页,而不是针对特定的网页,所以与基于 URL 规则获取的永久访问权限相比,该类型的权限更常用。

该权限基于用户的主动请求临时获取的(例如通过点击 Action 控件),而且仅限制在当前激活页面,相对而言更安全。

支持多种用户交互方式激活 activeTab 权限,以与当前激活的标签页进行交互

  • 通过 Action
    js
    // 点击浏览器工具栏上的扩展程序图标
    chrome.action.onClicked.addListener((tab) => {
      // 获取当前激活的标签页对象
      // 并植入内容脚本
      chrome.scripting.executeScript({
        target: { tabId: tab.id },
        function: reddenPage
      });
    });
    
    // 将网页背景色变为绿色
    function reddenPage() {
      document.body.style.backgroundColor = 'green';
    }
    
  • 通过 Context Menu
  • 通过 Commands
  • 通过 Omnibox
提示

当前标签页获取权限后,如果后续的导航是在同源 origin 中进行的,则权限依然生效,例如当前页面的 URL 从 https://example.com 切换到 https://example.com/foo

如果导航到其他源 origin,或关闭当前页面后,则需要重新获取权限

该权限一般用以替代在配置清单 manifest.json 的选项 host_permissions 中声明的 <all_urls> 权限,达到访问(任意 url)当前页面的效果,而且不会在安装扩展程序时弹出警告

without activeTab
without activeTab

with activeTab
with activeTab

Content Scripts

参考

内容脚本 content script 是运行在网页中的 JavaScript 代码,它除了可以访问页面的 DOM 对象,还可以以通过信息传递 message passing 的方式与扩展程序进行通讯,可以将它看作是页面与扩展程序之间的桥梁角色。

可使用的 API

除了借助扩展程序(通过 message passing 的方式)间接调用 Chrome 提供的 API,它还可以直接访问部分 API

例如可以将扩展程序的静态资源展示在网页上

js
// Code for displaying <extensionDir>/images/myimage.png

// 访问扩展程序的静态资源(图片)
var imgURL = chrome.runtime.getURL("images/myimage.png");
// 将图片展示在运行着内容脚本的页面上
document.getElementById("someImage").src = imgURL;

独立运行环境

内容脚本 content script 在一个独立的环境中执行(私有作用域),因此页面和扩展程序都无法访问内容脚本 content script 的变量,可以避免与页面或扩展程序的脚本发生冲突。

如果页面原来就有一个按钮点击的事件监听器

html
<html>
  <button id="mybutton">click me</button>
  <script>
    const greeting = "hello, ";
    const button = document.getElementById("mybutton");
    button.person_name = "Bob";
    button.addEventListener("click", () =>
      alert(greeting + button.person_name + ".")
    , false);
  </script>
</html>

在内容脚本植入到页面后,设置另一个事件监听器

js
const greeting = "hola, ";
const button = document.getElementById("mybutton");
button.person_name = "Roberto";
button.addEventListener("click", () =>
  alert(greeting + button.person_name + ".")
, false);

两个脚本互不冲突,所以最后点击一次按钮后,会有两次 alert 弹出。

注意

虽然内容脚本 content scripts 运行在独立环境,但是它可以与网页交互的这个特点,仍很可能会被恶意程序利用

因此在内容脚本 content scripts 中请求访问外部服务器的数据时,需要谨慎,避免 XSS 攻击中间人攻击

植入脚本

内容脚本 content script 有两种方式植入到网页中

  • 静态植入:通过显式声明的方式将脚本植入页面
  • 动态植入:通过编程的方式按需将脚本植入到页面
静态植入

在配置清单 manifest.json 的选项 content_scripts 中声明的 JavaScript 脚本文件和 CSS 样式文件,会植入到匹配的网页

  • match (必须)属性:包含一系列匹配规则的数组,用以筛选/匹配出那些需要静态植入内容脚本的网页
  • exclude_match 属性:匹配的网页将会被排除,不会植入脚本
  • css 属性:包含一系列样式文件(路径)的数组,样式文件会在页面的 DOM 构建完成之前,按照数组元素的顺序依次植入到网页里
  • js 属性:包含一系列脚本文件(路径)的数组,脚本按照数组元素的顺序依次植入到网页里
  • match_about_blank 属性:一个布尔值,默认值为 false,控制内容脚本 content script 是否植入到 about:blank 空页面中(当空页面与 match 条件之一匹配时)
  • run_at 属性:设置脚本植入的时间点,默认值是 document_idle,让浏览器决定在 document_end(DOM 树构建完成时)和 window.onload 事件触发之间择时植入;还可以设置为 document_startdocument_end
  • all_frames 属性:一个布尔值,用以控制植入的脚本是否在标签页中所有的 frame 中运行,默认值是 false,即脚本只是植入到主框架中
manifest.json
json
{
  // ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "css": ["my-styles.css"],
      "js": ["content-script.js"]
    }
  ],
}
提示

还有一种设置网页匹配规则的方式是属性 include_globsexclude_globs,它们的规则更加灵活

动态植入

在后台脚本中就可以使用 chrome.scripting.executeScript() 方法为页面动态植入代码,需要在配置清单 manifest.json 中先声明相应的权限

manifest.json
json
{
  // ...
  "permissions": ["scripting"],
}

为了可以在扩展程序运行时以编程的方式动态地在页面植入脚本,需要在配置清单 manifest.json 的选项 host_permissions 中获取访问相应页面的权限;或者在选项 permissions 中声明 activeTab 权限,这样就可以为当前激活页面植入脚本

manifest.json
json
{
  // ...
  "permissions": [
    "scripting",
    "activeTab"
  ],
  "background": {
    "service_worker": "background.js"
  }
}
content-script.js
js
// 需要植入的代码
document.body.style.backgroundColor = 'orange';
background.js
js
chrome.action.onClicked.addListener((tab) => {
  // 在指定的标签页中植入指定的脚本或样式文件
  chrome.scripting.executeScript({
    target: { tabId: tab.id },
    files: ['content-script.js']
  });
});

方法 chrome.scripting.executeScript(settingObj, callbackFn) 可以接收两个参数

  • 第一个参数 settingObj 是配置对象,有多个属性
    • target (必须)属性:设置植入目标对象(标签页)
      植入的脚本默认只运行在主框架中,由目标对象的属性 allFrames(一个布尔值)控制,它的默认值是 false,即注入的脚本并不是运行在标签页里的所有 frame
      提示

      如果希望注入的脚本运行在指定的 frame 中,可以在设置 target 时提供 frameIds 属性 例如 target: { tabId: tabId, frameIds: frameIds }

    • files 属性:一个包含脚本文件(路径)或样式文件(路径)的数组(💡 当前最多只支持一个文件)
      如果植入的是 JavaScript 脚本,还支持指定需要执行的函数
      js
      function getTitle() {
        return document.title;
      }
      
      const tabId = getTabId();
      
      chrome.scripting.executeScript(
        {
          target: {tabId: tabId},
          func: getTitle,
        },
        () => { ... }
      );
      
      注意

      如果植入的是函数,它并不会携带在定义处的上下文,因此函数中类似 this 和指向函数外部作用域的变量会失效而报错。

      js
      const color = getUserColor();
      
      function changeBackgroundColor() {
        document.body.style.backgroundColor = color;
      }
      
      const tabId = getTabId();
      
      chrome.scripting.executeScript(
        {
          target: {tabId: tabId},
            func: changeBackgroundColor,
          },
        () => { ... }
      );
      
      // result in a ReferenceError
      // because `color` is undefined when the function executes
      

      如果希望使用函数作用域外部的变量,可以通过选项 args 预先声明,它会作为参数传入到函数中

      js
      const color = getUserColor();
      
      function changeBackgroundColor(backgroundColor) {
        document.body.style.backgroundColor = backgroundColor;
      }
      
      const tabId = getTabId();
      
      chrome.scripting.executeScript(
        {
          target: {tabId: tabId},
          func: changeBackgroundColor,
          args: [color],
        },
        () => { ... }
      );
      
  • 第二个参数 callbackFn 是回调函数,它接收包含脚本执行完成后返回值的数组作为入参(有多个返回值是因为植入脚本可以在标签页的所有 frame 中都运行),其中主框架的返回值是数组的第一个元素,而其他 frame 的返回值在数组中的排序是乱序的
    js
    function getTitle() {
      return document.title;
    }
    
    const tabId = getTabId();
    
    chrome.scripting.executeScript(
      {
        target: { tabId: tabId, allFrames: true },
        func: getTitle,
      },
      (injectionResults) => {
        for (const frameResult of injectionResults)
          console.log('Frame Title: ' + frameResult.result);
      }
    );
    

还可以使用方法 chrome.scripting.insertCSS() 专门用于植入样式文件,除了支持以文件数组的方式,还支持字符串的形式,对于简单的样式这个方式更方便。

该方法是植入样式到页面的,所以没有返回值作为回调函数的入参

js
const css = 'body { background-color: red; }';
const tabId = getTabId();
chrome.scripting.insertCSS(
  {
    target: {tabId: tabId},
    css: css,
  },
  () => { ... }
);

监听网页消息

可以借助内容脚本 content scripts,实现从普通的网页向扩展程序传递信息

首先使用方法 window.postMessage() 向网页自身传递信息

然后在内容脚本 content scripts 中监听消息事件 message 以捕获当前页面发送给自己的信息,而在消息事件的处理函数中,内容脚本 content scripts 就可以基于拦截的信息,执行相应的操作,例如与扩展程序进行交互

js
// content scripts
const port = chrome.runtime.connect();

window.addEventListener("message", (event) => {
  // We only accept messages from ourselves
  if (event.source != window) {
    return;
  }

  if (event.data.type && (event.data.type == "FROM_PAGE")) {
    console.log("Content script received: " + event.data.text);
    port.postMessage(event.data.text);
  }
}, false);
html
<!-- web page -->
<button>send message</button>

<script>
document.getElementById("theButton").addEventListener("click", () => {
  window.postMessage({ type: "FROM_PAGE", text: "Hello from the webpage!" }, "*");
}, false);
</script>

Cross-Origin XHR

内容脚本 content script 以植入网页的方式运行,因此它受到同源策略的限制

说明

扩展程序的后台脚本 background scripts 不受同源策略限制,只需要在 host_permissions 中声明后就可以访问相应的远程服务器;如果获取的是扩展程序内部的静态资源,则不需要声明权限

如果希望在扩展程序访问外部服务器,由于在 MV3 版本中,后台脚本 background script 运行在 Service Workers 中,没有全局变量 window 对象,因此无法使用 XMLHttpRequest ,但可以使用方法 fetch() 发起网络请求

由于内容脚本 content scripts 受到同源政策的限制,可以通过信息传递 message passing 借助扩展程序来 fetch 相应的服务器获取数据。

content-script.js
js
chrome.runtime.sendMessage(
  {
    contentScriptQuery: 'fetchUrl',
    url: 'https://another-site.com/price-query?itemId=' +
              encodeURIComponent(request.itemId)},
  response => parsePrice(response.text())
);
background.js
js
chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    if (request.contentScriptQuery == 'fetchUrl') {
      // WARNING: SECURITY PROBLEM - a malicious web page may abuse
      // the message handler to get access to arbitrary cross-origin
      // resources.
      fetch(request.url)
        .then(response => response.text())
        .then(text => sendResponse(text))
        .catch(error => ...)
    return true;  // Will respond asynchronously.
  }
});
注意

但是这种处理方式会让恶意网页有可乘之机,它们可以伪造信息请求,而让扩展程序访问指定的服务器。

应该采用更安全的方式,不要通过信息传递 message passing 的方式直接传递完整的 URL,而是传递部分 query 参数,这样就可以在扩展程序中预设(限制)可以访问的域名

js
// content-script.js
chrome.runtime.sendMessage(
  {
    contentScriptQuery: 'queryPrice',
    itemId: 12345
  },
  price => {...}
);
js
// backgound.js
chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    if (request.contentScriptQuery == 'queryPrice') {
      var url = 'https://another-site.com/price-query?itemId=' +
            encodeURIComponent(request.itemId);
      fetch(url)
        .then(response => response.text())
        .then(text => parsePrice(text))
        .then(price => sendResponse(price))
        .catch(error => ...)
    return true;  // Will respond asynchronously.
  }
});

在扩展程序中使用从外部服务器的返回的数据时,需要谨慎处理,应该更安全的逻辑代码 和 API

例如对于以下不安全的处理方法

js
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // WARNING! Might be evaluating an evil script!
    var resp = eval("(" + xhr.responseText + ")");
    ...
  }
}
xhr.send();
js
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // WARNING! Might be injecting a malicious script!
    document.getElementById("resp").innerHTML = xhr.responseText;
    ...
  }
}
xhr.send();

应该改用更安全的方式

js
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // JSON.parse does not evaluate the attacker's scripts.
    var resp = JSON.parse(xhr.responseText);
  }
}
xhr.send();
js
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    // textContent does not let the attacker inject HTML elements.
    document.getElementById("resp").textContent = xhr.responseText;
  }
}
xhr.send();

定制开发调试工具

可以使用 chrome.debugger API 或 Chrome 内置的开发者工具 DevTools 进行开发调试(但两者不可混用)

可以将 debugger 附加到一个或多个标签页中,以检测网络交互、调试 JavaScript、改变 DOM 和 CSS 等,具体使用方法可以查看官方文档示例(MV2 版本)

扩展程序也可以为 Chrome 的开发者工具 DevTools 增添功能,例如添加新的 UI 面板和侧边栏,与被检查的页面交互,获取有关网络请求的信息等。

这一类的扩展程序除了一般常用的构成部分以外,还需要使用另一套 DevTools 特有的 API,还有一个 DevTools 页面

DevTools-extension
DevTools-extension

在 MV3 版本中,background page 已取消,后台脚本 background scripts 运行在 service workers 中

关于增添开发者工具 DevTools 功能的扩展程序的开发可以参考官方文档示例(MV2 版本),以及一些实例

扩展程序发布

扩展程序主要是通过 Chrome 网上应用店进行分发

开发者通过 Developer Dashboard 平台,将扩展程序项目打包 .zip 文件进行上传;然后该平台会对扩展程序进行安全审核以保护用户,审核成功后扩展程序会转换成后缀为 .crx 的特殊 ZIP 压缩包,再分发给用户

提示

关于扩展程序的更新,Chrome 浏览器会定期检查已安装扩展程序的版本,如果发现新版本就会进行更新,无需用户干预。扩展程序的版本在配置清单 manifest.json 的选项 version 中设置

manifest.json
json
{
  ...
  "version": "1.5",
  ...
}

通过提升数值来表示新版本

manifest.json
json
{
  ...
  "version": "1.5",
  ...
}

然后将新的扩展程序项目打包为 .zip 文件,在 Developer Dashboard 平台中找到相应扩展程序的旧版本,选择 Edit 上传压缩包,再点击 Publish 进行发布。

提示

其他扩展程序的分发方式可以参考官方文档


Copyright © 2024 Ben

Theme BlogiNote

Icons from Icônes