本文用一个最小的“页面标记器”示例,演示 Chrome 扩展从目录结构、
manifest.json、弹窗脚本到本地加载和调试的完整链路。重点不是堆功能,而是先建立一套可以定位问题的开发骨架。
一、先明确:Chrome插件不是普通网页
Chrome 插件由浏览器管理,代码运行在不同上下文中:
- 扩展页面:例如弹窗、设置页,可以使用部分
chrome.*API; - 内容脚本:运行在网页上下文附近,用于读取或修改页面内容;
- 扩展 Service Worker:在 Manifest V3 中承担事件处理任务,但不会直接访问网页 DOM;
- 清单文件:
manifest.json声明名称、版本、入口、权限和资源。
如果把它当成“打开一个 HTML 文件”,常见结果就是:页面能显示,但权限、脚本注入和调试方式全部混乱。开发时应该先确定每段代码属于哪个上下文,再决定使用哪一个 API。

二、这次实战要做什么
本例做一个尽量小的扩展:点击扩展图标打开弹窗,再点击按钮给当前网页添加一个固定的“已标记”提示条;再次点击则移除提示条。
这个例子可以练习四个关键点:
- 用 Manifest V3 配置扩展入口;
- 用
activeTab获得用户操作后的临时当前页访问能力; - 用
scripting.executeScript()向当前标签页注入函数; - 用
chrome://extensions加载未打包扩展并查看错误。
示例不请求全站访问权限,也不读取网页表单、Cookie或用户账号信息。这样做的目的,是让权限范围和功能范围保持一致。

三、测试环境与资料边界
- 测试日期:2026-09-18
- 运行环境:Windows + Chrome 浏览器开发者模式
- 扩展规范:Manifest V3
- 示例类型:本地未打包扩展
- 资料来源:Chrome for Developers 官方扩展文档
- 端到端状态:本文文件结构和代码已做静态检查,未在本机真实 Chrome 窗口中完成点击加载验证
Chrome 扩展的权限、接口和商店要求可能随浏览器版本和官方文档变化。发布前应重新核对当前文档,不要把本文的示例版本号当成生产版本策略。
四、创建项目目录
先创建一个空目录,例如:
chrome-page-marker/
├─ manifest.json
├─ popup.html
└─ popup.js
manifest.json 必须位于扩展目录的根目录。目录名可以自定义,但不要把扩展放在包含密钥、客户资料或其他不应被工具读取的目录中。

五、编写 Manifest V3 清单
新建 manifest.json:
{
"manifest_version": 3,
"name": "页面标记器",
"version": "1.0.0",
"description": "点击按钮后给当前页面添加或移除标记提示条。",
"permissions": [
"activeTab",
"scripting"
],
"action": {
"default_popup": "popup.html"
}
}
这里有三个容易写错的点:
manifest_version使用3;action.default_popup指向真实存在的 HTML 文件;activeTab和scripting是本示例所需权限,不要为了“以后可能用到”而额外申请tabs、cookies或全站主机权限。
activeTab 的思路是:用户主动点击扩展后,扩展获得当前标签页的临时访问能力。它不等于永久访问所有网站。权限越少,用户看到的警告和潜在影响通常越容易解释。
六、编写弹窗页面
新建 popup.html:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>页面标记器</title>
<style>
body {
width: 220px;
margin: 0;
padding: 16px;
font-family: system-ui, sans-serif;
}
button {
width: 100%;
padding: 8px 12px;
cursor: pointer;
}
#status {
min-height: 20px;
margin-top: 10px;
color: #475569;
font-size: 12px;
}
</style>
</head>
<body>
<button id="mark">切换页面标记</button>
<div id="status" role="status"></div>
<script src="popup.js"></script>
</body>
</html>
弹窗只负责用户操作和状态显示,不把复杂的页面逻辑直接写在 HTML 的内联事件里。这样更容易定位脚本错误,也方便以后拆分功能。

七、编写注入脚本
新建 popup.js:
const button = document.querySelector("#mark");
const status = document.querySelector("#status");
button.addEventListener("click", async () => {
status.textContent = "正在处理当前页面……";
try {
const [tab] = await chrome.tabs.query({
active: true,
lastFocusedWindow: true,
});
if (!tab || typeof tab.id !== "number") {
throw new Error("没有找到当前标签页");
}
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
const id = "local-page-marker";
const oldMarker = document.getElementById(id);
if (oldMarker) {
oldMarker.remove();
return "已移除页面标记";
}
const marker = document.createElement("div");
marker.id = id;
marker.textContent = "当前页面已标记";
Object.assign(marker.style, {
position: "fixed",
top: "12px",
right: "12px",
zIndex: "2147483647",
padding: "8px 12px",
color: "#ffffff",
background: "#2563eb",
borderRadius: "6px",
font: "14px/1.4 system-ui, sans-serif",
});
document.documentElement.appendChild(marker);
return "已添加页面标记";
},
});
status.textContent = "操作完成";
} catch (error) {
console.error(error);
status.textContent = "当前页面无法注入脚本,请查看扩展错误信息";
}
});
这里的 func 会被注入当前标签页执行。它只创建或移除一个带固定 ID 的 DOM 元素,不读取页面正文,也不把页面内容发送到外部服务。
需要注意:chrome:// 页面、扩展商店页面以及部分受限制的页面不一定允许普通扩展脚本注入。遇到“代码没反应”时,先确认是不是页面类型限制,不要马上判断为权限配置完全错误。

