简介:这是一份面向Chrome扩展开发初学者与前端工程师的实战示例资源,围绕浏览器插件自动填表单这一典型场景展开,重点演示如何为Worktile任务描述表单实现自动化填写,帮助读者理解插件从架构到落地的完整思路。压缩包共22个文件,包含9个js脚本、7个html页面、3个json配置、2个png图标及1个css样式文件,整体约185KB,涵盖manifest配置、内容脚本、背景脚本、选项页与弹出窗口等核心模块,结构紧凑便于对照学习。资源涉及DOM操作填充表单、localStorage数据存取、dispatchEvent模拟用户输入、MutationObserver监听页面变化以及与Worktile的API调用和用户授权等知识点,同时兼顾开发者工具调试、权限最小化与隐私保护等实践要点。目前已有1886人学习下载,适合希望快速上手Chrome插件开发、掌握自动填表与页面交互技巧的读者参考借鉴。
1. 从零写一个 Chrome 浏览器插件:为什么“能跑起来”比“看懂文档”更重要
很多人第一次接触 Chrome 浏览器插件开发,卡住的地方不是 JavaScript 语法,而是不知道一个能加载进chrome://extensions/的最小插件到底长什么样。网上搜“chrome浏览器插件例子”,出来的要么是官方文档的英文长文,要么是几年前的完整项目源码,中间缺了一层:一个你今天下午就能写完、明天就能装进浏览器验证的骨架。这篇笔记就补这一层。我会按“最小可运行插件 → 核心能力逐个加 → 常见翻车点 → 进阶调试技巧”的顺序,把 Chrome 插件开发里真正会用到的东西讲清楚。适合两类人:一是完全没写过插件、但会基本前端的前端或测试工程师;二是写过油猴脚本、想升级成正式扩展的自动化从业者。读完你应该能独立写出一个带 popup、content script、background 和存储能力的插件,并且知道每个参数改哪里、报错看哪里。
2. 最小可运行插件:manifest、popup 与加载流程
2.1 先搞清楚 Manifest V3 的四个必填字段
Chrome 插件从 Manifest V2 迁移到 V3 之后,很多老例子的写法已经不能直接用了。V3 里manifest.json最核心的必填字段是manifest_version、name、version、action(或background)。manifest_version现在固定写3,写2虽然部分旧版还能加载,但新版本 Chrome 会直接提示不支持。action取代了 V2 的browser_action,它决定工具栏图标点击后弹出什么。
一个最小骨架的目录结构是这样的:
my-extension/ ├── manifest.json ├── popup.html ├── popup.js └── icons/ └── icon128.pngicons目录不是必须的,但如果没有图标,加载后工具栏会显示一个灰色占位块,调试时容易误以为插件没生效。图标建议准备 16、48、128 三个尺寸,至少给一个 128 的。
2.2 写一个能弹出“Hello”的 popup
先写manifest.json:
{ "manifest_version": 3, "name": "My First Extension", "version": "1.0.0", "description": "一个用于验证加载流程的最小插件", "action": { "default_popup": "popup.html", "default_icon": { "128": "icons/icon128.png" } }, "icons": { "128": "icons/icon128.png" } }这里每个字段的作用:action.default_popup指向点击图标后显示的 HTML 文件,路径是相对于插件根目录的;icons是插件在扩展管理页和商店里显示的图标。注意default_popup不能指向一个不存在的文件,否则点击图标不会有任何反应,控制台也不会报错,这是新手最容易懵的地方。
接着写popup.html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <style> body { width: 240px; padding: 12px; font-family: system-ui; } button { width: 100%; padding: 8px; cursor: pointer; } </style> </head> <body> <p id="msg">等待点击</p> <button id="btn">点我</button> <script src="popup.js"></script> </body> </html>popup.js:
// popup 页面的脚本,运行在扩展自己的页面上下文里 document.getElementById('btn').addEventListener('click', () => { const msg = document.getElementById('msg'); msg.textContent = '插件已生效 ' + new Date().toLocaleTimeString(); });这段代码没有任何 Chrome API 调用,纯粹验证 popup 能否正常渲染和响应事件。逻辑说明:popup 是一个独立的 HTML 页面,它的生命周期只在弹窗打开期间存在,关闭弹窗后页面销毁,所有变量丢失。参数说明:body的width建议固定,否则弹窗宽度会随内容跳动,体验很差。
2.3 加载与热更新的正确姿势
打开chrome://extensions/,右上角开启“开发者模式”,点击“加载已解压的扩展程序”,选择my-extension目录。加载成功后工具栏会出现图标,点击就能看到 popup。
改代码之后不需要重新加载整个插件的情况:改popup.html或popup.js,直接关掉弹窗再点开就生效;改manifest.json或background.js,必须点扩展卡片上的刷新按钮,否则改动不生效。这个差异是血泪经验,很多人改了 manifest 发现没反应,以为代码写错了,其实是没刷新。
提示:如果加载时提示“清单文件缺失或不可读取”,先检查
manifest.json是不是有 JSON 语法错误,比如多了一个逗号。Chrome 对 JSON 格式要求严格,不允许注释和尾随逗号。
3. 让插件真正干活:content script、background 与存储
3.1 content script 注入页面的三种方式与选择
popup 只能做界面,真正要操作网页 DOM,得靠 content script。content script 是注入到目标页面的脚本,它能读写页面 DOM,但和页面本身的 JS 变量是隔离的。注入方式有三种:在 manifest 里静态声明、用chrome.scripting.executeScript动态注入、通过chrome.tabs配合权限注入。
静态声明适合“所有页面都要跑”的场景:
{ "content_scripts": [ { "matches": ["https://*.example.com/*"], "js": ["content.js"], "run_at": "document_idle" } ] }matches是匹配规则,*不能匹配所有协议,写https://*/*才是所有 HTTPS 页面。run_at有三个值:document_start在 DOM 构建前执行,document_end在 DOM 完成后、资源加载前执行,document_idle在两者之间由浏览器决定,通常最安全。参数说明:如果脚本依赖页面元素存在,用document_idle;如果要拦截请求或改早期样式,用document_start。
动态注入适合“用户点击后才注入”的场景,需要在 manifest 里申请scripting和activeTab权限:
// 在 popup.js 或 background.js 中调用 chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { chrome.scripting.executeScript({ target: { tabId: tabs[0].id }, files: ['content.js'] }); });逻辑说明:tabs.query拿到当前活动标签页,executeScript把文件注入进去。参数说明:activeTab权限只在用户主动触发(点击图标、快捷键)时授予,不需要在安装时申请宽泛的 host 权限,适合做“按需操作”的工具。
3.2 background service worker 与消息通信
Manifest V3 把 V2 的 background page 换成了 service worker。区别是 service worker 没有 DOM,不能直接操作页面,而且会被浏览器随时休眠。它适合做事件中转、网络请求代理、定时任务。
注册方式:
{ "background": { "service_worker": "background.js" } }background.js里监听消息:
// 接收来自 content script 或 popup 的消息 chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.type === 'GET_DATA') { // 模拟异步处理 setTimeout(() => { sendResponse({ data: '来自 background 的响应' }); }, 100); return true; // 关键:异步响应必须返回 true } });这里return true是必须的,否则sendResponse在异步回调里调用时,消息通道已经关闭,发送方收不到响应。这个坑非常隐蔽,现象是 popup 里发消息后一直 pending,控制台也不报错。
content script 发送消息:
chrome.runtime.sendMessage({ type: 'GET_DATA' }, (response) => { console.log(response.data); });参数说明:sendMessage的第一个参数是任意可序列化对象,第二个是回调。如果 background 没有监听或没有返回 true,回调里的response会是undefined,同时chrome.runtime.lastError会有值,记得检查。
3.3 用 chrome.storage 做持久化,别再用 localStorage
popup 和 content script 里都能用localStorage,但它有两个问题:一是 content script 的 localStorage 属于目标页面,换页面就没了;二是 popup 关闭后上下文销毁,localStorage 虽然还在,但跨设备不同步。Chrome 插件应该用chrome.storage.local或chrome.storage.sync。
// 写入 chrome.storage.local.set({ count: 10 }, () => { console.log('保存完成'); }); // 读取 chrome.storage.local.get(['count'], (result) => { console.log(result.count); // 10 });local容量默认 10MB(申请unlimitedStorage后可更大),sync只有 100KB 左右,但能跨设备同步。参数说明:存配置用sync,存缓存数据用local。注意get的参数是数组或对象,传字符串也能用但不推荐,容易和对象 key 混淆。
4. 避坑与排查:插件加载失败、消息不通、权限报错的真实原因
4.1 现象:加载插件时提示“无法加载清单文件”
原因:manifest.json存在 JSON 语法错误,最常见的是尾随逗号、中文引号、BOM 头。Chrome 的 JSON 解析器不接受注释,也不接受单引号。
解决:把manifest.json内容贴到任意 JSON 校验工具里过一遍。如果文件是用 Windows 记事本保存的,检查编码是不是 UTF-8 无 BOM。用 VS Code 的话,右下角编码选“UTF-8”而不是“UTF-8 with BOM”。
4.2 现象:content script 没执行,页面毫无反应
原因:matches规则没匹配上当前页面。比如写的是https://*.example.com/*,但当前页面是http://example.com,协议不匹配就不会注入。另外,插件安装后已经打开的标签页不会自动注入,必须刷新页面。
解决:在chrome://extensions/里点开插件的“详细信息”,查看“有权访问的网站”列表,确认目标域名在列。改完 matches 后刷新插件,再刷新目标页面。调试时可以在 content script 第一行写console.log('injected'),在页面控制台看有没有输出。
4.3 现象:popup 发消息给 background,回调一直不执行
原因:background 的onMessage监听器里做了异步操作,但没有return true。Chrome 默认在监听器同步返回后关闭消息通道,异步的sendResponse就丢了。
解决:只要sendResponse是在异步回调里调用的,监听器必须返回true。如果用了async/await,注意addListener的回调不能是 async 函数,否则返回值是 Promise 而不是 true,同样会丢消息。正确写法是在回调里手动返回 true,或者用 Promise 包装后同步返回。
4.4 现象:调用 chrome.tabs 相关 API 报 “Cannot read properties of undefined”
原因:没有在 manifest 里声明对应权限。chrome.tabs的大部分方法需要tabs权限,chrome.scripting需要scripting权限,操作特定域名需要host_permissions。
解决:在manifest.json里补权限声明:
{ "permissions": ["tabs", "scripting", "storage"], "host_permissions": ["https://*.example.com/*"] }注意 V3 里host_permissions是独立字段,不再放在permissions里。改完必须刷新插件。如果只是读取当前标签页的 URL 和标题,用activeTab权限更轻量,不需要tabs。
4.5 现象:service worker 里 setTimeout 不执行或状态丢失
原因:Manifest V3 的 service worker 会在空闲约 30 秒后被浏览器终止,所有内存变量清空,定时器失效。这是设计行为,不是 bug。
解决:不要依赖 service worker 里的全局变量保存状态,改用chrome.storage。需要定时任务用chrome.alarmsAPI,它能在 service worker 休眠后唤醒:
{ "permissions": ["alarms"] }chrome.alarms.create('myAlarm', { periodInMinutes: 1 }); chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === 'myAlarm') { console.log('定时触发'); } });参数说明:periodInMinutes最小值为 1(开发模式下可更短,正式环境有下限)。chrome.alarms是 service worker 场景下唯一可靠的定时方案。
5. 进阶技巧:用 DevTools 和 source map 把调试效率提上来
5.1 分上下文调试:别在错误的控制台里找报错
Chrome 插件有四个独立的执行上下文,报错出现在哪个控制台取决于代码运行在哪里。popup 的报错在弹窗内右键“检查”打开的控制台;content script 的报错在目标页面的控制台,但需要在控制台左上角的上下文下拉框里切换到插件名;background service worker 的报错在chrome://extensions/里点击“Service Worker”链接打开的控制台;options 页面和普通网页一样。
我一般会同时开三个窗口:目标页面控制台看 content script,扩展管理页看 service worker,弹窗内看 popup。这样任何一处报错都能立刻定位,不用猜。
5.2 用 source map 调试压缩前的代码
如果插件用了打包工具(webpack、vite、rollup),产出的代码是压缩过的,断点打上去变量名全是a、b。在打包配置里开启devtool: 'source-map'或build.sourcemap: true,Chrome DevTools 会自动加载.map文件,断点就能落在源码上。
验证方法:打开 DevTools 的 Sources 面板,看文件树里有没有出现webpack://或源码目录。如果没有,检查.map文件是否和.js文件在同一目录,以及 manifest 里引用的路径是否正确。注意发布到商店时不要带 source map,会暴露源码结构。
5.3 一个我常用的调试习惯:先验证权限,再验证逻辑
插件开发里最常见的两类问题,一是权限没给够导致 API 直接 undefined,二是权限给了但逻辑写错。我的习惯是在 background 或 content script 开头先打印一次权限自检:
// 自检:确认关键 API 是否存在 console.log('runtime:', typeof chrome.runtime); console.log('storage:', typeof chrome.storage); console.log('scripting:', typeof chrome.scripting); console.log('tabs:', typeof chrome.tabs);如果某个 API 打印出undefined,不用往下查逻辑,直接去 manifest 补权限。这个习惯帮我省掉了大量“以为是代码问题、其实是权限问题”的时间。插件开发没有后悔药,但把自检做在前面,能少走很多弯路。希望帮到你。
本文还有配套的精品资源,点击获取