前言

浏览器插件,简单理解为一段脚本文件。通过 WEB 技术,结合 chrome 提供的相关 api,制作集成为一个可供浏览器直接运行的扩展程序。通常需要以下几个文件

  • manifest.json: 插件的配置文件,定义插件的基本信息和权限
  • background.js: 插件的后台脚本,负责执行后台任务
  • popup.html: 用户点击插件图标时显示的界面
  • style.css: 用于美化插件界面的样式表

开始制作

参考 官网

  1. manifest.json
  • manifest_version: manifest 的版本,2 或者 3,浏览器会根据这个值去指定该版本拥有的功能。必须

  • name: 名称。必须

  • version: 版本。必须

  • icons: 图标集合

    • 16: 扩展程序页面和上下文菜单中的图标
    • 32: Windows 计算机通常需要此大小
    • 48: 显示在 “扩展程序” 页面上
    • 128: 会在安装过程中和 Chrome 应用商店中显示
  • action: 浏览器行为

    • default_popup: 点击扩展程序的操作图标时在弹出式窗口中显示的 HTML 网页
    • default_icon: 声明 Chrome 扩展程序的操作图标,没有将 name 的第一个字符作为图标,也可以设置为 icons 集合形式
  • background: 插件的后台常驻程序,生命周期和浏览器的生命周期一样,独立运行,不与特定的网页关联。即使浏览器窗口关闭或者切换页面,background 脚本依然可以继续运行

    • service_worker: 使用 扩展程序的服务工作器 在后台监控浏览器事件,但无法访问 DOM
    • type: 模块类型,例如 module 代表 es 模块
  • content_scripts: 插入到当前浏览器网页的脚本文件,可以访问 DOM

    • matches: 标记可让浏览器确定要将内容脚本注入哪些网站
  • permissions: 权限设置

    • activeTab: 授予扩展程序在有效标签页上临时执行代码的权限
    • scripting: 授予 scripting API 权限
    • storage: 授予 chrome.storage API 权限
    • alarms: 授予 chrome.alarms API 权限
  • host_permissions: 如需从远程托管位置提取扩展程序提示,需要请求主机权限

  • commands: 快捷键

    • _execute_action: 运行与 action.onClicked() 事件相同的代码
  • minimum_chrome_version: 如果插件需要浏览器版本支持,则可设置需要的最低版本

  • omnibox: 地址栏事件监听器,当输入关键字后跟 Tab 键或空格时,Chrome 会根据存储空间中的关键字显示建议列表。onInputChanged() 事件负责填充建议,它会接受当前用户输入和 suggestResult 对象

    • keyword: 关键字
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
{
"manifest_version": 3,
"name": "A Simple Extension",
"version": "1.0",
"description": "This is a simple Chrome extension",
"icons": {
"16": "icon/icon-16.png",
"32": "icon/icon-32.png",
"48": "icon/icon-48.png",
"128": "icon/icon-128.png"
},
"action": {
"default_popup": "popup.html",
"default_icon": "icon/avatar.png"
},
"background": {
"service_worker": "scripts/background.js"
},
"permissions": ["activeTab"],
"host_permissions": ["https://chrome.dev/f/*"],
"content_scripts": [
{
"js": ["scripts/content.js"],
"matches": [
"https://developer.chrome.com/docs/extensions/*",
"https://developer.chrome.com/docs/webstore/*"
]
}
],
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Ctrl+B",
"mac": "Command+B"
}
}
},
"minimum_chrome_version": "102",
"omnibox": {
"keyword": "api"
}
}

注意事项

  1. 点击扩展图标会优先处理弹出窗口的显示逻辑,即如果定义了 default_popup,会优先显示页面,从而导致 service_worker 中定义的 chrome.action.onClicked 事件不会执行,可以把相关逻辑添加在 popup.js 中引入

通信机制

  1. popup.jsbackground.js 通信:

    • chrome.extension.getBackgroundPage()
    • chrome.extension.getViews({type:'popup'})
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // popup.js
    const backend = chrome.extension.getBackgroundPage();
    backend.test(); // 访问 background 的函数

    // background.js
    const views = chrome.extension.getViews({ type: "popup" });
    let popup = null;
    if (views.length > 0) {
    popup = views[0];
    // 直接访问popup的函数
    popup.test();
    }
  2. content-scriptsbackground 通信:

    • chrome.runtime.sendMessage(message):
    • chrome.runtime.onMessage.addListener(): 监听 content-scripts 发送的消息
    • chrome.tabs.query + chrome.tabs.sendMessage: 主动给 content-scripts 发送消息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // content-scripts
    chrome.runtime.sendMessage("message content", (res) => {
    console.log("from background:", res);
    });

    // background.js
    chrome.runtime.onMessage.addListener(function (message, sender, callback) {
    console.log(mesasge); // meesage content
    callback && callback("yes this from background");
    });

    // background.js
    // {active: true, currentWindow: true} 表示查找当前屏幕下的active状态的tab;
    chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
    chrome.tabs.sendMessage(tabs[0].id, "message content", (res) => {
    console.log("from content:", res);
    });
    });
  3. popup.jscontent-scripts 通信:

    • window.postMessage
    • window.addEventListener

