前言
浏览器插件,简单理解为一段脚本文件。通过 WEB 技术,结合 chrome 提供的相关 api,制作集成为一个可供浏览器直接运行的扩展程序。通常需要以下几个文件
manifest.json
: 插件的配置文件,定义插件的基本信息和权限
background.js
: 插件的后台脚本,负责执行后台任务
popup.html
: 用户点击插件图标时显示的界面
style.css
: 用于美化插件界面的样式表
开始制作
参考 官网
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
对象
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" } }
|
注意事项
- 点击扩展图标会优先处理弹出窗口的显示逻辑,即如果定义了
default_popup
,会优先显示页面,从而导致 service_worker
中定义的 chrome.action.onClicked
事件不会执行,可以把相关逻辑添加在 popup.js
中引入
通信机制
popup.js
和 background.js
通信:
chrome.extension.getBackgroundPage()
chrome.extension.getViews({type:'popup'})
1 2 3 4 5 6 7 8 9 10 11 12
| const backend = chrome.extension.getBackgroundPage(); backend.test();
const views = chrome.extension.getViews({ type: "popup" }); let popup = null; if (views.length > 0) { popup = views[0]; popup.test(); }
|
content-scripts
和 background
通信:
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
| chrome.runtime.sendMessage("message content", (res) => { console.log("from background:", res); });
chrome.runtime.onMessage.addListener(function (message, sender, callback) { console.log(mesasge); callback && callback("yes this from background"); });
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { chrome.tabs.sendMessage(tabs[0].id, "message content", (res) => { console.log("from content:", res); }); });
|
popup.js
和 content-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); });
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 }); }
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();
chrome.alarms.onAlarm.addListener(updateTip);
|
Service Worker 生命周期
安装: 当从 Chrome 应用商店安装或更新服务工件,或者使用 chrome://extensions 页面加载或更新已解压缩的扩展程序时,系统就会进行安装。系统会按以下顺序触发三项事件
ServiceWorkerRegistration.install
: Web Service Worker 的 install 事件
chrome.runtime.onInstalled
: 扩展程序的 onInstalled 事件,可以来设置状态或进行一次性初始化
ServiceWorkerRegistration.active
: 服务工件的 activate 事件,会在安装扩展程序后立即触发
扩展程序启动:
chrome.runtime.onStartup
: 当用户个人资料启动时,系统会触发事件,但不会调用任何服务工作器事件
空闲和关停: 通常,当满足以下任一条件时,Chrome 会终止服务工件,针对失活情况,可使用 持久保存数据 (chrome.storage API
/ CacheStorage API
/ IndexedDB API
) 或者 设置 Chrome 最低版本
- 无操作 30 秒后。接收事件或调用扩展程序 API 会重置此计时器
- 单个请求(例如事件或 API 调用)的处理时间超过 5 分钟时
- fetch() 响应到达时间超过 30 秒