多上下文打包与热重载
本教程共 56 篇 · 第 56 篇 · 更新于 2026-08-13 · 约 6 分钟阅读
本节目标:学完你理解 crxjs 怎样为后台、弹出页、内容脚本分别打包并自动产出 manifest,也知道 dev 模式的热重载怎么帮你省时间。
前两章把环境、框架、TS 都铺好了。这一章讲构建工具真正的核心价值:把多个上下文分别打成独立文件,并自动维护那份最易出错的 manifest.json。最后看热重载怎么把改代码的等待降到最低。
56-1 什么是多上下文
扩展不是「一个程序」,而是好几个运行环境拼起来的。它们彼此隔离,加载时机也不同:
- 后台服务工作者:平时休眠,事件驱动。
- 弹出页 / 选项页:用户点开时才跑的普通网页。
- 内容脚本:注入到网页里,和页面共享 DOM。
构建工具要做的,是给每个上下文产出「互不干扰」的包。弹出页用到的 Vue 运行时,不该混进后台脚本;内容脚本的体积,也得单独控制。
Note「上下文」在这里指运行环境,不是代码文件。一个上下文可以对应一个入口文件,也可以对应一组被它依赖的文件。
56-2 manifest 由插件生成
手写 manifest.json 时,路径要手动对。一旦文件被打包重命名,清单就得跟着改,极易错位。
crxjs 把这件事接管了。你写的是一份带开发路径的 manifest.config.ts:
import type { ManifestV3Export } from "@crxjs/vite-plugin"
const manifest: ManifestV3Export = {
manifest_version: 3,
name: "多上下文示例",
version: "0.0.1",
background: {
service_worker: "src/background/index.ts",
type: "module",
},
action: { default_popup: "src/ui/popup/index.html" },
content_scripts: [
{ js: ["src/content-script/index.ts"], matches: ["<all_urls>"] },
],
}
export default manifest
构建时插件会读这份声明,把 src/background/index.ts 编译成 assets/background-[hash].js,再自动写进最终 manifest.json 的对应字段。你全程不用手碰产物路径。
Tip这就是为什么清单要写成
.ts再export default——插件需要在构建图里追踪每个入口,纯 JSON 做不到这点。
56-3 后台单独打成模块
后台服务工作者在 MV3 里是 type: "module" 的 ES 模块。crxjs 会按模块方式打包它,支持 import 拆分代码。
// src/background/index.ts
import { setupAlarms } from "./alarms"
chrome.runtime.onInstalled.addListener(() => {
setupAlarms()
})
打包后,alarms.ts 的逻辑会被合并进后台产物(或按需拆分),但对外始终是一个 service_worker 文件,清单里写一份就行。
Note后台要保持轻。别在这里引大型 UI 框架,服务工作者讲究快启动、快休眠,重依赖会拖累唤醒速度。
56-4 弹出页与选项页各自成包
页面类入口(popup、options、side-panel)都在 rollupOptions.input 里列出,每个生成独立的 HTML 加 JS/CSS 资源。
export default defineConfig({
plugins: [crx({ manifest })],
build: {
rollupOptions: {
input: {
popup: "src/ui/popup/index.html",
options: "src/ui/options/index.html",
},
},
},
})
这样弹出页用到的框架代码,只打进弹出页的包;选项页用到的,只进选项页的包。彼此不会互相拖累体积。
Tip页面入口务必列全。漏写一个,构建就不会生成它,加载扩展时那个页就会 404。
页面入口用 index.html 做壳,里面挂载框架根节点。crxjs 会保证每个页面的资源互不串门,弹出页的 JS 不会漏进选项页。这种隔离对调试很有利:一个页报错,不影响另一个页正常运行。
56-5 内容脚本的特别处理
内容脚本最特殊:它要注入到真实网页,受 CSP 和隔离环境约束。crxjs 对它有专门处理,比如把 CSS 单独抽出来按要求注入。
manifest 里照常声明即可:
content_scripts: [
{
js: ["src/content-script/index.ts"],
css: ["src/content-script/style.css"],
matches: ["<all_urls>"],
run_at: "document_end",
},
]
插件会保证 js 和 css 分别按内容脚本的规则注入,不会和弹出页的打包逻辑混在一起。
Note内容脚本和页面之间不能随便共享变量,这点在模块四讲过。构建工具不改变这个隔离规则,它只负责把脚本正确地打成可注入的形式。
56-6 热重载怎么提速
没有热重载时,改一行代码就要:切回扩展页 → 点刷新 → 重新打开弹出页。反复几十次,半天没了。
dev 模式下 crxjs 接了 Vite 的 HMR(热模块替换)。原理是:构建工具盯着 src 文件,你一保存,它就通过本地服务把变更推给已加载的扩展,浏览器侧自动应用。
npm run dev
跑起来后,改弹出页组件,弹出页几乎瞬时更新;改后台脚本,服务工作者会被重新加载。你不用在 chrome://extensions 里手动点刷新了。
Tip热重载对「页面类」上下文最灵敏(popup、options)。后台服务工作者变更通常触发整段重新加载,稍慢但依旧自动。首次加载扩展仍需你手动点一次。
56-7 开发与生产两条构建
同一个配置,靠环境变量区分开发和生产。常见做法是脚本里切 NODE_ENV:
{
"scripts": {
"dev": "cross-env NODE_ENV=development vite",
"build": "cross-env NODE_ENV=production vite build"
}
}
开发态:开 sourcemap、开 watch、产物落 dist/chrome 供加载。生产态:关 sourcemap、压缩代码、可顺带打 zip 包上架。
const IS_DEV = process.env.NODE_ENV === "development"
export default defineConfig({
build: {
sourcemap: IS_DEV ? "inline" : false,
watch: IS_DEV ? {} : undefined,
},
})
Note
sourcemap让你在开发者工具里看到「源码位置」而非打包后的乱码,调 bug 必备。生产构建关掉它,既能减小体积也能少暴露源码结构。
56-8 产物怎么自查
构建完别急着上架,先本地加载验证一遍。打开 chrome://extensions,开发者模式打开,点「加载已解压的扩展程序」,选 dist/chrome。
重点看三处:弹出页能开、后台服务工作者在「服务工作线程」里出现、内容脚本在目标网页注入了。三处都正常,说明多上下文打包没漏入口。
Tip上架前用
web-ext lint这类工具扫一遍清单,能提前抓出权限冗余、字段拼写等低级问题,比被商店审核打回再改省事。
56-9 几个常踩的坑
第一,改了 manifest.config.ts 却没生效。清单变更有时需要完全重新加载扩展,光热重载不够,去 chrome://extensions 点一下刷新图标。
第二,dist/chrome 和 src 加载错目录。要注意:永远加载 dist 产物,不是 src 源码。
第三,多个浏览器配置冲突。若你有 vite.chrome.config.ts 和 vite.firefox.config.ts,dev 时要指定用哪个,别让两份产物互相覆盖。
Tip卡住时,删掉
dist/整个目录重新dev一次,能排除大半「旧产物没更新」的假象。
56-10 小结
多上下文打包的本质,是「按运行环境分别产出、互不污染」。crxjs 用一份 manifest.config.ts 把入口、清单、路径全部串起来,你在开发态只管写 src。
热重载把「改完等刷新」变成「改完即更新」,是开发体验上最大的提速点。到这里,模块十三的工具链三章就讲完了:环境、框架与 TS、打包与热重载。你已经具备用现代工具开发 MV3 扩展的完整认知。