首页
/ Create React App 实践 Progressive Web App:用 Workbox InjectManifest 实现离线优先的 PWA

Create React App 实践 Progressive Web App:用 Workbox InjectManifest 实现离线优先的 PWA

2026-09-04 21:52:48作者:宣聪麟

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.jsonLICENSE 文件,不进入预缓存清单
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(运行时缓存路由),为它们套用任意的运行时缓存策略(如 StaleWhileRevalidateCacheFirst 等)。

定制 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 注册,必须考虑以下六点:

  1. Service Worker 生命周期决定内容更新时机。 为防止懒加载内容的竞态条件,CRA 的默认行为是保守地让更新后的 Service Worker 停留在 "waiting" 状态:用户必须关闭已打开的标签页(仅仅刷新不够)才能看到新内容。
  2. 向用户解释离线行为。 很多用户不熟悉离线优先应用。建议在 Service Worker 完成缓存后提示"此 Web 应用可离线使用!",在新内容就绪、等待下次加载时提示"关闭现有标签页后即可获取新内容"。cra-template-pwa 模板中的 serviceWorkerRegistration.js 演示了应当监听哪些生命周期事件来识别这两种场景,默认实现是把这些消息打到 JS 控制台。
  3. Service Worker 要求 HTTPS,但 localhost 除外(方便本地测试)。如果生产服务器不支持 HTTPS,SW 注册会失败,但 Web 应用其余部分仍可正常工作。相关开发环境配置可参考 使用 HTTPS
  4. Service Worker 只在生产环境启用(即 npm run build 的输出),这与源码中 isEnvProduction && 的条件严格对应。官方建议不要在开发环境启用离线 SW——缓存的旧资产不包含你本地的最新改动,会非常令人沮丧。
  5. 本地测试离线行为:构建应用(npm run build),然后从 build/ 目录运行标准 HTTP 服务器。构建脚本执行后会打印本地测试方式(源码见 printHostingInstructions.js 输出的 serve -s build 命令);其他部署方式参见 部署文档务必使用隐身窗口,避免浏览器缓存干扰。
  6. 默认不拦截跨域流量。 生成的 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.jsonworkbox-webpack-plugin ^6.4.1
默认 Web App Manifest public/manifest.json
构建后本地测试提示 printHostingInstructions.js
部署方式 deployment.md

一句话总结:CRA 4 的 PWA 能力 = manifest 默认生效 + Service Worker 文件存在即启用;开启前理解生命周期 "waiting" 行为、HTTPS 前提与仅生产生效这三条约束,你就能安全地交付一个离线优先的应用。

登录后查看全文
热门项目推荐
相关项目推荐