
1. 為什么現在還要重新啃 Manifest.jsonMV3 的一場強制遷徙2023 年初開始Chrome Web Store 就關閉了 Manifest V2 插件的新提交通道到 2024 年更是全面停止了對 MV2 擴展的支持。這意味著什么如果你手上還有一套用browser_action、background.scripts寫的老插件哪怕它運行得好好的也會在用戶瀏覽器里被強制禁用取而代之的是一個灰色圖標加上已停用的提示。很多開發(fā)者就是在這個節(jié)點上被迫開始讀 Manifest.json 文檔的我也不例外。當時我手上有三個線上插件要遷移翻遍官方文檔和各類碎片文章發(fā)現了一個尷尬的事實Manifest V3 的字段說明散落在不同頁面很少有文章能從頭到尾把所有字段講清楚更沒有幾篇能告訴你這個字段在 MV3 里已經改了、那個字段千萬別照舊填。如果你正準備開發(fā)一個新的 Chrome 插件或者正在為老插件做遷移那么 Manifest.json 就是你繞不開的第一道門。它不只是一個聲明文件它決定了你的插件能拿到什么權限、能跑哪些代碼、能在什么頁面里注入腳本、UI 長什么樣。我在做遷移的過程中因為字段理解偏差踩了不少坑有些坑在官方文檔里壓根不會寫。這篇文章我就按照自己實際操作過的順序把 MV3 版本 Manifest.json 的字段從頭到尾拆一遍重點標注那些和 MV2 差異巨大、特別容易翻車的地方。文章適合三種人第一次寫插件的新手、從 MV2 遷移的老手、以及想在發(fā)布前自查一遍配置細節(jié)的開發(fā)者。在進入逐字段分析之前先想明白一件事MV3 的整體設計思路說白了就是收緊權限、消滅常駐后臺、禁止遠程代碼。這三個原則幾乎解釋了 Manifest.json 里所有字段的增刪改變化。你只有把這三個原則刻在腦子里再看字段列表時就不會覺得有些限制莫名其妙了。2. 三個看不見但決定一切的 MV3 設計原則2.1 常駐后臺頁被砍掉一切邏輯交給 Service WorkerMV2 時代我們習慣在后臺配置一個background.html里面掛一個長期運行的 JavaScript 環(huán)境用來監(jiān)聽事件、維護狀態(tài)、處理消息。這個頁面只要瀏覽器開著就會運行哪怕什么都不做也在消耗內存。MV3 直接把這種模式廢了換成了 Service Worker——一種用完即走的腳本環(huán)境插件被觸發(fā)時才啟動、空閑一段時間后自動休眠下次再被事件喚醒時重新執(zhí)行。這個設計對 Manifest.json 最直接的影響就是background字段的寫法完全不同了。MV2 里常見的是background: {scripts: [bg.js], persistent: true}到了 MV3 變成background: {service_worker: bg.js}。別小看這個變化persistent字段整個消失了因為 MV3 里不存在持久后臺的概念而且service_worker只能指定一個文件不能再像 MV2 那樣寫一個數組。如果你想按模塊拆分代碼需要在background.service_worker.type里設置為module用 ES Module 的方式來組織代碼。我剛遷移時在這個坑里卡了兩天我把老代碼拆成了background.js和utils.js兩個文件按照 MV2 的慣性把它們寫進數組結果 MV3 直接報錯Service worker cannot be an array改成只保留一個入口文件后又發(fā)現我在background.js里用import導入utils.js的函數報錯說import語句只能用在外層 module 環(huán)境。最后才發(fā)現要加type: module。這一個小字段官方文檔里放在不起眼的角落但不知道它的話ES Module 寫法就是跑不通。2.2 權限邊界大幅收窄h(huán)ost_permissions 被單獨拎出來MV2 里所有權限都堆在一個permissions數組里包括tabs、storage、http://*/這類站點權限。MV3 把訪問哪些網站這種高危權限單獨拆成了一個字段host_permissions和permissions分開放置。邏輯很清楚permissions管的是 Chrome 提供的 API 能力比如storage存儲、alarms定時器、clipboardRead讀剪貼板而host_permissions管的是你的腳本能跑在哪些域名上比如https://*.example.com/*。為什么要這么拆因為在 Chrome 的權限提示 UI 里用戶可以清楚地看到這個插件能讀取你在所有網站上的數據而不會把它和插件能使用存儲 API混為一談。如果你只聲明了host_permissions而沒有在permissions里聲明對應 API那么你能往頁面上注入腳本但未必能調用chrome.storage反過來你聲明了storageAPI 權限但沒聲明任何host_permissions你的腳本就哪兒也去不了。這兩個字段是配合關系不是替代關系。我在給一個網頁標注工具做遷移時把permissions: [activeTab, scripting, storage, https://*/*]直接復制到了 MV3Chrome 倒是沒報錯但審核時被拒了理由是權限申請范圍過大。后來我改成permissions: [activeTab, scripting, storage]加上host_permissions: [https://*/*]的組合。這里有個隱藏邏輯如果只用activeTab其實可以完全不用host_permissions因為這個權限會讓你在用戶主動點擊插件圖標時獲得當前標簽頁的一次性訪問權。能少聲明就少聲明審核更容易通過用戶安裝時的安全感也更強。2.3 遠程代碼全面禁止CSP 規(guī)則從字符串變成對象MV2 時代在content_security_policy里你可以寫script-src self https://some-cdn.com;然后直接在插件頁面里引入遠程 CDN 的腳本。MV3 一刀切了推廣線上 CSP 只允許self不允許任何遠程源、不允許eval、不允許wasm-eval。這意味著你的插件代碼、依賴庫、所有邏輯都必須打包進插件本地的文件里想在運行時從服務器拉腳本沒門。對應到 Manifest.json 里MV3 的content_security_policy字段變成了一種對象結構包含extension_pages和sandbox兩個屬性。extension_pages管的是插件自帶的頁面比如彈窗頁、設置頁、獨立標簽頁這個值基本只能設為script-src self; object-src self;幾乎沒有操作空間sandbox才是留給你自由發(fā)揮的空間如果你需要做一些不安全的操作比如通過eval執(zhí)行動態(tài)代碼可以讓這些代碼在沙盒頁面里跑。我之前寫過一個小工具需要在彈窗頁里動態(tài)拼一段模板字符串然后執(zhí)行MV2 里靠eval就解決了。遷到 MV3 后發(fā)現整個彈窗頁控制臺全是 CSP 報錯代碼一行都跑不了。最后只能把動態(tài)執(zhí)行的部分單獨拆到一個沙盒 iframe 頁面里然后在主頁面和 iframe 之間用postMessage通信。這個改動上線后反而更穩(wěn)定了因為沙盒頁面里的錯誤不會影響主頁面。如果你習慣了在插件里從遠程服務器拉取最新腳本做熱更新MV3 下這個方案基本是死路必須改為發(fā)版更新。3. 基礎信息字段逐個拆name、version、icons 里的小監(jiān)獄3.1 manifest_version、name、version格式不對連加載都失敗先說最基礎但最容易被忽略的。manifest_version在 MV3 下必須寫整數3而且必須是數字類型不能寫字符串3。這是 Chrome 在解析時直接強校驗的寫錯的話插件在chrome://extensions頁面會顯示無法加載清單文件。name字段限制是 45 個字符description限制是 132 個字符。這兩個都算好說真正坑人的是version。MV3 要求版本號最多 4 段數字每段只能是 1 到 2 位數字也就是說1.0、1.0.2、1.2.3.4都合法但1.0.0.0.0不合法1.10合法因為10是兩位數1.100就出問題了——等等每段允許 1 到 2 位數字所以1.100不合法。很多開發(fā)者在這里翻車是因為他們用日期當版本號比如2024.12.31看起來沒問題但如果月份或日期變成三位數呢而且 Chrome Web Store 的版本號必須是遞增的你傳了一個比線上更低的版本號上去直接給你拒回來。我自己的做法是維護一個簡單的遞增規(guī)則主版本號.功能版本號.修復版本號比如2.4.1。每次提交前先在本地跑一遍chrome --pack-extension打包驗證確保版本號能被解析。如果你用自動化發(fā)布腳本記得在腳本里加一個版本號遞增的檢查邏輯不然 CI 里很容易因為手滑把版本號填低了導致發(fā)布失敗。3.2 default_locale、key、icons平時不起眼缺了就出亂子default_locale這個字段是在你要做國際化i18n時才需要的。一旦你設置了它就必須在插件根目錄下建一個_locales文件夾里面至少有一個和default_locale值對應的語言文件。比如default_locale: zh_CN時_locales/zh_CN/messages.json必須存在否則插件加載直接報錯。這個字段的設計初衷是告訴 Chrome 你的默認語言是什么然后 Chrome 會優(yōu)先加載用戶瀏覽器語言對應的 messages 文件找不到再回退到默認語言。key字段可能是所有字段里最容易被誤解的一個。它不是你在 Manifest.json 里手寫的而是 Chrome 在你打包插件時生成的。它的作用是固定插件的 ID這樣無論用戶從商店安裝還是你用--load-extension本地加載插件 ID 都是一樣的。這對依賴固定 ID 的開發(fā)場景特別重要比如你通過externally_connectable讓別的擴展和自己通信時ID 變了就全斷了。如果你在開發(fā)環(huán)境里發(fā)現插件 ID 每次刷新都不一樣那大概率是沒設置key字段。怎么拿 key先在瀏覽器里加載一次未打包的擴展然后在chrome://extensions頁面開啟開發(fā)者模式查看擴展詳情復制它的 ID再到擴展目錄里用工具生成對應 Base64 的 key填進 manifest 后重新加載就能固定 ID 了。icons字段聲明 16、32、48、128 四種尺寸的圖標很多新手會把四個尺寸都設置成同一張圖片這其實不太對。16 和 32 是頂欄工具欄用的圖標太小的話會糊48 是擴展管理頁大圖標128 是商店列表頁圖標。最省事的方案是準備一張 128x128 的源圖然后用工具導出四個尺寸。注意icons里的路徑是相對 Manifest.json 所在目錄的不要用../這種寫法會解析失敗。4. 核心功能字段全面拆解從 background 到 content_scripts 到 action4.1 background.service_worker注冊規(guī)則與生命周期控制在 MV3 里background字段最常見的寫法是這樣background: { service_worker: service-worker.js, type: module }service_worker的值是相對于插件根目錄的路徑。type設為module時這個 worker 會以 ES Module 的身份執(zhí)行你可以在里面用import語句引入其他模塊這是目前最推薦的寫法。但注意MV3 的 Service Worker 有幾個和普通頁面腳本完全不同的特性理解它們才能正確配置字段它是事件驅動的空閑約 30 秒就會被終止。所以你不能在 worker 里掛一個全局變量長期保存狀態(tài)狀態(tài)要存到chrome.storage或 IndexedDB。監(jiān)聽器必須在頂層同步注冊。如果你在chrome.runtime.onInstalled.addListener的回調里再注冊其他事件的監(jiān)聽worker 休眠后這些監(jiān)聽器可能丟失。在 worker 里訪問 DOM 是不行的document、window這些對象都不存在。我早期犯過一個錯我在 worker 里用了setInterval定時去輪詢接口想著這跟 MV2 的后臺頁一樣會一直跑。結果每次休眠喚醒后定時器就斷了甚至接口請求都會因為 CORS 問題失敗。后來我把定時任務改成使用chrome.alarmsAPI配合chrome.storage存儲上一次的輪詢時間戳才做到了永久任務。如果你原本在 MV2 里用setInterval做了很多周期任務遷移到 MV3 時務必要全部改成chrome.alarms。4.2 content_scripts注入時機與匹配規(guī)則的細節(jié)content_scripts字段在 MV2 和 MV3 里形式差不多但有幾個細節(jié)需要注意。一個典型的配置content_scripts: [ { matches: [https://*.example.com/*], js: [content.js], css: [content.css], run_at: document_idle, all_frames: false, world: ISOLATED } ]matches是匹配規(guī)則寫法是 Chrome 匹配模式match pattern比如https://*/*、http://localhost/*、all_urls。注意不要在matches里出現http://localhost:8080/*這種帶端口的寫法端口在匹配模式里是不支持的如果你要匹配本地開發(fā)地址得寫成http://localhost/*然后靠include_globs或 JS 里自行判斷端口。run_at有三個可選值document_startDOM 剛開始構建、document_endDOM 解析完、document_idle頁面加載完默認值。如果你要在頁面加載早期攔截某些請求就需要document_start如果只是往頁面上加個按鈕document_idle足夠。這個字段直接影響腳本的執(zhí)行時機改錯了會導致找不到 DOM 元素。world是 MV3 新增的字段取值為ISOLATED默認或MAIN。ISOLATED表示腳本運行在獨立的 JavaScript 環(huán)境中和頁面本身的 JS 環(huán)境隔離——頁面里的全局變量你訪問不到你定義的變量也不會污染頁面MAIN則表示腳本注入到頁面自己的世界可以和頁面腳本共享 DOM 和全局變量。需要注意的是MAIN模式下你的腳本等于和頁面里其他腳本平起平坐頁面腳本可能會惡意修改你的方法所以除非真需要操作頁面自己的全局函數否則保持默認ISOLATED就好。4.3 action 與 commands工具欄按鈕、彈窗和快捷鍵MV2 里的browser_action和page_action在 MV3 被統(tǒng)一成了action。因為很多插件其實不需要區(qū)分工具欄按鈕一直可見和只在特定頁面可見Chrome 干脆合并了統(tǒng)一默認可見然后用程序控制在特定頁面禁用按鈕。Manifest.json 里action字段的典型寫法action: { default_popup: popup.html, default_title: 點擊打開面板, default_icon: { 16: icons/icon16.png, 32: icons/icon32.png } }default_popup指定點擊按鈕后彈出的 HTML 頁面路徑這個頁面有自己的獨立窗口寬高受限最高 800x600。如果你不想用彈窗而是想在點擊按鈕后通過chrome.scripting.executeScript執(zhí)行一段腳本那就不需要default_popup只需要在background里監(jiān)聽chrome.action.onClicked事件。注意一個坑一旦設置了default_popupchrome.action.onClicked事件就不會觸發(fā)了。有時我在調試時發(fā)現點擊按鈕沒反應第一反應是去看 onClicked 監(jiān)聽器結果發(fā)現是上次忘了刪default_popup。兩個機制是互斥的只能選一個。commands字段用來聲明快捷鍵它不屬于action但經常配合action使用。例如commands: { toggle-feature: { suggested_key: { default: CtrlShiftY, mac: CommandShiftY }, description: 開關某項功能 } }當你在 manifest 里聲明了commands需要在一個頁面里調用chrome.commands.onCommand.addListener來監(jiān)聽觸發(fā)并執(zhí)行對邏輯。系統(tǒng)快捷鍵有保留鍵位比如 CtrlShift某些特殊鍵如果沖突Chrome 會在chrome://extensions/shortcuts頁面顯示為可手動修改所以測試時如果沒有生效先去看看是不是被其他軟件或插件搶占了快捷鍵。4.4 options_page 與 options_ui設置頁的兩種形態(tài)設置頁面有兩種聲明方式。options_page是老的寫法設置頁會作為獨立的標簽頁打開相當于一個完整的網頁這個頁面里可以自由使用chrome.*API。options_ui是新寫法可以配合open_in_tab: false讓設置頁在一個嵌入式面板中打開。兩者都只能出現一個同時寫進 manifest 的話 Chrome 會優(yōu)先使用options_ui的設置。options_ui: { page: options.html, open_in_tab: true }如果用嵌入式設置頁最好不要在options.html里使用window.close()這類操作因為頁面不是在獨立標簽頁中打開關閉邏輯可能不受控。另外有些插件希望讓用戶右鍵圖標菜單里直接進設置頁這個需要你在chrome.runtime.openOptionsPage()里手動觸發(fā)即便沒有寫options_ui這個函數依然可用因為 Chrome 會自動兜底。5. 權限和資源聲明MV3 最容易審核被拒的區(qū)域5.1 permissions 和 host_permissions怎么寫才既不報錯也不過度索權permissions里的 API 權限非常多開發(fā)中最常用的大概是這些storage使用chrome.storage.local或chrome.storage.session存取數據scripting使用chrome.scripting.executeScript/insertCSS動態(tài)注入腳本activeTab用戶主動交互時獲得當前標簽頁的臨時訪問權限tabs讀取標簽頁的 url、title 等敏感信息不需要注入腳本時常常沒必要申請alarms使用定時器clipboardRead/clipboardWrite讀寫剪貼板downloads主動觸發(fā)下載notifications桌面通知別把permissions當成許愿池凡是你能想到的 API 全往上堆。Chrome Web Store 審核時會評估權限是否合理申請了一堆用不到的權限會被打回。而且權限多用戶在安裝時看到的警告信息也多很影響轉化率。我見過有些插件只是做個網頁高亮標注卻申請了tabs和all_urls這些權限完全可以通過activeTab按需獲取。host_permissions則用來聲明腳本能運行在哪些站點上。注意如果你同時在content_scripts里寫了matches那么host_permissions的作用不是重復聲明匹配規(guī)則而是讓你的插件在后臺 Service Worker 和彈窗頁面里也能進行跨域請求、使用chrome.tabs.query讀取這些站點的標簽數據等。也就是說content_scripts 匹配規(guī)則和 host_permissions 是兩套體系后者決定插件自身的代碼能訪問哪些網絡資源前者決定注入到頁面里的腳本能出現在哪些頁面。一個比較常見的組合是使用activeTabscripting來代替all_urls的權限申請。這樣用戶點擊插件圖標時你的腳本才被注入到當前頁面不需要提前聲明所有域名。這個方案在權限審查上極其友好缺點是用戶第一次使用時不夠自動需要多點一下圖標。5.2 web_accessible_resources語義從可訪問資源變成特定頁面可用資源MV2 的web_accessible_resources就是一個簡單的資源路徑數組聲明了之后任何網站上的腳本都可以通過chrome.runtime.getURL拿到這些資源的地址并訪問。這在當時引發(fā)了很多濫用比如惡意網站可以通過訪問你的插件資源來探測你是否安裝了某個插件。MV3 把整個機制改了變成對象數組每個對象里必須包含resources和matches表示這些資源只允許在這些匹配的頁面里被訪問。web_accessible_resources: [ { resources: [injected.js, images/*.png], matches: [https://*.example.com/*] } ]這個字段通常和content_scripts中注入的腳本配合使用你的內容腳本想要動態(tài)加載插件里的某個資源就得把該資源聲明為 web accessible否則頁面端的 JavaScript 無法讀取。還有一個新屬性use_dynamic_url把它設為true時Chrome 每次啟動插件會生成一個隨機的資源路徑前綴可以防止網站固定路徑來探測插件。這個屬性特別適合做隱私保護類插件值得用起來。5.3 content_security_policyMV3 里它基本沒有發(fā)揮空間正如 2.3 節(jié)說的MV3 下content_security_policy已經變成content_security_policy: { extension_pages: script-src self; object-src self;, sandbox: sandbox allow-scripts; script-src self unsafe-eval }如果你沒有特殊的沙盒需求可以完全不聲明extension_pagesChrome 會使用默認策略。一旦你自己聲明了就只能比默認策略更嚴格不能更寬松所以沒必要去填一個和默認一樣的值。如果你真的需要eval或者new Function來動態(tài)執(zhí)行代碼那就得把相關頁面聲明為sandbox。比如你在插件目錄下建了一個sandbox.html然后在sandbox里給它指定 CSP就可以在這個頁面里自由執(zhí)行eval但這個頁面不能直接調用chrome.*API只能通過postMessage和父頁面通信。我在做模板渲染工具時就是這么干的渲染邏輯放在沙盒頁面數據通過消息傳給父頁面對于合法用途來說這個是可行的但絕大多數情況你應該用正常的函數引用或Function.prototype的安全替代方案避免引入代碼注入風險。5.4 optional_permissions 與 optional_host_permissions把權限申請延后到運行時這兩個字段在 MV3 里同樣存在作用是聲明插件可能需要的權限但安裝時不會提示等到用戶實際操作到對應功能時通過chrome.permissions.request接口彈窗申請。這種模式可以顯著降低首次安裝的心理門檻。optional_permissions: [downloads], optional_host_permissions: [https://api.example.com/*]要注意optional_permissions里的權限不能和permissions里的重復用戶一旦授予了可選權限之后chrome.permissions.remove可以再取消。如果你的插件核心功能需要某個權限盡量不要把它放到 optional 里否則用戶拒絕授權你的主流程就跑不通了。合理用法是核心權限在安裝時聲明周邊功能權限比如導出 PDF要用到downloads可以推遲到用戶點擊導出按鈕時再申請。6. 那些容易被忽略的邊緣字段與綜合配置示例6.1 minimum_chrome_version、incognito、externally_connectableminimum_chrome_version指定你的插件最低能支持的 Chrome 版本。MV3 本身要求 Chrome 88 及以上但如果你用到了更新的 API比如chrome.scripting中較新的方法就要把最低版本調高。這個字段常常被忽略導致部分老版本瀏覽器用戶反饋插件裝上了但功能不生效我在自己插件里就把minimum_chrome_version設成了109因為這個版本之后 MV3 的穩(wěn)定性才真正到位。incognito字段控制插件在無痕模式下的表現取值有spanning默認在有痕和無痕模式間共享、split每個模式單獨運行一個后臺、not_allowed無痕模式下完全禁用。如果你的插件需要緩存一些用戶數據最好明確設為split避免隱私數據在無痕和有痕之間串臺。externally_connectable字段用于聲明哪些外部擴展或網站可以通過chrome.runtime.connect或chrome.runtime.sendMessage給你的插件發(fā)消息。配置時可以用ids指定擴展 ID也可以用matches指定網頁域名。這個字段安全相關建議收斂到最小范圍不寫的話默認為不允許任何外部來源通信。6.2 一份可直接復制修改的最小完整 Manifest.json把前面講的字段綜合到一起給出一個我目前線上插件實際在用的配置骨架{ manifest_version: 3, name: 我的網頁助手指南, version: 1.2.0, description: 一個用于網頁標注與數據提取的示例插件, minimum_chrome_version: 109, icons: { 16: icons/icon16.png, 32: icons/icon32.png, 48: icons/icon48.png, 128: icons/icon128.png }, action: { default_popup: popup/popup.html, default_title: 打開助手 }, background: { service_worker: background/service-worker.js, type: module }, content_scripts: [ { matches: [https://*/*, http://*/*], js: [content/content.js], run_at: document_idle, world: ISOLATED } ], permissions: [storage, scripting, activeTab], host_permissions: [https://*/, http://*/], web_accessible_resources: [ { resources: [injected/injected.js], matches: [https://*/*, http://*/*], use_dynamic_url: true } ], options_ui: { page: options/options.html, open_in_tab: false }, commands: { toggle-annotation: { suggested_key: { default: AltShiftA }, description: 切換標注模式 } } }這份配置覆蓋了絕大多數插件的核心需求。你要根據自己的功能刪減不需要它就用不到不要照抄。6.3 加載與排錯流程從本地加載到控制臺報錯定位寫完 Manifest.json 后打開chrome://extensions開啟右上角的開發(fā)者模式點擊加載已解壓的擴展程序選擇插件目錄。如果 Manifest.json 有 JSON 語法錯誤或字段校驗不通過這里會直接給出報錯信息具體到哪個字段有問題照著改就行。如果提示清單文件缺失或不可讀先檢查文件編碼不要用帶 BOM 的 UTF-8Chrome 對 BOM 的容忍度比較低。加載成功后去擴展詳情頁點擊查看錯誤按鈕能看到 Service Worker 和各個頁面的報錯日志。我在調試時遇到過一個詭異現象插件明明加載成功了但一點擊圖標就縮回去沒任何反應。打開錯誤面板才發(fā)現是popup.html里引用的一個本地 JS 文件因為路徑寫錯 404 了而彈窗頁面一有 JS 錯誤就會自動關閉表現的就像點了沒反應。這類問題排查時錯誤信息面板比什么都好使。7. 我實際踩過的坑和最終的排錯結論說實話Manifest.json 的字段本身不算復雜真正麻煩的是 MV3 整體架構改變帶來的連鎖反應。有幾個坑我特別想單獨拎出來說因為它們在官方文檔里不會寫但幾乎每個遷移者都會遇到。第一個坑是content_scripts里用了include_globs后發(fā)現腳本就是不在預期頁面上運行。include_globs和exclude_globs是匹配模式的補充規(guī)則格式和 glob 相似比如*://*.example.com/*。但它們和matches的交集邏輯比較繞只有同時滿足 matches 和 globs 規(guī)則才會注入。如果你習慣了只寫 matches突然加了 globs反而會縮小注入范圍。我的建議是能用 matches 解決的絕不寫 globs否則調試時很容易懷疑人生。第二個坑是 MV3 下沒法在插件內部使用XMLHttpRequest訪問普通 HTTP 接口必須使用fetch。這個不是 Manifest.json 字段的問題但很多人把報錯Service worker cannot use XMLHttpRequest當成 manifest 配置錯誤翻遍配置文件也找不到答案。實際上這是 Service Worker 環(huán)境的限制改用fetch即可。第三個坑最隱蔽某些 API 在 MV3 的permissions里寫錯了也不報錯功能卻靜默失敗。比如我用chrome.storage.sync時忘了在 permissions 里聲明storage結果是同步功能完全不生效但瀏覽器控制臺里不打印任何錯誤只有在你調用chrome.runtime.lastError檢查時才會看到。所以寫完 manifest 后建議逐個功能點走一遍并且通過chrome.runtime.lastError檢查異步調用是否真的成功。最后一個排錯結論如果插件在chrome://extensions頁面加載時報Extension must be signed in to use this API或者一些莫名其妙的錯誤優(yōu)先看你是不是同時開了 Chrome 的多用戶配置、或者用了企業(yè)策略限制了擴展權限。這些情況下的報錯信息往往指向 manifest 字段但根源其實在瀏覽器運行環(huán)境別在上面空耗時間。Manifest V3 的遷移陣痛是真實的但理解它背后的安全優(yōu)先、資源友好思路之后你會發(fā)現每個字段限制都有它存在的理由。上面這些內容是我自己在遷移兩個生產插件過程中邊看文檔邊試錯總結出來的希望你能避開我走過的彎路花更少的時間把 Manifest.json 一次寫對。