给网页添加脚本

content.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const article = document.querySelector("article");

if (article) {
const text = article.textContent;
const wordMatchRegExp = /[^\s]+/g;
const words = text.matchAll(wordMatchRegExp);
const wordCount = [...words].length;
const readingTime = Math.round(wordCount / 200);
const badge = document.createElement("p");
badge.classList.add("color-secondary-text", "type--caption");
badge.textContent = `⏱️ ${readingTime} min read`;

const heading = article.querySelector("h1");
const date = article.querySelector("time")?.parentNode;

(date ?? heading).insertAdjacentElement("afterend", badge);
}

将脚本注入到当前活动的标签页

background.js

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
const extensions = "https://developer.chrome.com/docs/extensions";
const webstore = "https://developer.chrome.com/docs/webstore";

chrome.action.onClicked.addListener(async (tab) => {
if (tab.url.startsWith(extensions) || tab.url.startsWith(webstore)) {
const prevState = await chrome.action.getBadgeText({ tabId: tab.id });
const nextState = prevState === "ON" ? "OFF" : "ON";

await chrome.action.setBadgeText({
tabId: tab.id,
text: nextState,
});

if (nextState === "ON") {
await chrome.scripting.insertCSS({
files: ["css/style.css"],
target: { tabId: tab.id },
});
} else if (nextState === "OFF") {
await chrome.scripting.removeCSS({
files: ["css/style.css"],
target: { tabId: tab.id },
});
}
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
* {
display: none !important;
}

html,
body,
*:has(article),
article,
article * {
display: revert !important;
}

[role="navigation"] {
display: none !important;
}

article {
margin: auto;
max-width: 700px;
}

使用 Service Worker 处理事件

background.js

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
// 设置全局建议
chrome.runtime.onInstalled.addListener(({ reason }) => {
if (reason === "install") {
chrome.storage.local.set({
apiSuggestions: ["tabs", "storage", "scripting"],
});
}
});

const URL_CHROME_EXTENSIONS_DOC =
"https://developer.chrome.com/docs/extensions/reference/";
const NUMBER_OF_PREVIOUS_SEARCHES = 4;

chrome.omnibox.onInputChanged.addListener(async (input, suggest) => {
await chrome.omnibox.setDefaultSuggestion({
description: "Enter a Chrome API or choose from past searches",
});
const { apiSuggestions } = await chrome.storage.local.get("apiSuggestions");
const suggestions = apiSuggestions.map((api) => {
return { content: api, description: `Open chrome.${api} API` };
});
suggest(suggestions);
});

chrome.omnibox.onInputEntered.addListener((input) => {
chrome.tabs.create({ url: URL_CHROME_EXTENSIONS_DOC + input });
updateHistory(input);
});

// 获取 Omnibox 输入并将其保存到 storage.local
async function updateHistory(input) {
const { apiSuggestions } = await chrome.storage.local.get("apiSuggestions");
apiSuggestions.unshift(input);
apiSuggestions.splice(NUMBER_OF_PREVIOUS_SEARCHES);
return chrome.storage.local.set({ apiSuggestions });
}

// Fetch tip & save in storage
const updateTip = async () => {
const response = await fetch("https://chrome.dev/f/extension_tips");
const tips = await response.json();
const randomIndex = Math.floor(Math.random() * tips.length);
return chrome.storage.local.set({ tip: tips[randomIndex] });
};

const ALARM_NAME = "tip";

async function createAlarm() {
const alarm = await chrome.alarms.get(ALARM_NAME);
if (typeof alarm === "undefined") {
chrome.alarms.create(ALARM_NAME, {
delayInMinutes: 1,
periodInMinutes: 1440,
});
updateTip();
}
}

createAlarm();

// Update tip once a day
chrome.alarms.onAlarm.addListener(updateTip);

Service Worker 生命周期

  1. 安装: 当从 Chrome 应用商店安装或更新服务工件,或者使用 chrome://extensions 页面加载或更新已解压缩的扩展程序时,系统就会进行安装。系统会按以下顺序触发三项事件

    • ServiceWorkerRegistration.install: Web Service Worker 的 install 事件
    • chrome.runtime.onInstalled: 扩展程序的 onInstalled 事件,可以来设置状态或进行一次性初始化
    • ServiceWorkerRegistration.active: 服务工件的 activate 事件,会在安装扩展程序后立即触发
  2. 扩展程序启动:

    • chrome.runtime.onStartup: 当用户个人资料启动时,系统会触发事件,但不会调用任何服务工作器事件
  3. 空闲和关停: 通常,当满足以下任一条件时,Chrome 会终止服务工件,针对失活情况,可使用 持久保存数据 (chrome.storage API / CacheStorage API / IndexedDB API ) 或者 设置 Chrome 最低版本

    • 无操作 30 秒后。接收事件或调用扩展程序 API 会重置此计时器
    • 单个请求(例如事件或 API 调用)的处理时间超过 5 分钟时
    • fetch() 响应到达时间超过 30 秒