ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Chrome插件开发实战:从Manifest V3最小例子到避坑指南

Chrome插件开发实战:从Manifest V3最小例子到避坑指南 简介Chrome浏览器插件例子是一份面向前端与Web开发者的插件开发示例围绕自动填写Worktile任务描述表单的典型场景演示如何用HTML、CSS、JavaScript构建可用的浏览器扩展。压缩包共22个文件约185KB以9个JS脚本、7个HTML页面、3个JSON配置为主辅以CSS样式和PNG图标这些文件分别对应后台脚本、内容脚本、弹窗、选项页、本地化配置等模块能帮助理解插件各组成部分的职责划分。资源已有1885人学习下载适合希望快速上手Chrome扩展开发、弄清自动填表与页面交互机制的初学者。通过完整代码可掌握manifest.json的权限配置、DOM元素查找与value赋值、EventTarget.dispatchEvent模拟输入、localStorage数据存储、MutationObserver监听页面变化以及开发者模式调试和Web Store发布流程开发者还能参考其分层写法快速迁移到其他网站的表单自动化场景或在现有插件基础上扩展更多功能。1. chrome 浏览器插件例子不是黑匣子一个最小扩展能解决什么问题很多人搜「chrome 浏览器插件例子」搜到的多半是网盘里一份源码包下载下来要么版本对不上要么代码还绑死在 Manifest V2 上改了字段照样无法加载。真实情况是一个能跑的扩展远没有想象中复杂一个 manifest.json、一个后台脚本、一个内容脚本加起来三十行左右装进浏览器就能看到效果。它真正解决的是网页脚本做不了的三件事在页面加载早期注入逻辑、跨域读取当前标签页数据、调用浏览器下载或存储能力。适合所有需要跟网页打交道的人前端想省掉反复手测爬虫工程师想少写无头浏览器运营和测试想把手上的重复操作变成一个工具栏按钮。下文会从一个最小例子走到可复用的工具型插件途中把权限、消息、版本兼容这些坑一个个填掉。2. 最小例子的骨架Manifest V3 的三个关键文件与权限边界很多教程挂在「谷歌浏览器插件开发实战指南」这类标题下教你在 manifest.json 里写manifest_version: 2配一个background.html再用chrome.tabs.executeScript注入代码。这套写法在当前 Chrome 里基本走不通应用商店不再接受新的 Manifest V2 扩展本地加载也会被标记为不受支持。所以新项目的起点直接定义成 Manifest V3下面的最小例子也只按 V3 来写省得你拿旧代码二次翻译。2.1 为什么新项目默认选 Manifest V3Manifest V3 的核心变化有三个都直接影响写代码方式。第一后台脚本从常驻的background.html页面变成 service worker平时不占内存只有注册的事件被触发时才醒来事件处理完又休眠。第二扩展不能再加载远程 JavaScript线上 CDN 脚本、eval、new Function都在禁用范围所有逻辑必须打在自己包里。第三权限模型收得更紧host_permissions要和permissions分开声明浏览器对「这个扩展想读哪些站点」给用户展示得更清楚。这三件事听起来像限制实际是在帮插件做「干净」。一个只在用户点击工具栏按钮时才活跃的插件比一个时刻监听所有网站的插件更容易通过企业审核也更好排查问题。只有一种情况我建议回头去碰 MV2在维护企业内网专用的老插件浏览器又锁在 Chrome 40-44 这类上古版本上API 带不动 V3。普通用户的常规环境没必要守着一套被淘汰的写法。2.2 manifest.json 最小字段与权限表直接可抄的最小manifest.json{ manifest_version: 3, name: page-title-catcher, version: 0.1.0, description: 最小 chrome 浏览器插件例子读取当前页 title, permissions: [activeTab], action: { default_title: 读取当前页面标题 }, background: { service_worker: background.js }, content_scripts: [ { matches: [*://*/*], js: [content.js], run_at: document_idle } ] }manifest_version: 3是协议版本不要写成 2。name和version是商店展示与版本控制的核心字段version 建议三位数字如0.1.0上线后必须严格递增。action定义浏览器右上角的工具栏按钮没写default_popup时点击事件会落到后台的chrome.action.onClicked里。background.service_worker声明后台脚本V3 下只能是一个 service worker 文件。content_scripts数组里的matches: [*://*/*]表示对所有 http/https 页面注入js按顺序加载run_at: document_idle等页面 DOM 就绪后再执行。权限字段是新手最容易抄错的地方整理成表格区分字段管什么例子permissions扩展 API 能力activeTab、scripting、storage、downloadshost_permissions能访问哪些站点的数据https://example.com/*、all_urlscontent_scripts.matches脚本自动注入哪些站点*://*/*permissions不包含域名host_permissions不包含 API。典型场景你想读取页面 DOM 并下载一个文件至少要activeTab scripting downloads三个permissions再加一个host_permissions指向要操作的站点。activeTab是个折中方案——用户点击扩展的那一瞬间浏览器临时授予当前标签页访问权适合「用完即走」的工具型插件没必要为整个网站申请长期权限。2.3 service worker后台脚本的事件驱动与休眠chrome.runtime.onInstalled.addListener(() { console.log(extension installed); }); chrome.action.onClicked.addListener(async (tab) { try { const response await chrome.tabs.sendMessage(tab.id, { type: GET_TITLE }); console.log(page title:, response.title); } catch (error) { console.warn(content script not ready:, error.message); } });chrome.action.onClicked对应 manifest 里没有default_popup的按钮用户点图标时触发tab参数直接给出当前标签页信息。这里用chrome.tabs.sendMessage向页面里的 content script 发消息跟 2.4 的监听端成对出现。await等对方sendResponse返回如果页面还没注入脚本Chrome 会抛「Receiving end does not exist」所以务必 try/catch别让错误直接死在 console 里。service worker 最大的坑是不常驻。脚本里的全局变量在休眠后会被销毁下次被事件唤醒时重新初始化。不要在 background 里做「先 setTimeout 等一下再干活」的长流程浏览器随时可能把进程收走。正确姿势是把状态写成事件驱动收到消息 → 处理 → 返回结果完事就睡。2.4 content script 的监听与 sendResponse 时序chrome.runtime.onMessage.addListener((msg, sender, sendResponse) { if (msg.type GET_TITLE) { sendResponse({ title: document.title }); } });background 和 popup 发的tabs.sendMessage都会落到页面里的 content script。document.title在 DOM 就绪后一定存在。这里的sendResponse是同步模式调用完立刻返回如果你的监听函数里要等一个异步操作比如 fetch必须在回调里写return true;表示「我会在稍后手动调用 sendResponse」否则消息通道会提前关掉。这个return true在初学者代码里最容易漏。content script 再强调一句它和页面共享 DOM但 JavaScript 运行在隔离上下文里页面自己的window全局对象、Vue/React 实例你都拿不到。这个隔离问题到第 3 章「调用页面 js 函数」再专门破。3. 让例子动起来加载未打包扩展、注入页面与调用页面 js 函数3.1 chrome://extensions/ 加载与命令行两种姿势把上面三个文件放进一个目录目录名用英文。打开 Chrome地址栏输入chrome://extensions/右上角打开「开发者模式」左上角点「加载已解压的扩展程序」选中那个目录。此时浏览器工具栏会出现一个扩展图标固定它。改完代码后回到chrome://extensions/在扩展卡片上点刷新按钮刷新页面改动才生效。命令行方式用于自动化测试/Applications/Google Chrome.app/Contents/MacOS/Google Chrome \ --load-extension/absolute/path/to/extension \ --user-data-dir$(mktemp -d)Windows 下路径写法有差异C:\Program Files\Google\Chrome\Application\chrome.exe \ --load-extensionD:\dev\my-ext \ --user-data-dirD:\tmp\chrome-test-profile--load-extension需要一个绝对路径相对路径在某些版本里不认。--user-data-dir指定独立的浏览器配置目录我一般用它搭一个便携版 Chrome 式的测试环境和日常用的配置完全隔离登录状态、书签互不影响调试完直接删目录。如果你要兼容「便携版 chrome」用户也一样给它独立 user-data-dir插件、缓存互不污染。3.2 content script 注入给标题加一个高亮框// content.js const heading document.querySelector(h1); if (heading) { heading.style.outline 3px solid #ff9800; heading.style.outlineOffset 4px; }这段代码在document_idle时执行大多数页面h1已经存在直接改样式就能验证注入生效。刷新页面后看到标题多一圈橙色边框说明 content script 已经跑通。如果页面迟迟不出边框先看两处一是matches是否覆盖当前站点二是有些单页应用首屏靠异步渲染document_idle时h1还没挂上。这时候用MutationObserver等节点出现再操作不要靠setTimeout碰运气。3.3 popup 弹窗到页面发消息、收消息、处理异常在 manifest 的action里加一行default_popup: popup.html再新建popup.html和popup.js。!doctype html html body stylewidth: 260px button idreadBtn读取页面文本/button pre idoutput stylewhite-space: pre-wrap/pre script srcpopup.js/script /body /htmlasync function readPageText() { const [tab] await chrome.tabs.query({ active: true, currentWindow: true }); const output document.getElementById(output); try { const response await chrome.tabs.sendMessage(tab.id, { type: EXTRACT_TEXT }); output.textContent response.text.slice(0, 200); } catch (error) { output.textContent content script 未注入 error.message; } } document.getElementById(readBtn).addEventListener(click, readPageText);chrome.tabs.query拿到当前活动标签页currentWindow: true防止多窗口时取错。sendMessage把EXTRACT_TEXT发给页面脚本页面那边同样用onMessage监听并返回{ text }。这个往返链是插件开发里最核心的消息模式popup 只负责当入口真正读数据的是 content script。第一次运行如果报错看不懂绝大多数是权限问题。activeTab只在你点开 popup 的这一次生效所以读取逻辑放在按钮点击回调里而不是popup.js顶层提前执行。顶层一跑「用户还没点击授权」tabs.sendMessage自然被拒。3.4 浏览器插件如何调用页面 js 函数隔离世界的两种破法页面里可能定义了window.buildReport这样的函数你在 content script 里typeof window.buildReport得到的是undefined。不是函数不存在而是你根本不在页面的 JS 世界。做法一把 script 标签插进页面主世界。const script document.createElement(script); script.src chrome.runtime.getURL(inject.js); document.documentElement.appendChild(script);inject.js要在 manifest 里声明为可访问资源web_accessible_resources: [ { resources: [inject.js], matches: [all_urls] } ]inject.js里面可以window.pageFn?.()调用页面函数。这个做法的边界是它只拿到页面世界的执行环境拿不到 Vue 组件内部状态且 CSP 严格的站点可能拦掉外部 script。做法二用chrome.scripting.executeScript指定world。chrome.scripting.executeScript({ target: { tabId: tab.id }, world: MAIN, func: () window.pageFn?.(), });需要permissions里加scripting并且有对应站点的 host 权限。world: MAIN明确告诉浏览器在主世界执行不写这个参数默认是 isolated world照样调不到。这个方法适合「页面自己暴露了公开函数」的场景比如触发页面上某个已有的保存按钮逻辑。两种姿势各有用前者用来塞自己的长逻辑后者适合短表达式。无论哪种都不要做绕过站点自身安全策略的事只调用页面本就对外开放的函数。4. 把例子做成工具网页内容提取、表单抓取与媒体嗅探插件骨架跑通以后插件真正有生产力的方向有三类把页面上人工看的内容变成结构化文本把页面上的表单收集成 JSON把页面里的媒体资源变成一个可保存的列表。这三个方向分别对应了大家常搜的「网页内容提取工具」「chrome 网页表单抓取插件」「视频下载插件」下面按同一套消息结构往下走。4.1 网页内容提取工具标题、正文、链接一次导出function extractContent() { const title document.title; const main document.querySelector(article) || document.querySelector(main) || document.body; const links [...main.querySelectorAll(a[href])].map( (a) ${a.textContent.trim()} - ${a.href} ); return { title, text: main.innerText.slice(0, 2000), links: [...new Set(links)].slice(0, 50), }; } chrome.runtime.onMessage.addListener((msg, sender, sendResponse) { if (msg.type EXTRACT_CONTENT) { sendResponse(extractContent()); } });提取优先级是article→main→body这样在多数新闻页和文档站能自动避开侧边栏。innerText比textContent更接近人眼看到的排版代价是多一点计算量。截断 2000 字是为了避免 content script 到 popup 传一大段文本卡顿。链接去重用Set只保留前 50 条防止目录页几千个链接把消息通道堵死。如果想复制到剪贴板在 content script 里直接navigator.clipboard.writeText(text)就行。Chrome 把扩展页当成 secure context权限上一般能过如果个别站点不授权就在 background 里用chrome.scripting.executeScript再读一次。4.2 chrome 网页表单抓取插件受控组件和复选框别漏function extractForm() { const fields []; document.querySelectorAll(input, textarea, select).forEach((el) { const value el.type checkbox || el.type radio ? el.checked : el.value; fields.push({ name: el.name || el.id || anonymous, type: el.type || el.tagName.toLowerCase(), value, }); }); return fields; }checkbox 和 radio 的「值」不是value属性而是checked布尔这一步漏了抓回来的数据全错。el.value对文本输入框是实时的但对 React 和 Vue 的受控组件存在经典坑组件内部 state 没触发渲染时DOM 上的value属性可能停在初始值。遇到这种情况不要尝试去读框架实例隔离世界也读不到直接派发input事件让框架自己更新再等一个微任务读取。表单抓取的常见产出是 JSONpopup 端拿到后做JSON.stringify(fields, null, 2)用户复制出去就能喂给脚本批量处理。注意只抓字段名和值别把密码框的 value 往日志里打——typepassword的输入框默认跳过对表单工具这是底线。4.3 视频下载与媒体嗅探video downloadhelper、猫抓 Cat Catch 的原理网页媒体嗅探插件如 video downloadhelper、猫抓 Cat Catch 这类的通用原理不是破解播放器而是收集页面上真实存在的媒体资源地址再交给浏览器下载。实现上常见两种扫描 DOM 里的video/audio标签以及用chrome.webRequest观察网络请求。后者需要更多权限先给一个 DOM 扫描的最小实现function scanMedia() { const urls new Set(); document.querySelectorAll(video, audio).forEach((media) { if (media.currentSrc) { urls.add(media.currentSrc); } media.querySelectorAll(source).forEach((source) { if (source.src) { urls.add(source.src); } }); }); return [...urls]; }把扫描结果发给 backgroundchrome.runtime.onMessage.addListener((msg) { if (msg.type DOWNLOAD_MEDIA) { for (const url of msg.urls) { chrome.downloads.download({ url, saveAs: true, conflictAction: uniquify, }); } } });saveAs: true让每个文件弹出保存窗口用户有决定权conflictAction: uniquify在同名文件时自动加序号而不是覆盖。chrome.downloads.download需要permissions里的downloads。这段代码只对页面里已经公开的媒体地址生效对加密流、分段切片无能为力也不该去碰。做这类工具时说明里始终写一句「只下载你拥有版权的资源」既是规范也是自保。补充有些在页面后台播放、不在 DOM 里的媒体需要webRequest监听onBeforeRequest拿到请求 URL再判断 content-type 是否video/*或audio/*。MV3 里webRequest不能改写响应但拿 URL 列表没问题够用了。4.4 网页长图插件captureVisibleTab 拼接思路「chrome 网页出长图的插件」本质是整页截图。Chrome 没有一键截长图的 API常见做法是chrome.tabs.captureVisibleTab截当前视口滚动一段截一张最后用 canvas 把 dataURL 依次拼起来。这里有两个容易翻车的地方一是固定背景的页面滚动时内容错位要在同一位置截二是懒加载图片只在你滚动到它时才出现拼接前要等图片加载完。更稳的思路不是截图拼接而是把 4.1 提取出来的正文重新排版渲染成一张图。文章类页面用这个方案拼出来的长图反而更整齐也不会被懒加载搞出白条。5. chrome 浏览器插件开发避坑5 个最常见的翻车现场5.1 图标是灰的点了没反应现象扩展加载成功工具栏图标是灰的怎么点都不弹窗、不执行。原因manifest 里要么没写action要么还在用 MV2 时代的browser_action字段。MV3 统一成action后老字段直接被忽略入口自然就没了。解决在 manifest 加action: { default_popup: popup.html }如果需要点击图标立即执行后台逻辑则不写 popup改在 background 里监听chrome.action.onClicked。改完去chrome://extensions/点刷新按钮重新触发一次。5.2 出现「该扩展程序未列在 chrome 应用商店中」警告现象把本地打包好的.crx往 Chrome 里拖浏览器直接弹「该扩展程序未列在 chrome 应用商店中并可能是在您不知情的情况下添加的」然后拒绝安装。原因Chrome 对非商店来源的 crx 默认拦截。这个提示同样出现在被其他程序偷偷塞进浏览器的扩展上所以浏览器宁可误杀也不放行。解决开发调试不要拖 crx。进入chrome://extensions/开开发者模式点「加载已解压的扩展程序」选 manifest.json 所在目录。目录要干净别把上一轮打包生成的.pem密钥文件也放进去技术上无害但泄露 pem 会让人伪造你的扩展更新。5.3 content script 的 CSS 把页面样式搞乱了现象插件在 UI 里写了body { font-size: 15px }或者直接往 document 里插 style 标签结果页面本身字体、按钮全变样。原因content script 的 DOM 操作和样式覆盖的是同一个页面文档全局选择器没有任何隔离网站自己的样式也能反过来把插件 UI 顶飞。解决所有 class 加项目前缀比如mcex-widget、mcex-highlight不要用body、*这类通配选择器。如果插件要做弹窗或面板用el.attachShadow({ mode: open })把内容关进 shadow root这是最省心的隔离方式。注意 shadow root 里的表单值不会自动同步到页面需要手动读出来再发送。5.4 Receiving end does not exist 消息错位现象background 或 popup 调用tabs.sendMessage报错说接收端不存在但页面明明是打开状态。原因页面里没有注入 content script或者注入脚本还没就位。matches没覆盖当前站点、页面刚刷新、单页应用切路由导致旧脚本状态丢失都是常见来源。解决先检查 manifest 的content_scripts匹配规则不要在一个页面加载完立刻发消息等document_idle之后给 sendMessage 包 try/catch让失败提示反馈到用户界面而不是默默断掉。还有个团队内部经验在 content script 监听里始终return true即使同步响应异步消息通道也不容易被过早回收。5.5 Win7 用户卡在 Chrome 109老环境兼容性怎么测现象本地最新版 Chrome 一切正常发给客户后说插件不显示、点了没反应一问系统Win7。原因Chrome 109 是 Windows 7 能装到的最后一个版本之后的新版本不再支持该系统。虽然 109 完整支持 Manifest V3但部分新 API 或行为与最新内核存在差异。解决开发期准备一个便携版 Chrome配合--user-data-dir指定同版本内核做回归测试代码里对可能存在差异的 API 做typeof chrome.xxx ! undefined判断不存在就在 UI 里提示「当前浏览器版本不支持该功能」。别只在最新版里自嗨老环境掉链子的概率比你想的高。6. 进阶调试技巧命令行打包、chrome devtools mcp 与我的验证习惯6.1 命令行加载和打包加载调试除了界面上点按钮自动化脚本里更常用--load-extension。真正分发的时候到chrome://extensions/点「打包扩展程序」会生成.crx和.pem.pem是私钥丢了就等于丢了更新资格。早期 Chrome 也有--pack-extension命令行参数对批处理有用但界面按钮足够日常不用专门折腾。给一个自动化冒烟示例chrome.exe --headlessnew --disable-gpu --load-extensionD:\dev\my-ext \ --user-data-dirD:\tmp\chrome-test-profile https://example.com--headlessnew在部分新版本可用能省一个窗口但截图和调试不那么直观。如果只是验证注入效果我更建议带界面跑配合--user-data-dir用完删目录。6.2 chrome devtools mcp把浏览器交给工具做冒烟现在开发调试有一个很顺手的组合给编辑器或 AI 助手接一个 chrome devtools mcp 服务让它唤起 Chrome、切标签、执行表达式、抓 console 和网络面板。插件写完让 MCP 打开测试页、刷新扩展、点按钮看 console 里有没有报错一轮冒烟一分钟跑完。这比我手动点 tab 快特别适合 content script 反复注入的场景。我一般把 mcp 指向--user-data-dir的临时 profile和正式登录环境分开避免测试过程污染 cookie 状态。6.3 一个稳定的验证顺序还有一个习惯每改一次代码按固定顺序重验——先到chrome://extensions/刷新扩展卡片再停掉 service worker卡片上能看到它的运行状态最后开一个没访问过的干净页面跑一遍最小操作。顺序反了会踩到缓存里的旧脚本看起来像「改没生效」这种玄学问题浪费过不少时间。这套流程帮我躲掉了大量返工。「玄学」对我来说基本等于「缓存没清、消息没等到、权限没触发」三件事。希望帮到你。本文还有配套的精品资源点击获取
返回列表