Create React App 实践 Progressive Web App:用 Workbox InjectManifest 实现离线优先的 PWA
Create React App(CRA)的生产构建自带构建一流的 Progressive Web App(PWA)所需的全部能力,但离线/缓存优先(offline/cache-first)行为是选择性开启(opt-in)的。本文基于仓库中的 PWA 官方文档 与 react-scripts 的真实构建配置,讲解如何在 CRA 4+ 中通过 src/service-worker.js 接入 Workbox 的 InjectManifest 插件,完成预缓存(precache)、缓存优先策略与 Web App Manifest 元数据的完整配置,并说明启用离线优先前必须考虑的生命周期、HTTPS 与调试问题。
CRA 4 的 PWA 支持模型:以 src/service-worker.js 作为开关
CRA 3 及以前,离线能力由内置的 generateSW 自动托管,开发者几乎没有控制力。从 Create React App 4 开始,策略被彻底改变(这一变更对应仓库 CHANGELOG-4.x.md 中 "Switch to the Workbox InjectManifest plugin" 的记录,PWA 模板也随之迁移到了独立的 cra-template/pwa 仓库):
- 开关就是文件本身:只要项目里存在
src/service-worker.js,生产构建就会编译它,并用 Workbox 把"需要预缓存的 URL 列表"注入进去; - 不想要 Service Worker? 不要创建这个文件即可。
InjectManifest插件根本不会被执行,你的构建里不会生成任何 SW 产物; - 想要完整的离线优先起点:使用 PWA 自定义模板创建项目,模板会附带一个经过实践检验的
src/service-worker.js:
npx create-react-app my-app --template cra-template-pwa
TypeScript 版本等价命令:
npx create-react-app my-app --template cra-template-pwa-typescript
本仓库的 默认模板(packages/cra-template/template/src/)并不包含 service-worker.js,也就是说 cra-template 默认只提供了 PWA 元数据(manifest.json),离线能力需要显式选择 PWA 模板或手写该文件。这一点可以从模板源码结构直接确认。
注册:在 src/index.js 中切换 register()
仅仅有 Service Worker 文件还不够,它必须在页面中被注册才会生效。PWA 模板的 src/index.js 中默认是取消注册的,并留有一段注释提示你如何开启:
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://cra.link/PWA
serviceWorkerRegistration.unregister();
按注释的说明,把 serviceWorker.unregister() 切换为 serviceWorker.register(),即完成离线优先行为的开启。PWA 模板中随附的 src/serviceWorkerRegistration.js 还封装了各生命周期事件的监听逻辑(默认只把相应消息打到 JS 控制台),可直接作为你自己 UI 提示的起点。
源码剖析:生产构建如何接入 Workbox InjectManifest
CRA 并没有要求开发者写任何 webpack 配置,预缓存逻辑内建在 react-scripts 的生产配置里。可以从以下源码链路完整还原这一机制:
1)Service Worker 入口的探测。 packages/react-scripts/config/paths.js 中定义了:
swSrc: resolveModule(resolveApp, 'src/service-worker'),
resolveModule 会按 js / mjs / ts / tsx 等模块扩展名依次尝试解析 src/service-worker,因此文件扩展名是 .js 或 .ts 都能被识别。
2)插件挂载条件与参数。 在 packages/react-scripts/config/webpack.config.js 的生产构建插件列表中:
// Generate a service worker script that will precache, and keep up to date,
// the HTML & assets that are part of the webpack build.
isEnvProduction &&
fs.existsSync(swSrc) &&
new WorkboxWebpackPlugin.InjectManifest({
swSrc,
dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
// Bump up the default maximum size (2mb) that's precached,
// to make lazy-loading failure scenarios less likely.
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
}),
逐项解读这组参数:
| 参数 | 作用 |
|---|---|
swSrc |
你手写的 Service Worker 源文件路径(即 src/service-worker.js) |
dontCacheBustURLsMatching |
匹配 /\.[0-9a-f]{8}\./ 的 URL(内容哈希文件名)不再叠加 Workbox 自己的 query 缓存破坏参数,避免双份 hash |
exclude |
明确排除 .map 源映射、asset-manifest.json 和 LICENSE 文件,不进入预缓存清单 |
maximumFileSizeToCacheInBytes |
把 Workbox 默认 2MB 的单文件预缓存上限提到 5MB,降低 code-splitting 后大懒加载 chunk 无法被预缓存、离线场景加载失败的概率 |
注意两个前置条件:isEnvProduction(见下文"仅在生产环境启用")与 fs.existsSync(swSrc)——即"没有 src/service-worker.js 就没有 SW 插件"这一文档表述在源码里得到了逐字印证。
3)依赖版本。 packages/react-scripts/package.json 声明了 "workbox-webpack-plugin": "^6.4.1",说明当前仓库对应的 Workbox 生态版本为 6.x 系列。
构建完成后,你的 src/service-worker.js 会被编译打包,其中的 self.__WB_MANIFEST 占位符被替换为全部 webpack 产物的 URL 数组,sw.js 与这些资源一起输出到 build/ 目录。
预缓存边界:哪些资源会被缓存,哪些不会
InjectManifest 采用缓存优先(cache-first)策略处理所有 webpack 生成的资源请求,包括针对 HTML 的导航请求(navigation requests)——这意味着在慢网或断网环境下,页面仍能一致地快速加载。
需要明确预缓存的边界:
- 会被预缓存:
webpack构建产物(JS、CSS、由import引入的图片/字体等静态站点资源),并在每次部署更新时自动保持最新;更新在后台下载; - 不会被预缓存:未经 webpack 处理的静态文件——例如直接从本地
public/目录原样拷贝的文件,以及第三方域名的资源; - 对上述"清单外"资源,你可以自行在
src/service-worker.js中配置 Workbox 的 routes(运行时缓存路由),为它们套用任意的运行时缓存策略(如StaleWhileRevalidate、CacheFirst等)。
定制 Service Worker:唯一约束是保留 self.__WB_MANIFEST
CRA 4 起,Service Worker 的逻辑完全归你控制:你可以基于 cra-template-pwa / cra-template-pwa-typescript 模板提供的文件修改,也可以从零创建自己的 src/service-worker.js。可以引入 Workbox 的其他模块、加入推送通知库,或删掉默认的缓存逻辑。
唯一的硬性要求是:文件里必须保留 self.__WB_MANIFEST。Workbox 编译插件正是靠检测这个值来生成并注入预缓存 URL 清单的。如果你决定完全不用预缓存,可以把它赋给一个被忽略的变量:
// eslint-disable-next-line no-restricted-globals
const ignored = self.__WB_MANIFEST;
// Your custom service worker code goes here.
为什么是 Opt-in:收益与调试代价
离线优先的 PWA 相比传统网页更快、更可靠,移动端体验也更沉浸:
- 属于
webpack构建的所有静态资源都被缓存,后续访问无论网络状况(2G/3G 均可)都能快速加载,更新在后台完成; - 应用不依赖网络状态也能运行——用户在万米高空或地铁里都能继续使用;
- 在移动端可以直接"添加到主屏幕"(含应用图标),无需应用商店。
但缓存也会让部署问题的排查更难(CRA 历史上有专门的 issue 讨论过,例如 #2398、#3613)。这正是官方把它设计为 opt-in 的原因:不开启则零心智负担,开启则需理解下面的取舍。
离线优先的六点实操考虑
文档明确要求,一旦决定开启 Service Worker 注册,必须考虑以下六点:
- Service Worker 生命周期决定内容更新时机。 为防止懒加载内容的竞态条件,CRA 的默认行为是保守地让更新后的 Service Worker 停留在 "waiting" 状态:用户必须关闭已打开的标签页(仅仅刷新不够)才能看到新内容。
- 向用户解释离线行为。 很多用户不熟悉离线优先应用。建议在 Service Worker 完成缓存后提示"此 Web 应用可离线使用!",在新内容就绪、等待下次加载时提示"关闭现有标签页后即可获取新内容"。
cra-template-pwa模板中的serviceWorkerRegistration.js演示了应当监听哪些生命周期事件来识别这两种场景,默认实现是把这些消息打到 JS 控制台。 - Service Worker 要求 HTTPS,但
localhost除外(方便本地测试)。如果生产服务器不支持 HTTPS,SW 注册会失败,但 Web 应用其余部分仍可正常工作。相关开发环境配置可参考 使用 HTTPS。 - Service Worker 只在生产环境启用(即
npm run build的输出),这与源码中isEnvProduction &&的条件严格对应。官方建议不要在开发环境启用离线 SW——缓存的旧资产不包含你本地的最新改动,会非常令人沮丧。 - 本地测试离线行为:构建应用(
npm run build),然后从build/目录运行标准 HTTP 服务器。构建脚本执行后会打印本地测试方式(源码见 printHostingInstructions.js 输出的serve -s build命令);其他部署方式参见 部署文档。务必使用隐身窗口,避免浏览器缓存干扰。 - 默认不拦截跨域流量。 生成的 Service Worker 不会拦截或缓存跨域请求——如指向其他域名的 HTTP API 请求、图片或嵌入内容。CRA 4 起,你可以按上文"定制"一节自行扩展这一行为。
PWA 元数据:public/manifest.json
PWA 元数据与 Service Worker 是两条独立的能力线:无论你是否开启 SW 注册,manifest 元数据始终生效。
CRA 的默认配置已包含一个位于 public/manifest.json 的 Web App Manifest(该文件会原样拷贝进 build/ 目录),当用户在 Android 的 Chrome 或 Firefox 中把 Web 应用添加到主屏幕时,其中的字段决定图标、名称与品牌色的呈现。仓库默认模板的实际内容如下:
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
按你的 Web 应用特性替换名称、图标与品牌色即可。当主屏幕上的应用处于"有活跃 Service Worker + 已预缓存"的状态时,它的加载会更快并支持离线运行。
小结:能力地图与文件定位
| 关注点 | 位置 / 命令 |
|---|---|
| PWA 行为总览文档 | docusaurus/docs/making-a-progressive-web-app.md |
| SW 入口探测 | paths.js 中的 swSrc |
| InjectManifest 注入逻辑 | webpack.config.js |
| Workbox 插件依赖 | react-scripts/package.json(workbox-webpack-plugin ^6.4.1) |
| 默认 Web App Manifest | public/manifest.json |
| 构建后本地测试提示 | printHostingInstructions.js |
| 部署方式 | deployment.md |
一句话总结:CRA 4 的 PWA 能力 = manifest 默认生效 + Service Worker 文件存在即启用;开启前理解生命周期 "waiting" 行为、HTTPS 前提与仅生产生效这三条约束,你就能安全地交付一个离线优先的应用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00