八、在 Chrome 中加载未打包扩展
- 打开新标签页,输入
chrome://extensions; - 打开右上角的“开发者模式”;
- 点击“加载已解压的扩展程序”;
- 选择包含
manifest.json的项目根目录; - 加载成功后,打开一个普通网页;
- 点击扩展图标,再点击“切换页面标记”。
如果页面出现蓝色提示条,说明这个最小流程已经走通。修改 manifest.json 或 popup.js 后,回到扩展管理页点击重新加载,再重新打开弹窗测试。
九、出现错误时先看哪里
1. 扩展无法加载
优先检查:
manifest.json是否是合法 JSON;- 是否多了尾逗号;
default_popup的文件名是否拼写一致;- 选择的目录是否真的包含
manifest.json。
不要只看浏览器弹出的概括性提示。扩展管理页通常会提供具体错误信息,先复制错误文本,再定位对应文件和行号。
2. 点击按钮没有效果
依次排查:
- 弹窗是否正常打开;
popup.js是否加载;- 当前页面是否为受限制页面;
scripting是否写进permissions;tab.id是否存在;- 执行注入时是否抛出异常。
可以右键扩展弹窗,选择“检查”,在弹出的 DevTools Console 中查看 console.error 和异常堆栈。
3. 修改代码后看不到变化
不同扩展组件的重新加载要求不同。清单文件、Service Worker 和内容脚本的变化,通常需要在扩展管理页重新加载扩展;弹窗 HTML 的小修改可能在重新打开弹窗后生效,但为了避免缓存或上下文混淆,开发阶段建议形成固定动作:保存文件、重新加载扩展、关闭并重新打开弹窗、再测试目标页面。
十、什么时候需要 Service Worker
本示例没有使用后台事件,所以不必为了“看起来完整”强行加入 Service Worker。只有当你需要监听安装事件、右键菜单、通知、标签页事件或其他后台事件时,才在 manifest.json 中声明:
{
"background": {
"service_worker": "service-worker.js"
}
}
Manifest V3 中,扩展 Service Worker 会在需要时加载,空闲时可能被停止;它不能直接访问 DOM。如果需要操作网页,应使用内容脚本、脚本注入或其他合适的扩展页面通信机制。
另一个容易踩坑的点是:不要把远程 JavaScript 地址当作 Service Worker 或扩展代码来源。扩展代码应随扩展包一起提供,具体限制以当前官方文档为准。
十一、权限设计比功能堆叠更重要
Chrome 扩展开发不是权限越多越强。建议按下面的顺序设计:
- 核心功能必须使用的权限,才放进
permissions; - 可选功能需要的权限,考虑放进
optional_permissions或optional_host_permissions; - 能用
activeTab解决的临时页面操作,不要直接申请全站主机权限; - 不使用的
tabs、cookies、webRequest等权限不要提前申请; - 新增权限前,重新解释用户为什么需要它,以及会触发什么警告。
如果扩展需要访问 file:// 页面或无痕窗口,用户还需要在扩展详情页单独允许相应访问。这个开关不是写进代码后就自动获得的。
十二、上线前的最小检查清单
manifest.json可以被 Chrome 正常识别;- 扩展名称、描述和版本号与实际功能一致;
- 权限与功能一一对应,没有“预留权限”;
- 普通网页可以完成一次添加和移除标记;
- 受限制页面无法执行时,界面有可理解的提示;
- 没有把密钥、Cookie、用户数据写入扩展包;
- 修改清单、脚本和后台代码后知道如何重新加载;
- 远程代码、第三方库和图标素材的来源已经确认;
- 发布前重新查看 Chrome 官方文档和目标发布渠道要求。
十三、结论
一个能长期维护的 Chrome 插件,起点不是复杂功能,而是清晰的运行上下文、最小权限和可重复的调试流程。本文示例只做页面标记,却覆盖了 Manifest V3、本地加载、脚本注入、受限页面和权限排错这些最容易反复出问题的基础环节。
如果后续要扩展功能,建议每次只增加一个变量:先加一个按钮,再加一个 API,再加一个权限,并在每一步记录“为什么需要、在哪个上下文运行、如何验证、失败时看什么日志”。这比一次性搭建一个权限很多但无法解释的插件更容易维护。
文章底部技术标签
#Chrome插件 #ManifestV3 #JavaScript #浏览器扩展 #前端开发
参考资料
- Chrome for Developers:Hello World extension;核验日期:2026-09-18。
- Chrome for Developers:Declare permissions;核验日期:2026-09-18。
- Chrome for Developers:About extension service workers;核验日期:2026-09-18。
- Chrome for Developers:Extension service worker basics;核验日期:2026-09-18。

338

被折叠的 条评论
为什么被折叠?



