资讯动态

MV3与端侧AI实战:浏览器插件架构演进与工程化落地指南

发布时间:2026/9/15 3:02:45 来源:尧图企业网站定制
如果你对浏览器插件的印象还停留在“往页面里塞一个按钮、改几个 DOM 就算完事”那这两年你可能已经有点跟不上节奏了。我最近在做一个需要同时处理内容摘要、跨页面状态同步和本地端侧 AI 推理的插件项目时明显感觉到现代浏览器插件的工程复杂度已经逼近甚至超过了普通前端应用。这不是夸张——MV3Manifest V3全面落地之后插件在架构上被迫从“一个常驻的 Background Page 随处执行的脚本”变成了“事件驱动的 Service Worker 严格隔离的页面上下文 有边界的进程通信”再加上端侧 AI 这类重计算任务也被塞进浏览器整个开发模式完全变了。这篇文章不聊那些“教你写第一个 Hello World 插件”的入门内容而是把我在 MV3 工程化实战里踩过、填过的坑以及一套能跑通的架构设计思路完整拆开。内容包括三块MV3 到底改了什么、插件内部多上下文之间的跨进程通信怎么设计、端侧 AI 怎么在插件里落地最后给出一份可以直接照搬的工程化方案和避坑清单。适合已经写过或维护过插件、正准备往 MV3 迁移、以及想在插件里加本地模型能力的开发者。1. MV3 不是一次小版本升级插件的运行模型彻底变了1.1 从 Background Page 到 Service Worker一次“去常驻化”的架构革命MV2 时代写插件最舒服的地方在于你在background.js里可以维持一个常驻的全局环境声明一堆全局变量连接 WebSocket甚至开个定时器跑长任务。整个 Background Page 就是一个隐藏的浏览器标签页浏览器为它分配完整的渲染进程页面生命周期基本不受限制。MV3 把这个根基直接抽掉了。Chrome 从 88 版本开始推行 MV3到 2023 年之后逐步强制禁用了 MV2 扩展Firefox 也在跟进。MV3 要求后台逻辑全部跑在 Extension Service Worker 里。Service Worker 是事件驱动模型它没有页面 DOM不能访问window和document不能用XMLHttpRequest得换fetch更不能长期驻留——空闲一段时间就会被浏览器回收下次有事件进来再唤醒。这个变化对架构的影响是根本性的。以前你可以把“全局状态”理所当然地放在内存里现在不行了。我见过不少团队迁移 MV3 后遇到同一个诡异问题插件跑着跑着某些状态就丢了表现是“时好时坏”。原因基本都指向同一个——Service Worker 被回收内存里的状态全没了。MV2 和 MV3 的核心差异可以整理成一张表维度MV2MV3后台运行模型常驻 Background Page事件驱动、可回收的 Service Worker远程代码允许托管远程脚本禁止所有代码必须打包进扩展网络请求拦截webRequest 阻塞式可修改响应体declarativeNetRequest 规则引擎无法直接读改响应体页面脚本注入tabs.executeScript 任意注入chrome.scripting.executeScript且区分 ISOLATED/MAIN 世界跨域请求声明权限后用 XHR声明权限后用 fetch存储local/sync 两种新增 session 级存储适合临时状态后台页面 DOM可以直接操作 DOM无 DOM需要操作 DOM 得借助 offscreen document表格里每一行背后都有实际影响后面逐个展开。1.2 被彻底关掉的两扇门远程代码与阻塞式网络拦截MV3 最被开发者吐槽的两件事一是不能用远程代码二是 webRequest 的阻塞式拦截被砍。先说远程代码。MV2 时期你可以在 manifest 里配置content_security_policy允许扩展从你的服务器加载一段 JavaScript 动态执行这在 A/B 实验、动态下发规则这类场景里特别方便。MV3 把这条路堵死了扩展的 CSP 默认是script-src self所有 JS 必须打包在扩展本地不能用eval()、不能用new Function()也不能加载外部脚本。这个限制的直接影响是所有“规则类”“配置类”逻辑必须静态打包或者以纯数据的形式下发JSON 之类的数据仍然可以用。我见过一个 MV2 插件靠远程脚本做灰度开关迁移到 MV3 后不得不把策略改成了“本地内置规则 远端拉取 JSON 配置”灵活性降了一档但换来了更严格的安全边界。Google 这么做的理由说白了就是安全远程代码等于给恶意第三方留了一个随时可以执行任意代码的后门这在 Chrome 扩展商店的审核体系下是不可接受的。再说 webRequest。MV2 的webRequestAPI 允许你阻塞请求、修改请求头、甚至修改响应体网络代理类插件全靠它。MV3 用declarativeNetRequest取而代之你不能再写 JavaScript 去“处理”每一个请求而是声明一组规则让浏览器引擎去匹配和修改。静态规则集上限大约 30000 条动态规则大概 5000 条正则规则更少1000 条。这意味着什么意味着任何依赖“逐个请求动态判断”的逻辑都做不了了。比如你想根据响应内容动态决定要不要拦截MV3 里没有直接的 API 支持除非用chrome.webRequest的观察模式配合其他机制间接实现。做广告过滤类插件的团队对此感受最深很多复杂的自定义过滤规则只能改写。1.3 迁移 MV3 时最常见的三个误解第一个误解把 MV3 当成“换个 background 声明方式”。很多人以为把background字段从page改成service_worker就完事了。实际上代码里所有依赖全局变量、DOM 操作、定时器的逻辑都要重新设计。你需要的不是改声明而是重写后台架构。第二个误解webRequest 只是“换了个 API”。实际上不是换 API是换模型。你不能再写阻塞逻辑只能“声明规则”。如果你的核心功能依赖响应体分析MV3 里没有直接方案得用 offscreen document 自己发请求来绕或者干脆换一种产品形态。第三个误解Service Worker 和页面里的 Web Worker 是一回事。不是。扩展 Service Worker 的生命周期由浏览器统一调度除了 30 秒/5 分钟的空闲回收策略还有事件驱动的唤醒机制。它能用的 API 是扩展 API 网络 API 的子集和标准 Web Worker 有明显区别。最简单的一个例子你在 SW 里不能用alert()不能用localStorage用chrome.storage.session才能实现类似的临时状态效果。理解这三点后面聊通信和 AI 落地方案才有共同语言。2. 插件内部其实是个“分布式系统”跨上下文通信的完整链路2.1 一个插件里到底有几个运行上下文很多插件新手搞不清楚的一个核心问题是自己写的代码“活”在哪里。MV3 插件里代码分散在多个完全隔离的上下文里它们各自有独立的全局对象、独立的生命周期、独立的调试入口。我一般把它们分成这么几类上下文作用生命周期能访问的页面 DOMExtension Service Worker后台逻辑、事件监听、状态管理事件驱动空闲回收不能Content Script注入到网页里的脚本读取/修改页面 DOM与页面生命周期一致能ISOLATED 世界Popup / Options / Side Panel插件的 UI 页面打开时创建关闭即销毁不能直接操作网页Offscreen Document无 UI 场景下处理 DOM/音频/视频等任务按需创建用完关闭不能直接操作网页这个表格看起来简单但它是所有通信问题的基础。Content Script 和后台 Service Worker 虽然同属于一个扩展但它们之间的”进程”隔离是全方位的唯一通讯方式就是消息传递。2.2 三类最常用的消息通道与代码写法先看第一类扩展页面到 Service Worker 的通信。Popup、Side Panel 或 Options 页面里调用chrome.runtime.sendMessage()后台用chrome.runtime.onMessage.addListener()接收。这里有个非常容易踩的坑监听器里如果要执行异步操作必须return true否则 sendResponse 会在异步任务完成前就被回收调用方拿到的是 undefined。// popup 里发送 const result await chrome.runtime.sendMessage({ type: SUMMARIZE, tabId: tab.id });// service worker 里接收 chrome.runtime.onMessage.addListener((message, sender, sendResponse) { if (message.type SUMMARIZE) { // 异步任务必须 return true 保活 sendResponse (async () { const summary await doSummarize(message.tabId); sendResponse({ ok: true, summary }); })(); return true; } });第二类Content Script 到后台的消息。Content Script 只能访问页面的部分 API但它调chrome.runtime.sendMessage()时也会发到同一个扩展的 Service Worker。区别在于sender参数里带了 tab 信息你可以在后台知道消息来自哪个标签页。反过来后台要主动给某个 Content Script 发消息时用chrome.tabs.sendMessage(tabId, message)注意要指定 tabId。第三类长连接。如果消息非常频繁比如要持续推送进度用chrome.runtime.connect()建立一个长连接通过Port对象来传递消息。Port 的好处是双向通道不用每次发消息都走一遍事件注册。坏处是 Service Worker 被回收时 Port 会断开需要监听onDisconnect事件做重连。// 建立长连接 const port chrome.runtime.connect({ name: ai-task }); port.postMessage({ type: START }); port.onMessage.addListener((msg) { // 收到推理进度 });2.3 Service Worker 被回收后消息丢了怎么兜底跨上下文通信最让人头疼的问题不是“怎么发消息”而是“消息发过去了没人接”。Service Worker 被回收后有些消息会直接石沉大海。比如你从一个 UI 页面发送一个耗时操作指令如果 SW 此时正处于回收状态消息事件会唤醒它但如果你在代码里依赖了某个“全局状态”比如之前缓存的对象大概率会报错。我的兜底方案一般分三层第一临时状态不要放内存放chrome.storage.session。这是 MV3 专门设计的会话级存储SW 醒来后可以直接恢复状态。它不持久化到磁盘SW 存活期间有效正好弥补内存不稳定的问题。第二对关键任务做“唤醒确认”。发送方发出指令后等待接收方回一个RECEIVED确认消息如果几秒内没收到就重新发送。这个机制在实战中非常有用能避免大量“任务悄悄丢失”的难排查问题。第三如果任务真的很长比如一次推理要几十秒不要依赖 SW 的连续性把任务分解成多个小步骤每完成一步就写一次 session storage下次醒来从断点继续。这个思路和做松耦合设计是一个道理。2.4 跨出浏览器边界native messaging 与本地程序通信当插件需要和本地应用打交道——读写本地文件、调用系统硬件、跟桌面客户端联动——就得请出 native messaging。这个机制的工作原理是浏览器通过标准输入输出和一个本地可执行程序通信通信格式是 JSON 消息加长度前缀。配置分两步。第一步在扩展里调用chrome.runtime.connectNative(com.example.myhost)这里的字符串是本地 host 的名称。第二步在系统里安装一个 host manifest 文件。这个文件是 JSON 格式核心字段是name必须和扩展调用的一致、path本地可执行文件的绝对路径、typestdio、allowed_origins必须包含你的插件 ID格式是chrome-extension://你的ID/。在 Linux 上这个 manifest 放在~/.config/google-chrome/NativeMessagingHosts/目录macOS 在~/Library/Application Support/Google/Chrome/NativeMessagingHosts/Windows 则需要在注册表里注册。装完 manifest还要注意本地程序的stdout输出格式前 4 字节是小端整数表示的消息长度后面跟上 JSON 字符串。这套机制在工程上很实用。我就用它在插件里接了一个本地小服务负责处理插件跑不动的重计算任务比如大模型的量化加载数据和结果走 native messaging 通道。有一个坑值得提醒host manifest 里的插件 ID 是固定的一旦你在开发模式下加载了未打包扩展ID 会变化这在调试 native messaging 时最容易让人抓狂。3. 端侧 AI 进入插件的正确姿势3.1 为什么要费力在本地跑模型而不是调云 API先回答一个问题插件里跑端侧 AI到底是炫技还是刚需我的判断是在不少场景里它是刚需。第一个理由是隐私。插件经常要读取用户正在浏览的网页内容这些数据送到云端 API 会产生严重的数据合规问题。很多企业场景里用户打开的可能就是合同、审计报告这类敏感文档数据不能出设备。第二个理由是延迟和成本。一次云端大模型调用的往返延迟动辄一两秒还有 token 费用。端侧推理只要模型能跑第一次加载之后单次推理往往比云端更快而且边际成本为零。第三个理由其实最实际浏览器插件是一种特别适合“本地 AI”的载体。它天然长在用户设备上可以访问当前页面上下文有 storage 可以做模型缓存有 side panel 可以做持续交互界面。用户想要的是一个可离线、低延迟的智能助手端侧 AI 是逻辑上最顺的路线。3.2 四条端侧推理路线怎么选我实测过四条路线各有各的适用场景。方案底层技术优势劣势Transformers.js (huggingface/transformers)ONNX Runtime Web可走 WASM/WebGPU生态最丰富HuggingFace 上大批开源模型直接可用API 接近 Python transformers大模型跑不动适合 1B 以下小模型WebLLMWebGPU LLM 推理引擎能跑 1B~14B 的对话模型质量高需要 WebGPU老设备不支持首次加载模型文件大GB 级MediaPipe TasksWASM/WebGPUGoogle 普惠方案文本分类、embedding、翻译等小任务极快偏传统 NLP 任务不适合复杂生成本地代理 native messaging在本地起 Python/其他推理服务可以跑任意模型不受浏览器限制部署成本高用户体验依赖安装过程做选型时我建议遵循最简单够用原则如果只是做文本分类、摘要、命名实体识别Transformers.js 就够了几百 MB 的模型量化后可以被接受如果想做翻译WebLLM 或者本地代理更靠谱如果是中等规模对话模型WebLLM 是当前浏览器内唯一能打的。这里有个容易被忽略的兼容性问题WebGPU 在扩展 Service Worker 环境里是不可靠的很多 API 在 worker 里不可用或者行为不稳定。所以端侧 AI 推理不要放在 SW 里跑一定要放在一个可见或隐藏的页面上下文里比如 side panel 或 offscreen documentSW 只负责接收结果和分发消息。3.3 一个页面摘要助手的实现骨架用一个实际例子来说明端侧 AI 在插件里的完整链路用户点一下按钮对当前网页自动生成摘要。第一步在 side panel 页面初始化模型。用huggingface/transformers代码其实很简单import { pipeline } from huggingface/transformers; // 只在面板打开时一次性加载 const summarizer await pipeline( summarization, Xenova/distilbart-cnn-6-6, { dtype: q8 } );第二步从当前标签页抓取正文。这里要注意Content Script 拿到的 DOM 不能直接塞给模型需要做提取和清洗。我会先用document.querySelector(article)之类的选择器尽量提取正文再把超过模型输入长度限制的文本做分块chunk。第三步把分好的文本发送到 side panel 或者 offscreen document 做推理。消息链路的典型走向是Content Script 抓到文本 →runtime.sendMessage发给 SW → SW 决定把推理任务派发给 side panel 页面 → 结果原路返回。整个过程里SW 扮演的是路由角色不是计算角色。第四步把摘要结果渲染到 side panel同时存一份到chrome.storage.session避免用户切走再回来时结果丢失。实测下来int8 量化后的 distilbart 模型在一台普通笔记本上用 WebAssembly 跑一篇 500 字新闻摘要大约是 3 到 5 秒如果换成 WebGPU能压到 1 秒左右。这个体感是可以接受的。3.4 让模型启动不卡死插件的资源调度细节端侧 AI 在插件里最大的工程挑战不是“能不能跑”而是“别把插件拖死”。模型的加载和推理动辄占用几百 MB 内存处理不好用户会直接骂娘。我的经验是三个字懒加载。模型千万不要在插件启动时加载也不要跟着 popup 加载。正确做法是把推理页面side panel 或 offscreen当作一个“按需启动的推理服务”用户真正触发功能时才初始化。初始化时可以显示一个进度条这比用户毫无反馈地等要好得多。第二个细节是模型缓存。浏览器有 Cache API 和 IndexedDB首次下载的模型文件会进入浏览器缓存第二次加载时直接从本地读不需要联网。但要注意Cache API 在扩展环境里访问模型文件时可能涉及到跨域问题稳妥做法是把小模型直接打包进扩展几百 MB 的大模型才考虑运行时下载。第三个细节是降级策略。要主动检测navigator.gpu是否可用不可用时自动降级到 WASM 推理检测设备内存navigator.deviceMemory来决定加载多大的模型。比如 4GB 内存以下设备只加载 200MB 以内的小模型8GB 以上才尝试 700MB 的大模型。第四个细节最容易忽略observability。推理任务要记录耗时、内存峰值、模型加载状态这些统计信息用performance.mark和performance.measure记录后上报。否则模型升级或者参数调整时你根本不知道是变好了还是变差了。4. 工程化其实比功能更难构建、调试与发布4.1 别再手写 manifest 了现代脚手架怎么选MV3 的配置项变多之后手写 manifest 已经成为团队协作里的灾难。不同浏览器对 MV3 的兼容性还有细微差异Chrome 和 Firefox 对一些字段的接受程度不完全一致自己维护一套跨浏览器构建逻辑很容易出错。我对比过三个主流脚手架WXT、CRXJS Vite Plugin、Plasmo。脚手架底层核心优势适合场景WXTVite自动生成多浏览器 manifest类型安全热更新完善想用 Vue/React希望一套代码多浏览器发布CRXJSVite轻量只做 Vite 的插件增强已有 Vite 项目想快速改造Plasmo自研构建起步早文档全社区生态好React 技术栈需要快速原型我的选择是 WXT。理由是它把 manifest 的差异封装掉了你可以在wxt.config.ts里用一个配置函数针对不同浏览器做差异化配置构建时自动生成对应浏览器的 manifest。它能给我带来自动补全的类型定义还有相当好用的开发模式热更新。4.2 一次完整的项目结构与构建配置一个 WXT 项目的基本结构大致如下my-extension/ src/ entrypoints/ background.ts content-script.ts popup/ index.html main.tsx sidepanel/ index.html main.tsx lib/ models.ts messaging.ts assets/ wxt.config.ts package.json配置文件里权限声明是 MV3 项目里最需要小心的地方。权限越少越好少一个权限就意味着少一份审核风险也少一个攻击面。// wxt.config.ts import { defineConfig } from wxt; export default defineConfig({ manifest: { name: AI Page Assistant, description: 端侧 AI 驱动的页面摘要助手, version: 1.0.0, permissions: [storage, scripting, sidePanel], host_permissions: [all_urls], action: { default_popup: popup/index.html, }, }, });这里有个经验host_permissions不要一上来就写all_urls。很多功能实际上只需要当前活动标签页的访问权限可以用activeTab权限配合用户手势来临时获得。只有在确实需要跨域请求数据时才申请具体的域名权限。权限申请得越多上架审核时的解释成本越高。4.3 调试体验决定开发效率MV3 的调试体验和普通前端项目有相当大的区别。最大的坑在于不同的上下文有不同的控制台。Service Worker 的控制台在chrome://extensions页面里点“Service Worker”链接打开Content Script 的日志要看目标网页的 DevToolspopup 和 side panel 的控制台分别在各自页面里按 F12。第一次接触的人经常找不到日志在哪。实际开发中我建议在代码里封装一个带上下文的日志工具每条日志自动带上来源标识。比如[SW]、[Content]、[Panel]这样多上下文一起跑时一眼就能看出消息走到哪一步了。另一个极其有用的调试工具是chrome.runtime.getURL()。在 Content Script 里如果要加载插件包内的静态资源图片、iframe 页面必须用它来生成 URL不能写相对路径。调试资源路径问题时十有八九是这个原因。WXT 的开发模式会自动监听源码变更并刷新扩展但有一个小坑Service Worker 的状态不会因为热更新而保留。所以调试时需要人工确认“SW 已重启”否则你以为自己在验证新代码其实跑的是旧逻辑。4.4 多浏览器上架构建到分发的一条龙流水线发布环节是很多独立开发者容易忽略的工程化部分。Chrome Web Store、Edge Add-ons、Firefox Add-ons 三个平台的审核标准和流程各不相同。我的经验是先把 CI 流程搭起来做到每打一个 tag 自动构建三种浏览器的 zip 包。用 GitHub Actions 的话流程大致是代码 push 带 tag → 安装依赖 → 运行wxt build -b chrome、wxt build -b firefox、wxt build -b edge→ 上传产物。这样你可以手动把打包好的 zip 分别传到各个商店。上架时几个反复被审核问到的点权限用途说明要写清楚尤其host_permissions如果覆盖大量域名必须解释为什么需要。端侧 AI 类插件虽然不做数据上传但如果涉及用户数据比如打开页面内容隐私政策里必须写明数据不会离开设备。Firefox 对 MV3 的支持晚于 Chromium部分 API 是后补的构建前要看一眼官方兼容性文档别在 Firefox 上踩空 API。我自己踩过一次 Chrome Web Store 审核拒绝理由是“权限申请范围超出功能需要”。当时插件申请了tabs权限实际上只是为了读取标签页标题而tabs权限会暴露完整的 URL 历史属于敏感权限。后来改用仅声明activeTab解决了。这个教训让我后来每次加权限都会先问一句这个权限是否可以更小。5. 踩坑清单这些坑不趟一遍根本不知道5.1 Service Worker 的“复活”不是无代价的前面提到 SW 会被回收、再唤醒但“唤醒”不等于“回到之前的状态”。我见过最经典的一个 bug插件在启动时初始化了一个比较大的对象后续所有功能都依赖这个对象。MV2 下没问题迁移到 MV3 后用户操作稍慢一点SW 就被回收了用户再点扩展图标SW 以全新状态启动之前的初始化根本不会自动执行于是所有依赖这个对象的功能全部报错。解决方式有两个层面。一是代码层面不要在模块顶层做有副作用的初始化把状态初始化封装成带有“是否已初始化”检查的函数每次调用前确保状态就绪。二是架构层面把关键状态持久化到chrome.storage.session或者 IndexedDBSW 重新启动后从持久化状态恢复。另一个和 SW 生命周期强相关的坑是定时器。setInterval在 SW 里不可靠SW 睡着之后定时器不触发等到事件唤醒 SW定时器回调才会补执行。所以长轮询或周期任务要么用chrome.alarmsAPIMV3 专门为此设计要么用消息事件驱动代替定时轮询。5.2 消息大小、序列化与类型丢失runtime.sendMessage的消息体是序列化传输的这里有两个硬性限制。第一单个消息的大小是有上限的官方文档里写的是约 64MB但实际传输超过 32MB 就可能出问题第二消息体不能包含函数、Promise、WeakMap这类不可序列化的对象也不能传Error对象序列化后变成一个普通对象message、stack信息丢失。我遇到过最头疼的一个问题Content Script 里把一个自定义类的实例通过消息发给 SW接收端拿到的是一个普通的 plain object原型链和类方法全丢了。后来统一改成发送可序列化的数据在接收端再重建类实例。另外sendResponse回调的异步陷阱也值得再强调一次。MV3 中只要监听器里用了async/await就必须return true来保持通道存活。很多诡异问题——调用方拿不到结果、结果偶尔返回偶尔不返回——都是这个原因。5.3 模型文件加载时的 CORS 和配额问题端侧 AI 的模型文件往往放在 Hugging Face 或自己的 CDN 上。在浏览器页面里用fetch加载模型文件会有跨域限制如果你没有配置对应的 CORS 头请求会直接失败。我的两个稳妥做法一是把模型文件下载到本地后放进插件包完全绕开 CORS二是如果必须用 CDN给all_urls或者对应域名配置host_permissions同时在远程服务器上正确配置Access-Control-Allow-Origin。但即使过了 CORS 这一关还有浏览器存储配额这个暗坑。IndexedDB 和 Cache API 的配额不是无限的在部分系统上扩展的存储配额比普通网站更宽松但也不是没有上限。模型文件动辄几百 MB一定要做加载失败和配额不足时的错误提示别让用户以为插件卡死了。说一个实际案例我把一个 400MB 的量化模型放到 Cache API 里在低配 Windows 机器上反复测试发现第二次安装插件时缓存可能被浏览器清掉导致模型要重新下载。后来我把“模型下载进度”和“下载完成状态”落到了chrome.storage.local配合 UI 上的下载进度条才把这个体验理顺。5.4 MV2 到 MV3 迁移的隐性破坏点清单最后给一份我在迁移多个插件后整理的“隐性破坏点”清单这些不是文档第一页会告诉你的但几乎每个项目都会撞上tabs.executeScript→chrome.scripting.executeScript注意回调变 Promise且需要scripting权限。XHR →fetchService Worker 里没有XMLHttpRequest对象。后台页的全局变量 → 全部失效改用chrome.storage.session。webRequest阻塞能力丢失 → 需要declarativeNetRequest时请先确认你的功能能否用规则集表达。window.close()→ Service Worker 里没有window关闭 side panel 用chrome.sidePanel.close()。远程配置脚本 → 变成 JSON 数据文件且 JSON 数据不受 CSP 影响可以运行时拉取。chrome.extension.getBackgroundPage()→ 不可用旧代码里跨上下文拿后台对象的方式全部作废。事件监听器里return true的异步语法 → 必须严格遵守否则消息被静默丢弃。每次迁移我都会拿这份清单过一遍代码省掉了大量反复排查的时间。在整个 MV3 端侧 AI 的实战过程中我个人体会最深的一点是别把所有逻辑都塞给 Service Worker。它更像一个乐队的指挥负责协调各声部而不是亲自演奏每一种乐器。重计算放页面级上下文状态管理放 session storage跨系统通信走 native messaging模型推理放 side panelSW 只负责路由和调度。按这个思路去做插件的复杂度和可维护性都会健康得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价