Chrome 插件开发概论

开发插件需要的技术:HTML、CSS、JavaScript

插件构成

  • manifest.json:必要。插件的基本信息,包括名称、版本号、图标、脚本入口等。
  • background script:用于调用 Chrome 插件 API,是插件跨域请求、网页截屏、弹出 Chrome 通知消息的承担者。可以视为一个隐藏的浏览器页面,在插件管理中可打开background.html进行调试。
  • content script:插件注入到页面的脚本,不会显式地出现在页面 DOM 结构中。可以操作 DOM,但与页面本身的脚本是隔离的,无法访问页面本身的变量或函数,相当于独立的沙盒运行。可以调用有限的 Chrome 插件 API,但无法进行跨域请求。
  • 功能页面:包括插件图标的弹出页面popup和插件配置页面options

通信架构

1
2
3
4
5
6
7
8
9
10
11
graph TD
background.js -.-> popup.js
popup.js -.-> background.js
popup.html-.->popup.js
popup.js-.->popup.html
窗口-.->contentscript.js
contentscript.js-.->窗口
background.js-->contentscript.js
contentscript.js-->background.js
窗口-->background.js
background.js-->窗口

浏览器中的作用范围

功能页面是每个 window 一份,但是每个 tab 都会注入 content script

脚本注入

声明式注入

1
2
3
4
5
6
7
8
9
{
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"run_at": "document_idle",
"js": ["content.js"]
}
]
}
  1. matches 表示页面 url 匹配时才加载
  2. run_at 表示在什么时机加载,一般是 document_idle,避免 content_scripts 影响页面加载性能。

需要注意的是,如果用户已经打开了 N 个页面,然后再安装插件,这 N 个页面除非重新刷新,否则是不会加载 manifest 声明的 content_scripts。

安装插件之后新打开的页面是可以加载 content_scripts 的。

所以需要在用户点击插件图标时,探测页面中的 content_scripts 是否存在(发送消息是否有响应/出错),再提示用户刷新页面。

程序注入

使用程序动态注入脚本

1
2
3
chrome.tabs.executeScript({
file: "content.js",
});

如用户点击插件图标时执行注入脚本,则无需刷新页面

1
2
3
4
5
6
// 监听插件图标点击事件
chrome.browserAction.onClicked.addListener(() => {
chrome.tabs.executeScript({
file: 'content.js',
});
});

content.js 的执行会影响其所在的沙盒。

1
2
3
4
5
6
7
8
// content.js

if (!window.contentLoaded) {
// do something
window.contentLoaded = true;
}

console.log(window.contentLoaded);

使用沙盒内的全局变量则可以避免 content.js 重复执行带来的问题。

声明式只会注入一次,缺点是可能需要刷新页面。程序式不需要刷新页面,缺点是可能会注入多次。

permissions

该字段是一个字符串数组,用来声明插件需要的权限,这样才能调用某些 chrome API

tabs

activeTab

contextMenus:网页右键菜单,browser_action 右键菜单

cookies:操作 cookie,和用户登录态相关的功能可能会用到该权限

storage:插件存储,不是 localStorage

web_accessible_resources:网页能访问的插件内部资源,比如插件提供 SDK 给页面使用,如 ethereum 的 metamask 钱包插件。或者是修改 DOM 结构用到了插件的样式、图片、字体等资源。

background script

它是在一个隐藏的 tab 中执行,所在的页面域名为空

比如 background 需要和 a.com 通信。首先应该把 *://*.a.com/* 加入到 manifest 的 permissions 数组中。

当发送网络请求时,浏览器会自动带上 a.com 的 cookie,服务器的 set-cookie 也会对浏览器生效。这是符合预期的。

background 设置 document.cookie 时,不能指定域名,否则会设置失败。

1
2
3
4
5
// 会失败,因为指定的域名和 background 所在的域名不符
document.cookie = `session=xxxxxxx; domain=a.com; max-age=9999999999; path=/`;

// 正确的做法,不要指定域名
document.cookie = `session=xxxxxxx; max-age=9999999999`;

background 使用 tabs 接口操作浏览器的 tab 窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 打开新 tab
async function open(url: string): Promise<number> {
return new Promise((resolve) => {
chrome.tabs.create(
{
url,
},
(tab) => resolve(tab.id!)
);
});
}

// 获取活跃的 tab,通常是用户正在浏览的页面
async function getActiveTab(): Promise<chrome.tabs.Tab | null> {
return new Promise((resolve) => {
chrome.tabs.query(
{
active: true,
currentWindow: true,
},
(tabs) => {
if (tabs.length > 0) {
resolve(tabs[0]);
} else {
resolve(null);
}
}
);
});
}

// 将指定的 tab 变成活跃的
async function activate(
tabId?: number,
url?: string
): Promise<number | undefined> {
if (typeof tabId === "undefined") {
return tabId;
}

// firefox 不支持 selected 参数
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/update#parameters
const options: chrome.tabs.UpdateProperties = IS_FIREFOX
? { active: true }
: { selected: true };
if (url) {
options.url = url;
}

return new Promise((resolve) => {
chrome.tabs.update(tabId, options, () => resolve(tabId));
});
}

// 打开新窗口,或者是激活窗口
async function openOrActivate(url: string): Promise<number> {
const pattern = getUrlPattern(url);
return new Promise<number>((resolve) => {
chrome.tabs.query(
{
url: pattern,
},
(tabs) => {
if (tabs.length > 0 && tabs[0].id) {
return Tabs.activate(tabs[0].id);
} else {
this.open(url).then((id) => resolve(id));
}
}
);
});
}

content script

它只能使用有限的 chrome API

由于 content 可以访问 DOM,可以用它来选择、修改、删除、增加网页元素。

但是 content 是运行在隔离的空间(类似沙盒),所以如果需要和页面的其他脚本通信,需要采用 window.postMessage 的方式。

资源注入

content 可以向页面中注入 <script>,由此给页面提供 SDK 等功能,注入的脚本和页面自己的脚本一样,都无法和 content 直接通信。

注意:注入的资源要先在 menifest 的 web_accessible_resources 字段中声明。

1
2
3
4
// content 内容
const script = document.createElement("script");
script.src = chrome.runtime.getURL("sdk.js");
document.body.appendChild(script);
1
2
3
4
5
// sdk.js
window.jsbridge = {
version: "1.0.1",
// ...
};

注入的 sdk.js 脚本是可以被页面内其他脚本访问到的

常用的通信 API 是 chrome.runtime.sendMessage

Reference

chrome 插件开发指南 - 掘金 (juejin.cn)