ARTICLE DETAIL

资讯详情

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

Chrome扩展Manifest V3完全指南:manifest.json核心字段与避坑实践

Chrome扩展Manifest V3完全指南:manifest.json核心字段与避坑实践 在 Chrome 浏览器插件开发这件事上manifest.json基本称得上“一票否决”的存在不管你的功能逻辑写得多漂亮清单文件里某个字段写错插件就可能直接加载失败或者发布时被商店审核驳回。我从 MV2 时代一路迁到 MV3踩过不少坑这篇就把 Chrome 扩展 V3 版本中manifest.json的全部核心字段拆开讲一遍结合我在实际项目里的踩坑记录给你一份可以直接照着抄的参考。1. 先说清楚Manifest V3 到底改了什么东西1.1 Chrome 为什么强制切换到 V3Manifest V3以下统一叫 MV3从 Chrome 88 开始正式支持到 Chrome 109 之后新提交到应用商店的扩展只接受 MV3旧的 MV2 扩展也陆续被强制迁移或停用。谷歌官方的理由归纳起来就三件事安全、性能、隐私。MV2 时代的扩展可以常驻后台页面只要后台background.html没被关掉就能一直占着内存还能拿着“全权限”去做用户根本不知道的事。MV3 把后台改成基于事件的Service Worker用不到就销毁用完再唤醒权限上强制拆分permissions和host_permissions你要访问哪些站点、要动哪些浏览器能力必须分门别类地声明清楚。这三板斧下来插件开发的思维要跟着变不再是“我常驻后台等你调”而是“我埋好事件监听等浏览器叫我”。这也是为什么manifest.json在整个 MV3 体系里比其他扩展配套文件都更值得反复研读——它是浏览器理解你插件的第一道门槛更是审核人员判断你插件是否合规的重要依据。1.2 Manifest.json 究竟扮演什么角色简单说manifest.json是浏览器识别你用意的说明书。它放在扩展目录的根目录名字固定不能改内容必须是合法的 JSON。浏览器加载扩展时先解析这个文件来回答几个问题你这个扩展叫什么、什么版本、用来干什么name、version、description你的入口在哪里点图标之后弹什么action你要在后台干什么谁来监听这些事件background.service_worker你要往页面里注脚本还是改样式content_scripts你申请了哪些权限、能访问哪些网站permissions与host_permissions你允许哪些外部资源加载到你的插件的页面里web_accessible_resources。可以这么理解如果扩展是你要开的一家公司那manifest.json既是工商执照也是公司章程还是资产与责任清单。后续所有代码都必须围绕它声明的边界来写超过边界就会被浏览器拦截少了某项声明功能就直接失效。MV2 迁移到 MV3顶层字段从“按类型放进background页”改成“service worker 脚本”字段级别的语义变化非常细碎但逻辑是一脉相承的。接下来我把字段按权限级别、界面入口、后台行为、内容注入四个维度完整讲一遍。2. Manifest.json 字段逐个拆解2.1 身份字段manifest_version、name、version、description、default_locale{ manifest_version: 3, name: 示例插件页面标题助手, version: 1.0.0, description: 一键查看并复制当前页面标题, default_locale: zh_CN }manifest_version必须写成整数3写成字符串3或者2都会直接报错。这项是整个清单文件的“版本证明”决定了解析器用哪一套规则来读后面的字段。如果你的插件还留着 MV2 的browser_action、background.page在 MV3 下浏览器会直接忽略或者加载失败并不会帮你自动转换。name是展示给用户和商店的名字建议控制在 30 个字符内Chrome 网上应用店会截断过长的标识。version必须是 1 到 4 个由点分隔的整型数字比如1.0、1.0.0.5合法1.0-beta不合法。注意商店不支持每次都重新编号你迭代发布时只能递增这个版本号且不能使用前面带零的段比如01.0.0会报错。description建议控制在 132 个字符内这是商店展示的摘要长度写太长也会被截断。default_locale不是必填项但如果你要用_locales做多语言就必须声明并且目录结构中必须存在_locales/zh_CN/messages.json等对应文件否则加载会失败。踩坑记录我第一次写多语言扩展把default_locale写成zh-cnChrome 直接识别不了。规范里要求用下划线如zh_CN、en连字符的写法在_locales目录校验时会直接报错。2.2 界面入口字段action、icons、options_page、options_ui{ icons: { 16: icons/icon16.png, 32: icons/icon32.png, 48: icons/icon48.png, 128: icons/icon128.png }, action: { default_title: 页面标题助手, default_popup: popup.html, default_icon: { 16: icons/icon16.png, 32: icons/icon32.png } }, options_page: options.html, options_ui: { page: options.html, open_in_tab: true } }icons会用在商店列表、右键菜单、扩展管理页建议至少提供16、32、48、128四档。少了48和128在提交商店时会被提示缺少必要尺寸本地加载到不会挂但真实分发场景里不能偷懒。action是 MV3 对应以前browser_action的字段。它定义工具栏按钮的基本属性default_title是鼠标悬停时的提示文字default_popup是点按钮弹出的 HTML 页面default_icon管理不同尺寸下的图标。popup 里可以写 HTML、CSS、JS但它的生命周期跟点击行为绑定点开出现、点别处关闭不能长期存活所以不要试图在 popup 里维持全局状态。options_ui负责设置页。open_in_tab: true表示在单独标签页打开适合设置项较多的插件false则在弹窗里嵌一个迷你页面。很多开发者习惯直接用options_page但提交审核时会建议改成options_ui因为后者在 Chrome 和 Edge 的兼容性、窗口外观一致性上都更好。2.3 后台逻辑字段background.service_worker{ background: { service_worker: background.js, type: module } }MV3 的background不再接受page或scripts数组只支持service_worker一个入口脚本。这是 MV3 最核心的变化后台脚本从“常驻页面”变成“可休眠的事件处理器”。type: module表示用 ES Module 方式加载这样background.js里就能用import引入别的模块不加这个字段也可以后台脚本默认按普通脚本执行。用了type: module后顶层this不再是 window 上下文而是 worker 全局对象DOM API 不可用初始化代码最好都扔到事件监听器的回调里而不是在文件顶层执行完就不管了。Service Worker 会被浏览器反复休眠和唤醒休眠时全局变量全部丢失。如果你之前习惯用全局变量缓存登录状态或者页面数据MV3 下必须改掉数据要么放进chrome.storage要么在每次事件回调里通过chrome.storage.session重新拉取。Manifest V3 提供了chrome.storage.session专门用来存会话级数据内存读写比chrome.storage.local还快而且会跟随 worker 生命周期自动清空非常适合做临时状态缓存。2.4 权限声明permissions、host_permissions、optional_permissions{ permissions: [ storage, tabs, activeTab, scripting, notifications ], host_permissions: [ https://*.example.com/* ], optional_permissions: [ clipboardWrite, downloads ], optional_host_permissions: [ https://*/* ] }permissions是浏览器 API 权限常见项包括storage使用 chrome.storage、tabs读取标签页标题、URL、activeTab临时获得当前活动标签的注入权限、scripting动态执行脚本这是 MV3 收紧了tabs.executeScript之后的替代方案、notifications桌面通知。host_permissions则是匹配哪些网站被允许“被插件代码访问”。MV3 把这两者彻底分开后前者管能力、后者管范围。比如你想在特定域名下读取页面 DOM就必须同时有scripting能力和对应域名模式的host_permissions。匹配模式写法遵循 Chrome 的匹配规则比如https://*.example.com/*表示 example.com 的所有子域名都能访问all_urls是全部站点但审核时会非常敏感。optional_permissions和optional_host_permissions用于“运行时才申请的权限”。这对用户体验很重要基本权限只要最小集等用户点设置或某个功能时再触发权限弹窗。但注意activeTab是一个很特殊的权限它不写进optional_permissions也不需要在host_permissions里声明对应域名只要你点工具栏按钮或者调用chrome.action相关 API就会临时获得当前活动标签页的注入权限这块权限在用户离开当前标签页后就自动收回。平时能用activeTab解决的问题就别申请大范围host_permissions,这不仅是安全习惯也是商店审核的隐形加分项。2.5 内容注入content_scripts{ content_scripts: [ { matches: [https://*.example.com/*], js: [content.js], css: [content.css], run_at: document_idle, all_frames: false } ] }content_scripts负责在匹配的页面上自动注入脚本和样式。matches必填决定哪些页面会注入js和css是注入文件列表run_at可以设document_start、document_end、document_idle表示脚本注入的时点document_idle最常用页面大部分解析完成后执行all_frames设为true才让 iframe 里的页面也执行。内容脚本和页面是隔离世界内容脚本的 JS 变量不会污染页面全局变量页面里的 JS 也拿不到内容脚本里的变量。但两边共享同一个 DOM所以你可以改页面上的交互逻辑不能直接调用页面里定义的 function。要跟页面 JS 通信需要走window.postMessage或者借助 DOM 事件桥接。加一句经验之谈如果发现内容脚本没生效先看matches模式对不对再看有没有被host_permissions覆盖。MV3 里内容脚本的匹配声明在content_scripts.matches但如果你在background里用chrome.scripting.executeScript动态注入那还需要在host_permissions声明目标域名两套机制互相配合刚上手很容易漏掉前面那个。2.6 功能增强字段commands、web_accessible_resources、content_security_policy、externally_connectable{ commands: { toggle-feature: { suggested_key: { default: AltShiftT, windows: AltShiftT, mac: CommandShiftT }, description: 切换功能开关 } }, web_accessible_resources: [ { resources: [images/*.png, injected.js], matches: [https://*.example.com/*] } ] }commands注册快捷键。suggested_key中default是默认组合键Windows、macOS 下可以单独定义。注意 Chrome 不允许你霸占系统或浏览器固有快捷键如果组合冲突用户可以在chrome://extensions/shortcuts页面手动改即便如此suggested_key仍然要尽量挑不和常见快捷键冲突的组合。web_accessible_resources是 MV3 下变化较大的字段。MV2 时代可以直接写web_accessible_resources: [*.png]全局开放MV3 必须指定resources和matches两个属性只允许匹配的网站通过chrome-extension://URL 访问这些资源。注意这里的matches不能写*必须写具体的匹配模式否则校验直接不通过。MV3 里的content_security_policy也变得更严格。扩展默认的 CSP 已经限制内联脚本的执行你如果要用外部域名字体或需要评估wasm可以写{ content_security_policy: { extension_pages: script-src self; object-src self;, sandbox: script-src self wasm-unsafe-eval; } }extension_pages控制扩展自身页面的 CSPsandbox控制沙箱页面。强烈不建议在extension_pages里放开unsafe-eval或者引入远程脚本因为商店审核已把这类规则列为高危而且放开后等于自己拆了安全护栏。externally_connectable是给“其他扩展或网页”主动与你的扩展通信做白名单的比如你想让某个网页通过chrome.runtime.sendMessage给你扩展发消息就必须配置matches字段允许相应来源。3. 从零搭建一个 V3 插件的完整清单3.1 一个可以直接跑通的最小可运行示例我每次写新插件都会先搭一个最小的干净骨架。以“页面标题助手”为例完整目录结构大致这样title-helper/ ├── manifest.json ├── background.js ├── popup.html ├── popup.js ├── icons/ │ ├── icon16.png │ ├── icon32.png │ ├── icon48.png │ └── icon128.pngmanifest.json这样写{ manifest_version: 3, name: 页面标题助手, version: 1.0.0, description: 一键查看并复制当前页面标题, icons: { 16: icons/icon16.png, 32: icons/icon32.png, 48: icons/icon48.png, 128: icons/icon128.png }, action: { default_title: 页面标题助手, default_popup: popup.html, default_icon: { 16: icons/icon16.png, 32: icons/icon32.png } }, background: { service_worker: background.js }, permissions: [tabs, activeTab], content_scripts: [ { matches: [https://*/*], js: [content.js] } ] }我把tabs权限放进最小集是为了读取当前活动标签页的title与urlactiveTab则用来保证点击按钮时临时拥有当前页面的访问权限。这里没有申请storage权限因为示例功能不需要持久化数据。很多初学者不知道“权限是要按需申请的”一口气写十几个等商店审核人员逐条质询时又解释不清反而拖慢过审。background.js先放一个最简单的监听验证 worker 能正常被唤醒chrome.runtime.onInstalled.addListener(() { console.log(扩展已安装版本, chrome.runtime.getManifest().version); }); chrome.tabs.onActivated.addListener(async (activeInfo) { const tab await chrome.tabs.get(activeInfo.tabId); console.log(当前激活标签页, tab.title); });popup 页面里用chrome.tabs.query({ active: true, currentWindow: true })读当前页标题复制到剪贴板document.getElementById(copy).addEventListener(click, async () { const [tab] await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab) return; await navigator.clipboard.writeText(tab.title || ); document.getElementById(status).textContent 已复制; });注意 popup 里不能用alert也不建议直接执行费时逻辑因为它是一个浮层关闭即销毁。在这个小例子里navigator.clipboard在 popup 浮层运行时是可用的但如果你在后台 service worker 里调用剪贴板则需要额外申请clipboardWrite权限。3.2 MV2 到 MV3 的字段对照表迁移的时候最容易犯迷糊的就是“同一个功能在两边字段名不一样”。我整理了一张常用对照表建议直接保存功能类型MV2 写法MV3 写法清单版本manifest_version: 2manifest_version: 3工具栏按钮browser_action: {...}action: {...}后台页面background: { page: background.html }background: { service_worker: background.js }多个后台脚本background: { scripts: [a.js, b.js] }用importScripts()或 ES Module 导入域名权限写进permissions拆分到host_permissions动态脚本注入chrome.tabs.executeScript()chrome.scripting.executeScript()权限弹窗时机安装时统一授权支持运行时optional_permissions申请远程代码允许配置 CSP 后加载默认禁止商店对远程代码一律不通过MV2 转 MV3最痛苦的是后台逻辑重写。原本在background.html里可以加载 jQuery、可以操作 DOM、可以维护全局变量现在统统不行。比较好的迁移策略是先把后台脚本拆成纯事件处理器只在chrome.*事件回调里写逻辑所有数据落到chrome.storage再走scriptingAPI 动态注入能力。3.3 V3 开发落地时容易忽略的细节除了字段本身还有几个跟manifest.json强相关的整体逻辑必须注意。第一扩展 ID 的稳定性。未打包的加载插件如果没有key字段每次重新加载都可能生成新的扩展 ID导致你在chrome.runtime.id或storage里存的数据全乱。最稳妥的办法是打包成.crx时读取生成的文件里的key或者用--pack-extension参数固定密钥再把key写进manifest.json的key字段。非商业项目可能无所谓但涉及 OAuth 回调或者网站白名单时这个坑会坑到人。第二存储的键名规划。MV3 的chrome.storage.local是异步读写适合在 JSON 里规划统一的数据结构。每次改storage结构前要做好兼容因为用户本地可能存着老版本的数据升级后直接按新结构解析会报错。建议在chrome.runtime.onInstalled里做一次版本检测和迁移把老数据改写成新格式。第三声明文件之外的资源打包方式。manifest.json里声明的icons、popup、content_scripts、web_accessible_resources都会被打进包里但其他没用到的文件也会一股脑打包。商店审核和安装体积都会受影响建议在打包前清理掉测试脚本和临时图片不然商店后台会提示安装包过大。4. 常见问题与排查技巧实录4.1 插件加载失败的第一时间检查清单如果你本地加载 MV3 插件时看到“Manifest file is invalid”别急着怀疑 Chrome 坏了直接按这几步排查整个文件是否合法 JSON有没有注释、尾逗号。manifest.json是严格 JSON不允许写注释很多从 JSON 配置迁移过来的朋友会习惯性写// 注释直接加载失败manifest_version是否写成了3字符串。整数和字符串在 JSON 里是不同的类型Chrome 校验不通过background.service_worker的文件路径是否真实存在。路径写错也一样加载失败而且报错提示不会告诉你具体文件没找到只能自己核对content_scripts.matches是否为空或不符合匹配模式。空数组会导致内容脚本完全失效有时不会报错但功能就是没反应web_accessible_resources里matches是否写成了*。MV3 只接受具体匹配模式或all_urls这种完整表达式裸*会被校验器直接踢出。每次改完manifest.json到chrome://extensions页面点圆形刷新按钮重新加载再点“Service Worker”旁边的“查看”按钮看控制台输出。很多后台问题在浏览器自身控制台看不到必须打开 worker 专属控制台。4.2 商店审核里的常见驳回点商店审核虽然是人工结合自动规则但核心逻辑还是“最小权限原则”和“无远程代码”。权限过界你只做个天气插件却申请了history和all_urls审核不会通过。最佳实践是只申请当前功能真正用到的权限并把大范围请求留到optional_permissions由用户主动触发。远程代码MV3 起任何通过远程地址加载并执行的 JavaScript 都会被驳回。所有脚本必须打包在扩展内部。很多在线模板网站展示的“联网引入库”方案在商店发布根本走不通。个人数据说明如果插件确实要收集用户数据商店要求有隐私政策链接并在manifest.json里准确声明权限用途。建议每个申请权限都写清业务用途这也是审核快速通过的关键。4.3 我常用的调试手段chrome://extensions里打开“开发者模式”加载解压缩的扩展目录日常开发都用这个改完代码点刷新即可右键扩展图标选择“检查弹出式页面”可以直接像 DevTools 一样调试 popup 的 DOM 和 JS在background.js里打印日志后点“Service Worker”旁边的链接打开 worker 控制台能看到chrome.tabs.onActivated等事件是否被正确触发用chrome://net-internals可以查某些网络相关问题但涉及扩展自身请求更多还是看控制台的 Network 面板。最后一个提醒manifest.json里所有路径都是相对扩展根目录的不要用绝对路径也不要用 URL 编码。写错路径通常是打开扩展看到 404 的元凶。5. 我踩过几次坑之后的一些心得如果只能挑一条最重要的经验说那就是“manifest.json不只是配置文件它是插件的边界”。不要试图在字段之外钻空子比如借用unsafe-eval绕过 CSP或者把一堆权限塞进optional_permissions等用户点完再偷跑。Chrome 的 MV3 设计初衷就是让每个扩展都在明面上说清自己要什么、能去哪配合好这套规则开发体验反而更清爽。具体落地时我一般都会在新建项目的第一时间把manifest.json写严谨用action加最小权限跑通一次“加载 → 点击 → 响应”的闭环再逐步往上加service_worker逻辑和content_scripts。这个顺序能让你快速发现问题范围清单字段错误还是后台逻辑错误还是在页面注入环节出了问题基本一次定位。另外建议平时多维护一份符合自己团队的 manifest 模板把常用权限、图标尺寸、options_ui、commands这些都预先写好注释新项目直接复制再改。插件开发的细节非常琐碎靠记忆不如靠一套扎实的模板兜底毕竟这些字段值背后的坑我算是用一个又一个版本踩明白了。
返回列表