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 文档的路径(它必须在项目的根目录下)。
{
// ...
"background": {
"service_worker": "background.js"
},
}
后台脚本在 Service Workers 中运行基于监听事件-响应模型来执行操作,因此逻辑代码的编写也应该遵循该模型以优化性能:
- 在事件循环的第一轮中完成事件监听的注册,即将事件监听程序写在后台脚本最顶层的作用域中,而不应该内嵌在其他的逻辑代码中(因为 Service Workers 执行完代码会终止而不会长期驻留,当有事件需要分派时它才再次运行,如果未能在第一次事件轮询中注册监听器,这就无法响应事件)。background.jsjs
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 APIbackground.jsjs
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 版本中使用
setTimeout
或setInterval
实现延迟或定期操作,在 Manifest V3 版本的 Service Workers 中并不可行,因为 Service Worker 并不会长期驻留后台,当它终止时调度程序会注销计时器。
应该使用 Alarms API 来替代,它用以安排代码定期运行或在未来的指定时间运行。它也需要在后台脚本最顶层的作用域中注册。background.jsjs// 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 对象的环境,或借助一些库(如 jsdom 或 undom)弥补这个缺失。 - 在 Service Workers 中无法播放或捕获多媒体资源,可以通过
chrome.windows.create()
和chrome.tabs.create()
API 创建一个标签页,以提供一个具有 window 对象的环境,然后可以通过消息传递 message passing 在 service worker 中控制页面的多媒体播放。 - 虽然在 Service Worker 中无法访问 DOM,但对于
<canvas>
画布元素,可以通过OffscreenCanvas
API 创建一个 canvas。关于 OffscreenCanvas 相关信息可以参考这里。background.jsjs// for MV3 service workers function buildCanvas(width, height) { const canvas = new OffscreenCanvas(width, height); return canvas; }
Message Passing
参考
- Message passing
- 关于 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()
方法进行单次请求,发送信息
这两个方法都可以设置回调函数(默认接收返回的响应数据作为参数),以接收返回的信息进行后续处理
// 在页面脚本 content script 发送信息
chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
// 接收并处理返回的信息
console.log(response.farewell);
});
// 在扩展程序发送信息
// 在后台代码 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()
监听信息事件,以捕获请求/信息
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()
则是监听信息事件,以便接收信息
/**
* 发起端
*/
// 该代码片段编写在页面脚本 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
端口对象作为入参,然后接收端也可以使用这一相同的端口,在通道中发送和接收消息,这样通道两端的接口就可以相互接收和发送信息
/**
* 接收端
*/
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)
发起通道连接请求,并返回端口对象
// 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()
响应连接,并使用端口对象接收和发送信息
// 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
中声明/注册,希望与哪些外部网页进行连接(可以使用正则表达式,以支持一系列符合一定规则的网页,但至少包含二级域)
{
// ...
"externally_connectable": {
"matches": ["https://*.example.com/*"]
}
}
在网页使用方法 chrome.runtime.sendMessage()
或方法 chrome.runtime.connect()
发送信息(通过 ID 来指定与哪一个扩展程序进行通讯)
// 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()
监听信息
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", }
然后用户可以在浏览器工具栏的扩展程序图标右键点击选择「选项」打开设置页面
或者在扩展程序的「详情」页面中选择「扩展程序选项」打开设置页面 - 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()
以编程式的方法打开设置页面
<!-- 点击按钮打开扩展程序的设置页面 -->
<button id="go-to-options">Go to options</button>
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.connect()
方法和chrome.tabs.sendMessage()
方法,❓ 即无法从扩展程序向设置页面发送信息
但chrome.runtime.connect()
方法和chrome.runtime.sendMessage()
方法并不受限,即可从设置页面向扩展程序发送信息
提示
为了可以让用户的设置数据持久化及跨设备同步,可以使用 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()
动态申请,这样就可用基于用户主动交互,例如在按钮的点击事件的处理函数中,按需获取权限如果申请的权限会触发警告提示,则会弹出一个权限提示框询问用户许可,并等待结果返回再执行后续的代码
jsdocument.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 的,用于声明扩展程序可以去访问哪些主机
然后浏览器会在扩展程序安装时或在运行时,询问用户是否允许它获取相应的权限、访问特定的资源,让用户可以有主动权去保护自己的数据
{
// ...
"permissions": [
"tabs",
"bookmarks",
"unlimitedStorage"
],
"optional_permissions": [
"unlimitedStorage"
],
"host_permissions": [
"http://www.blogger.com/",
"http://*.google.com/"
],
}
提示
可以使用方法 chrome.permissions.contains()
查看扩展程序当前是否拥有某项权限,使用方法 chrome.permissions.getAll()
获取扩展程序当前具有的所有权限
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()
动态添加相应的权限,此时扩展程序并不会再告知/询问用户
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). Thestorage.managed
storage is read-only
有三种不同的存储域 ❓ StorageArea
sync
同步存储的数据local
本地存储的数据session
会话存储的数据(存储在内存里)managed
管理员设置的数据,只读
如果要使用该 API 需要先在配置清单 manifest.json
的选项 permissions
中声明/注册相应的权限
{
// ...
"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
所对应的数据
// 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);
});
// 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
事件,可以对其进行监听以作出响应
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 元素或 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">
- 文本:选择合适的字体和字号大小以确保文本内容的可读性,而且应该考虑视觉障碍者的使用体验,他们可能需要设置更大的字体,避免将自定义的快捷键与页面缩放的快捷键相冲突。为了确保版型的稳定,应该测试缩放至 200%,查看页面是否还显示正常。应该避免将文字嵌入到图片中,这样不仅无法调整字体,还无法让屏幕阅读器识别;如果必须将文字嵌入图片中,应该在图片的可替代文字(属性
更多相关资源
Internationalization
参考
chrome.i18n
API- 关于 Internationalization 官方示例:
国际化 I18N 是指在开发扩展程序时,可以让用户按需切换语言
多语言版本的开发
关于软件多语言版本的开发:
- 国际化 Internationalization,L18N:软件国际化是在软件设计和文档开发过程中,使得功能和代码设计能处理多种语言和文化传统,使创建不同语言版本时,不需要重新设计源程序代码的软件工程方法。
- 本地化 localization ,L10N:能够使网站、Web 应用或任何其他形式的内容适用于特定语言的范围和文化圈。
为了让扩展程序更方便地实现国际化,需要遵循以下建议:
- 将所有用户可见的内容(文本)放在一个
messages.json
文档中,并将该文档放在_locales/_localeCode_
相应的目录下(其中_localeCode_
是一个语言编码,例如英语就应该使用en
表示)。提示
如果项目中有
_locales
目录,需要在配置清单manifest.json
的选项default_locale
中声明默认语言,可以在这里查看具体的语言代码。
在messages.json
文档中,它以键值对的形式来定义内容,每一个键都包含一个需要国际化的项(大小写敏感),值是一个对象,其中常用的属性如下:- 属性
message
是需要显示的内容 - (可选)属性
description
是对该项的描述,便于告知翻译者该项的意义。
messages.jsonjson{ "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 中使用扩展程序中的静态图片作为背景
cssbody { background-image:url('chrome-extension://__MSG_@@extension_id__/background.png'); }
- 属性
- 然后就可以在项目的其他文档中,通过 message 项来使用相应的数据:
- 在
manifest.json
或 CSS 文档中,通过__MEG_messagename__
来使用 message 相应的项messagename
- 在扩展程序页面或 JavaScript 脚本中,通过方法
chrome.i18n.getMessage("messagename")
获取相应的项messagename
Tip
每次新增一种语言,就需要新增一个 message 文件到
_locales/_localeCode_
目录里。每一种语言中,不必包含与默认语言中定义的全部 message 项。
以上示例是支持英语、西班牙语、韩语的扩展程序项目文件结构Tip
浏览器会基于用户的语言设置和扩展程序的
default_locale
设置来自动选择采用哪一个messages.json
文档 - 在
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 文件
{
// ...
"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 创建图片。⚠️ 该方法是用于设置静态图片,而不应该用于设置动图。
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()
通过编程的方式进行设置
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 文件的路径。
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)
设置标记的背景色
// 设置 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 内容
function getTabId() { /* ... */}
function getTabBadge() { /* ... */}
chrome.action.setBadgeText(
{
text: getTabBadge(tabId),
tabId: getTabId(),
},
() => { ... }
);
Context Menu
参考
chrome.contextMenus
API- 关于 Context Menu 官方示例
该交互控件是指浏览器的右键菜单,可以通过扩展程序往其中添加选项,除了可以针对整个页面,还可以针对特定的 DOM 元素,或 action 图标,添加右键菜单项。
如果要使用该控件,需要先在配置清单 manifest.json
的选项 permissions
中进行声明/注册相关的权限。而且为了便于将菜单选项(通过图标指示)与扩展程序相匹配,需要在配置清单的选项 icons
中指定图标文件,最好提供多种尺寸的图片文件
{
// ...
"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()
(该钩子函数会在安装扩展程序完成时调用),以便安装完成时,右键菜单选项即可用
// 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
属性:为当前菜单选项分配一个唯一 IDtitle
属性:必须(除非该菜单选项的类型是分割线type: "separator"
)设置,作为菜单项的内容显示出来type
属性:菜单选项的类型,默认值是normal
,就是正常的菜单选项,还可以是checkbox
、radio
、separator
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
参考
chrome.omnibox
API- 关于 Omnibox 的官方示例
该交互控件是指地址栏上的搜索关键词,当用户在地址栏输入相应的关键词,就会触发 Omnibox(唤起相应的扩展程序),接下来用户在地址栏中输入内容是直接与该扩展程序进行交互。一般会在扩展程序中预先设置一系列的搜索建议,当用户输入的内容模糊匹配成功时,就会在一个 dropdown 中显示相应的搜索建议。
如果要使用该控件,需要先在配置清单 manifest.json
的选项 omnibox
中进行声明注册。当 Omnibox 被触发时,扩展程序图标(以灰阶的形式展示)和名称会显示在地址栏的左侧,了便于识别所以需要在配置清单的选项 icons
中指定图标文件,最好提供多种尺寸的图片文件(默认使用高和宽为 16px
的图标)
处理该控件的交互逻辑的代码则是在后台脚本 background script的 service worker 中设置,基于事件监听-响应的原理
{
// ...
"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 交互控件一般是通过监听相关的事件实现的
监听用户输入
使用 onInputChanged
可监听用户的输入。在地址栏进入 Omnibox 模式后,当用户在地址栏中输入内容时会触发相应的回调函数
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
在用户选择执行一个建议选项后,触发回调函数
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
参考
- Overriding Chrome pages
- 关于 Override Pages 的官方示例
- historyOverride(MV2 版本)
- blank_ntp(MV2 版本)
- override_igoogle(MV2 版本)
该交互控件是通过覆写页面实现的,扩展程序可以覆写三个 Chrome 的特殊页面:
- Bookmark Manager:书签管理页面
chrome://bookmarks
- History:历史记录页面
chrome://history
- New Tab:新建标签页
chrome://newtab
说明
每一个扩展程序只能覆写以上三个特殊 Chrome 页面之一,而每一种特殊 Chrome 页面,只能选择被一个扩展程序进行覆写
此外隐身模式下,新建标签页不能被覆写
如果要使用该控件,需要先在配置清单 manifest.json
的选项 chrome_url_overrides
中进行声明/注册,指定需要覆写的页面(bookmarks
、history
、newtab
三者之一)
{
// ...
"chrome_url_overrides" : {
"newTab": "newPage.html" // 覆写新建标签页
},
...
}
为了提供更好的用户体验,用于替换页面的 HTML 文档应该遵循以下指引:
- 页面文件大小应该较小,便于快速加载显示,避免使用同步访问网络或数据库资源,导致渲染阻塞
- 包含明确的信息,告知用户当前浏览的是 Chrome 的特殊页面
- 不要在新建标签页使用输入框聚焦功能,因为新建页面时,标签页的地址栏会首先获取焦点
提示
用以替换的页面可以引入样式文件或脚本文件,但不支持行内脚本 inline JavaScript
Commands
参考
chrome.commands
API- 关于 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
(可选)声明默认的快捷键
其值可以是一个表示(跨平台适用的)快捷键的字符串,或是一个对象(以便针对不同的系统平台设定不同的快捷键,其中系统平台支持default
、chromeos
、linus
、mac
、windows
)
如果该选项省略,则该命令默认没有相应的触发快捷键,相应等待用户来设定后再生效description
一段告知用户该快捷键功能作用的字符串,它会显示在扩展程序的快捷键管理界面中chrome://extensions/shortcuts
,对于标准快捷键 standard commands 它是必须的,对于 action commands 是可选的。
{
// ...
"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 监听快捷键,并触发相应的回调函数
{
"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"
}
}
// ...
}
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
先进行检查快捷键冲突,并告知用户
// 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.
// 以可视化的元素,如弹出通知,来告知用户
}
});
}
在为扩展程序设置(默认的)快捷键时,应该注意以下几点
- 快捷键必须包含
Ctrl
或Alt
两者之一,对大小写敏感,支持使用以下按键组合为快捷键:- 字母键
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.jsonjson{ // ... "commands": { "toggle-feature-foo": { "suggested_key": { "default": "Ctrl+Shift+5" }, "description": "Toggle feature foo", "global": true } }, }
但是 Chrome OS 不支持扩展程序设定全局快捷键
定制浏览器体验
扩展程序可以使用很多相关的 API 以定制浏览器体验:
- Bookmarks 书签管理
- Browsing Data 用户浏览数据
- Downloads 下载项管理
- Font Settings 字体设置
- History 浏览历史记录
- Privacy 隐私权限设置
- Proxy 代理设置
- Sessions 从浏览会话中查询和恢复标签页或窗口
- Tabs 创建、修改、重排标签页
- Top Sites 访问用户最常浏览的网页
- Themes 改变浏览器外观与主题
- Windows 创建、修改、重排浏览器窗口
Bookmarks
参考
Chrome.bookmarks
API- 关于 Bookmarks 的官方示例
- bookmarks basic(MV2 版本)
- bookmarks
可以使用扩展程序定制浏览器的书签管理行为。除了可以使用覆写页面 Override Page 的方式定制书签管理页面,还可以使用 Chrome.bookmarks
一些列 API 操作书签。
如果要使用 Chrome.bookmarks
API,需要先在配置清单 manifest.json
的选项 permissions
中进行相应的权限声明/注册
{
// ...
"permissions": ["bookmarks"]
}
书签数据是以树形结构组织的,每一个节点对象 bookmarks.BookmarkTreeNode
是一个书签或一个文件夹(也称为组),它具有多种属性,以下列出常用的几个:
id
(必须)该节点的唯一标识符title
(必须)节点所展示的名称children
所包含的子节点(数组)index
该节点在父级文件夹中的索引(从0
开始)parentId
父级文件夹的唯一标识符(如果是根节点,则会省略该属性)url
书签的链接,如果节点表示的是文件夹,则可以省略该属性
注意
根节点是一个文件夹,不能删除,且只能有一个。
有一些不能被重命名的特殊的节点(文件夹)
- 📁 书签栏
Bookmarks Bar
- 📁 其他书签
Other Bookmarks
使用方法 chrome.bookmarks.create()
创建一个书签节点
该方法接收两个参数
- 第一个是节点的属性对象
- 第二个(可选)参数是回调函数
// 创建一个文件夹
// 文件夹名称为 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- 关于 Browsing Data 的官方示例(MV2 版本)
扩展程序可以删除特定时间段的用户浏览数据。
如果要使用 Chrome.browsingData
API,需要先在配置清单 manifest.json
的选项 permissions
中进行相应权限声明/注册
{
// ...
"permissions": ["browsingData"]
}
使用方法 chrome.browsingData.remove()
删除用户浏览数据。
// 删除一周以来的数据
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
除了传递第二个参数,以指定删除哪些类型的的数据,还可以调用专门的方法来删除相应类型的用户数据
还可以在该方法的第一个参数中设置属性 origins
或 excludeOrigins
以删除指定网页(或不包含指定网页)下的用户数据(适用于cookies、cache、storage (CacheStorage, FileSystems, IndexedDB, LocalStorage, ServiceWorkers, and WebSQL) 类型的数据)
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- 关于 Downloads 的官方示例(MV2 版本)
扩展程序可以对浏览器中的下载功能进行操作,通过 chrome.downloads
API 可以启动、监控、操作和搜索下载项。
如果要使用 Chrome.downloads
API,需要先在配置清单 manifest.json
的选项 permissions
中进行相应权限声明/注册
{
// ...
"permissions": ["downloads"]
}
History
参考
chrome.history
API- 关于 History 的官方示例(MV2 版本)
扩展程序可以操作浏览器的历史记录。除了可以使用覆写页面 Override Page 的方式定制历史记录页面,还可以使用 Chrome.history
一些列 API 操作历史记录。
如果要使用 Chrome.history
API,需要先在配置清单 manifest.json
的选项 permissions
中进行相应权限声明/注册
{
// ...
"permissions": ["history"]
}
提示
浏览的历史记录是通过不同类型的页面跳转 transition type 所产生的,例如用户从一个网页中点击一个链接跳转到另一个网页,这时候产生的历史记录的类型是 link
。
Tabs
参考
chrome.tabs
API- 关于 Tabs 的官方示例(MV2 版本)
扩展程序可以创建、修改、重排标签页。
虽然使用 Chrome.tabs
部分的 API 可以不进行权限声明就直接使用,但是仍推荐在配置清单 manifest.json
的选项 permissions
中进行相应的权限声明/注册,因为需要访问标签的属性 url
、pendingUrl
、title
、favIconUrl
时,仍然需要权限许可的。
{
// ...
"permissions": ["tabs"]
}
以下是一些常见的应用场景
- 使用方法
chrome.tabs.create()
在新的标签页打开一个网页background.jsjs// 可以在安装成功后,打开扩展程序的 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.jsjsasync 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.jsjsfunction 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.jsjs// 监听标签页激活事件(点击浏览器最顶部一栏进行激活标签页切换时会触发该事件) 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- 关于 Windows 的官方示例
- merge_windows(MV2 版本)
- inspector(MV2 版本)
扩展程序可以创建、修改、重排浏览器窗口
由于使用 Chrome.windows
API 时,会获取标签页的相关信息 ,所以需要先在配置清单 manifest.json
的选项 permissions
中进行 tabs
的权限声明
{
// ...
"permissions": ["tabs"],
}
提示
当前窗口是指扩展程序目前执行代码所在的窗口,而不一定是浏览器的置顶窗口或当前聚焦窗口
例如扩展程序在一个窗口中打开了一个 HTML 文档,在文档中调用了 chrome.tabs.query()
方法,则当前窗口是指该 HTML 文档所在的窗口。
有些情况下对于后台脚本 background script 没有相应的当前窗口
定制网页体验
扩展程序可以使用很多相关的 API 以定制网页的体验
- Active Tab 可以直接访问当前的激活标签页,而不需要先为特定的域名设置权限
host_permission
(或使用<all_urls>
为所有域名设置权限) - Content Settings 设置网页可以运行的功能,例如 cookies、JavaScript、plugins、camera 等许可设置 💡官方示例(MV2 版本)
- Content Scripts 内容脚本,植入到网页中运行的脚本
- Cookies 获取或修改相应域名下的 cookie 💡 官方示例(MV2 版本)
- Cross-Origin XHR 向远程服务器发送和接收数据
- Desktop Capture 捕获屏幕、单个窗口或标签页的内容
- Page Capture 将标签页的源信息保存为 MHTML
- Tab Capture 与标签页的媒体流进行交互
- Web Navigation 实时更新导航请求的状态 💡 官方示例(MV2 版本)
- Declarative Net Request 声明规则让 Chrome 实时拦截、阻止或修改网络请求 💡 官方示例
Active Tab
参考
- The activeTab permission
- 关于 Windows 的官方示例
在配置清单 manifest.json
的选项 permissions
中声明 activeTab
权限,可以临时获取许可,以访问当前激活的标签页,并在当前页面使用 tabs 相关的 API。
{
// ...
"permissions": ["activeTab"]
}
由于扩展程序的很多使用场景都只需临时访问当前激活的标签页,而不是针对特定的网页,所以与基于 URL 规则获取的永久访问权限相比,该类型的权限更常用。
该权限基于用户的主动请求临时获取的(例如通过点击 Action 控件),而且仅限制在当前激活页面,相对而言更安全。
支持多种用户交互方式激活 activeTab
权限,以与当前激活的标签页进行交互
- 通过 Actionjs
// 点击浏览器工具栏上的扩展程序图标 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)当前页面的效果,而且不会在安装扩展程序时弹出警告。
Content Scripts
参考
- Content scripts
- 关于 Windows 的官方示例
内容脚本 content script 是运行在网页中的 JavaScript 代码,它除了可以访问页面的 DOM 对象,还可以以通过信息传递 message passing 的方式与扩展程序进行通讯,可以将它看作是页面与扩展程序之间的桥梁角色。
可使用的 API
除了借助扩展程序(通过 message passing 的方式)间接调用 Chrome 提供的 API,它还可以直接访问部分 API
chrome.i18n
APIchrome.storage
API- runtime API:
chrome.runtime.getManifest()
返回一个扩展程序的配置清单对象chrome.runtime.getURL(relatviePath)
将相对于扩展程序的路径转换为绝对路径(基于扩展程序的安装信息)id
扩展程序的唯一标识符chrome.runtime.connect()
发起(与指定的扩展程序间)建立长连接的信息通道的请求提示
而如果是从扩展程序发起长连接请求,则使用方法
chrome.tabs.connect()
chrome.runtime.onConnect
监听并响应扩展程序发起长连接的请求chrome.runtime.sendMessage
传递单次请求信息chrome.runtime.onMessage
监听单次请求信息
例如可以将扩展程序的静态资源展示在网页上
// 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>
<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>
在内容脚本植入到页面后,设置另一个事件监听器
const greeting = "hola, ";
const button = document.getElementById("mybutton");
button.person_name = "Roberto";
button.addEventListener("click", () =>
alert(greeting + button.person_name + ".")
, false);
两个脚本互不冲突,所以最后点击一次按钮后,会有两次 alert 弹出。
植入脚本
内容脚本 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_start
或document_end
all_frames
属性:一个布尔值,用以控制植入的脚本是否在标签页中所有的 frame 中运行,默认值是false
,即脚本只是植入到主框架中
{
// ...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"css": ["my-styles.css"],
"js": ["content-script.js"]
}
],
}
提示
还有一种设置网页匹配规则的方式是属性 include_globs
和 exclude_globs
,它们的规则更加灵活
动态植入
在后台脚本中就可以使用 chrome.scripting.executeScript()
方法为页面动态植入代码,需要在配置清单 manifest.json
中先声明相应的权限
{
// ...
"permissions": ["scripting"],
}
为了可以在扩展程序运行时以编程的方式动态地在页面植入脚本,需要在配置清单 manifest.json
的选项 host_permissions
中获取访问相应页面的权限;或者在选项 permissions
中声明 activeTab
权限,这样就可以为当前激活页面植入脚本
{
// ...
"permissions": [
"scripting",
"activeTab"
],
"background": {
"service_worker": "background.js"
}
}
// 需要植入的代码
document.body.style.backgroundColor = 'orange';
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 脚本,还支持指定需要执行的函数jsfunction getTitle() { return document.title; } const tabId = getTabId(); chrome.scripting.executeScript( { target: {tabId: tabId}, func: getTitle, }, () => { ... } );
注意
如果植入的是函数,它并不会携带在定义处的上下文,因此函数中类似
this
和指向函数外部作用域的变量会失效而报错。jsconst 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
预先声明,它会作为参数传入到函数中jsconst 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 的返回值在数组中的排序是乱序的jsfunction 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()
专门用于植入样式文件,除了支持以文件数组的方式,还支持字符串的形式,对于简单的样式这个方式更方便。
该方法是植入样式到页面的,所以没有返回值作为回调函数的入参
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 就可以基于拦截的信息,执行相应的操作,例如与扩展程序进行交互
// 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);
<!-- 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 相应的服务器获取数据。
chrome.runtime.sendMessage(
{
contentScriptQuery: 'fetchUrl',
url: 'https://another-site.com/price-query?itemId=' +
encodeURIComponent(request.itemId)},
response => parsePrice(response.text())
);
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 参数,这样就可以在扩展程序中预设(限制)可以访问的域名
// content-script.js
chrome.runtime.sendMessage(
{
contentScriptQuery: 'queryPrice',
itemId: 12345
},
price => {...}
);
// 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。
例如对于以下不安全的处理方法
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();
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();
应该改用更安全的方式
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();
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 页面
在 MV3 版本中,background page 已取消,后台脚本 background scripts 运行在 service workers 中
扩展程序发布
扩展程序主要是通过 Chrome 网上应用店进行分发
开发者通过 Developer Dashboard 平台,将扩展程序项目打包 .zip
文件进行上传;然后该平台会对扩展程序进行安全审核以保护用户,审核成功后扩展程序会转换成后缀为 .crx
的特殊 ZIP 压缩包,再分发给用户
提示
关于扩展程序的更新,Chrome 浏览器会定期检查已安装扩展程序的版本,如果发现新版本就会进行更新,无需用户干预。扩展程序的版本在配置清单 manifest.json
的选项 version
中设置
{
...
"version": "1.5",
...
}
通过提升数值来表示新版本
{
...
"version": "1.5",
...
}
然后将新的扩展程序项目打包为 .zip
文件,在 Developer Dashboard 平台中找到相应扩展程序的旧版本,选择 Edit
上传压缩包,再点击 Publish
进行发布。
提示
其他扩展程序的分发方式可以参考官方文档。