Hello, Extension!:动手写第一个 Manifest V3 扩展
理论铺垫到此为止。这一篇我们写一个完整可运行的 MV3 扩展:点击工具栏图标,把当前页面背景变成淡黄色。功能一分钟就能讲完,但它覆盖了扩展开发的全部骨架——manifest、后台 Service Worker、动态注入、权限声明。理解了这个骨架,后面所有章节都只是在往骨架上填肉。
需求拆解
「点图标改背景」拆开是三个动作:用户点击图标(action 的点击事件)、获得对当前页面的操作权(activeTab 权限)、往页面里执行一段脚本(scripting API)。MV3 官方示例 Page Redder 用的正是这个组合,我们照这个思路自己写一遍。
第一步:目录与 manifest
新建文件夹 hello-ext,放入三个文件。manifest.json 如下:
{
"manifest_version": 3,
"name": "Hello Ext",
"version": "1.0",
"description": "点击图标把页面背景改成淡黄色",
"permissions": ["activeTab", "scripting"],
"action": {
"default_title": "点亮页面"
},
"background": {
"service_worker": "background.js"
}
}
逐行说明:manifest_version 必须是 3,Chrome 只认这个值;name 和 version 是平台强制项,description 与 icons 是商店强制项,本地开发可先省 icons;permissions 里 activeTab 表示「用户点击时临时授权当前标签页」,scripting 允许调用脚本注入 API;action 声明工具栏图标行为,这里没配图标就显示默认字母图标;background 指定后台 Service Worker 文件。
第二步:后台逻辑
background.js 只有一件事——监听图标点击并注入函数:
function makeYellow() {
document.body.style.backgroundColor = "#fffbe6";
}
chrome.action.onClicked.addListener((tab) => {
// tab.url 为空说明是 chrome:// 等受限页面,activeTab 无效
if (!tab.url) return;
chrome.scripting.executeScript({
target: { tabId: tab.id },
func: makeYellow
});
});
两个值得注意的细节。第一,注入的 func 是被序列化后复制进页面的副本,函数体必须自包含,引用外部变量会抛 ReferenceError;要传参就写 args: [值],在函数签名里接。第二,chrome:// 开头的页面属于受限页面,activeTab 拿不到授权,用 tab.url 判断可以避免无意义的报错。
第三步:加载与验证
按上一篇讲的流程加载:打开 chrome://extensions、开启开发者模式、加载已解压的扩展程序、选中 hello-ext 文件夹。然后随便打开一个普通网页,点工具栏上的 Hello Ext 图标,页面背景应立刻变黄。再导航到另一个页面,黄色消失——这正是 activeTab「随用随授、走页即收」的特性。
如果没生效,按这个顺序排查:
- chrome://extensions 页面里扩展卡片有没有红色报错按钮;
- manifest.json 是否有多余逗号或注释(JSON 不允许注释);
- 点图标后打开 Service Worker 的 DevTools 看 Console。
验证「自包含」规则
把 background.js 改成下面这样试试:
const COLOR = "#fffbe6"; // 故意放在函数外
function makeYellow() {
document.body.style.backgroundColor = COLOR; // ReferenceError!
}
chrome.action.onClicked.addListener((tab) => {
if (!tab.url) return;
chrome.scripting.executeScript({
target: { tabId: tab.id },
func: makeYellow,
args: [COLOR] // 正确做法:通过 args 传进去
});
});
直接引用 COLOR 会报错,改成通过 args 传入就正常。亲手撞一次这个坑,比看十遍文档记得牢。
这个骨架能长成什么
Hello Ext 只用了两个权限、一个事件、一个 API,但结构上是标准的 MV3 形态:manifest 声明能力,Service Worker 做事件中枢,需要时向页面注入代码。往后学你会发现:
- 想让脚本自动注入而不是点击触发,用 content_scripts 静态声明;
- 想让后台常驻处理消息,用 runtime.onMessage;
- 想记住用户设置,用 chrome.storage;
- 想给图标加红点计数,用 action.setBadgeText。
每一项都是独立的一章。写扩展时 URL 匹配和文本处理绕不开正则,正则匹配写法可参考本站《正则表达式从入门到精通》:正则实战。本章完整代码建议动手敲一遍而不是复制,第一手的肌肉记忆最值钱。
官方对应入门文档见 https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world ,可作为事实核对。
变体练习:三个改动方向
骨架跑通后,用三个小改动加深理解,每个都只动一两处:
方向一:从 func 注入改成 files 注入。 新建 content.js 文件写入样式代码,把 executeScript 的 func 参数换成 files: ["content.js"]。观察差异:文件方式可以写长逻辑、可以互相 require(手动串联),函数方式适合一段小逻辑加动态传参。
方向二:给图标加角标。 在点击处理里加一行:
chrome.action.setBadgeText({ text: "OK" });
重新加载扩展,点图标后工具栏上出现角标。再想想怎么让它清零——提示:popup 打开时机或下一次点击时清空。
方向三:换成右键菜单触发。 在 manifest 加 "contextMenus" 权限,background.js 里注册菜单项并把逻辑挂到 contextMenus.onClicked。对比两种入口的体验差异:图标点击与右键菜单,用户心智不同,触发 activeTab 授权的效果相同。
做完这三个练习,你对「manifest 声明能力、事件触发、注入执行」的模型就有了第一手体感,第三章的内容会好读得多。
常见首次失败原因
第一次写扩展,失败原因高度集中,逐一对照:
| 现象 | 原因 | 修复 |
|---|---|---|
| 加载时报错「Manifest is not valid JSON」 | 逗号、注释、引号问题 | 用编辑器 JSON 校验逐行查 |
| 点图标无反应 | 目标是 chrome:// 页面,activeTab 无效 | 换普通网页测试 |
| 图标点击报错 tab.id undefined | 窗口没有活动标签页或 restricted 页 | 先判 tab.url 再注入 |
| 页面没变黄 | 扩展未重载或页面未刷新 | 重新加载扩展并刷新页面 |
| Service Worker 显示「已休眠」 | 正常现象 | 有事件会自动唤醒 |
其中最后一条最容易引起误会:扩展管理页里 Service Worker 状态显示「无效(已停用)」不是错误,MV3 的 worker 本来就按需启停。养成习惯:判断扩展是否正常,看「有没有报错按钮」和「功能是否工作」,不看 worker 是否常驻。
从 Hello Ext 看扩展的「信任模型」
这个一分钟的小扩展已经完整演示了扩展的信任模型:manifest 声明 activeTab,安装对话框里不会出现吓人的警告;用户点图标那一刻,浏览器才把当前标签页的权限交给扩展;页面一换,权限收回。用户装它几乎零心理成本,因为它对系统的常驻索取为零。后续做任何功能时,都可以回头问一句:我的功能能不能也设计成「用户主动触发才工作」?能,权限清单就能保持干净,上架审核也会顺畅。
这个最小工程没有用到任何外部依赖,但如果你打算引入构建工具或包管理,工程组织的起手式见 HBuilderX 效率技巧。
本章由 WorkBuddy 原创整理,API 行为依据 Chrome 扩展官方文档(Manifest V3)。
