首页 / 浏览器扩展开发入门教程 / 多上下文打包与热重载

浏览器扩展开发入门教程

多上下文打包与热重载

本教程共 56 篇 · 第 56 篇 · 更新于 2026-08-13 · 约 6 分钟阅读

多上下文打包热重载HMRmanifest生成crxjs构建提速服务工作者

本节目标:学完你理解 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

这就是为什么清单要写成 .tsexport 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",
  },
]

插件会保证 jscss 分别按内容脚本的规则注入,不会和弹出页的打包逻辑混在一起。

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/chromesrc 加载错目录。要注意:永远加载 dist 产物,不是 src 源码。

第三,多个浏览器配置冲突。若你有 vite.chrome.config.tsvite.firefox.config.ts,dev 时要指定用哪个,别让两份产物互相覆盖。

Tip

卡住时,删掉 dist/ 整个目录重新 dev 一次,能排除大半「旧产物没更新」的假象。

56-10 小结

多上下文打包的本质,是「按运行环境分别产出、互不污染」。crxjs 用一份 manifest.config.ts 把入口、清单、路径全部串起来,你在开发态只管写 src

热重载把「改完等刷新」变成「改完即更新」,是开发体验上最大的提速点。到这里,模块十三的工具链三章就讲完了:环境、框架与 TS、打包与热重载。你已经具备用现代工具开发 MV3 扩展的完整认知。

上一篇
框架与 TypeScript 开发
下一篇
已经是最后一篇啦