PWA(渐进式Web应用)并不是某一种单独的技术,而是一组Web能力的组合。其中Service Worker负责离线缓存和后台同步,Web App Manifest负责应用名称、图标和启动方式。React应用本质上是单页应用,首屏依赖JavaScript和CSS资源,一旦网络不可用,很容易出现白屏。借助Workbox,我们可以把构建产物中的静态资源预先写入缓存,并针对接口请求设计合理的缓存更新策略,让应用在弱网或离线环境下仍然可用。

从Service Worker到Workbox:为什么需要工具库
Service Worker是一个独立于页面运行的JavaScript Worker,它可以拦截页面发出的所有网络请求。要在React项目里使用它,通常需要完成注册、安装、激活、缓存清理几个生命周期步骤。手写这些逻辑最大的难点在于如何维护预缓存清单。只要构建产物的文件名带有hash,每次发布后文件名都会变化,手动列出所有要缓存的文件极易遗漏旧文件或缓存过期文件。Workbox的GenerateSW模式可以在webpack或Vite构建阶段扫描产物目录,自动生成一份包含所有静态资源的预缓存清单,并在新版本部署时更新缓存版本。
Workbox还提供了一系列缓存策略,例如CacheFirst、NetworkFirst、StaleWhileRevalidate。这些策略对应不同的应用场景:CacheFirst适合不常变化的字体和图片,NetworkFirst适合需要尽量新鲜的HTML页面,StaleWhileRevalidate适合前端静态资源。通过Workbox,开发者只需要声明匹配规则和策略类型,不必关心底层caches API和请求响应克隆的细节。对于React这类依赖JS分包的框架,预缓存能够显著减少二次访问的加载时间,同时提升离线场景下的可用性。
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { NetworkFirst, CacheFirst } from 'workbox-strategies';
precacheAndRoute(self.__WB_MANIFEST);
registerRoute(
({ request }) => request.destination === 'document',
new NetworkFirst({ cacheName: 'pages-cache' })
);
registerRoute(
({ request }) => ['style', 'script', 'image'].includes(request.destination),
new CacheFirst({ cacheName: 'static-cache' })
);
React项目中的Workbox接入与预缓存配置
在React项目中接入Workbox有两条常见路径。如果使用Create React App创建工程,框架内置了workbox-webpack-plugin,只需把src/index.js中的serviceWorkerRegistration.unregister()改为register(),生产构建就会自动生成Service Worker。不过内置配置相对固定,适合快速体验PWA。对于需要灵活控制缓存规则的项目,推荐使用自定义webpack配置或Vite插件。
以Vite为例,可以安装vite-plugin-pwa,它在内部集成了Workbox。配置时需要指定registerType、includeAssets和manifest信息。例如下面这段vite.config.js片段,关闭自动注册以便在代码中手动控制更新提示。includeAssets可以额外纳入public目录下的robots.txt或favicon等文件。workbox对象中的globPatterns负责声明要预缓存的构建文件类型,通常包含js、css、html和常见图片格式。这样每次执行npm run build后,插件会扫描dist目录并生成sw.js,同时把预缓存清单注入到Service Worker文件中。
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.ico', 'robots.txt'],
manifest: {
name: 'React PWA Demo',
short_name: 'PWA Demo',
theme_color: '#1976d2',
background_color: '#ffffff',
display: 'standalone',
start_url: '/',
icons: [
{
src: '/icons/icon-192x192.png',
sizes: '192x192',
type: 'image/png'
}
]
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.ipipp\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 300
}
}
}
]
}
})
]
});
自动更新模式下,当新版本Service Worker安装完成,Workbox会调用skipWaiting和clientsClaim,页面在下次加载时即可使用新缓存。如果希望提示用户刷新获取新内容,可以把registerType改为prompt,然后监听virtual:pwa-register事件。对于React项目,手动控制更新可以让用户体验更平滑,避免后台更新导致正在进行的表单提交被中断。
运行时缓存策略与请求兜底处理
离线缓存并不只是把静态文件预先缓存起来。React应用通常还会调用后端API获取数据,这些接口同样需要设计缓存策略。Workbox的runtimeCaching配置支持对不同类型的请求定义不同策略。比如对GET类型的文章详情接口可以采用NetworkFirst策略,优先请求网络,失败时回退到缓存;对用户头像或缩略图可以采用StaleWhileRevalidate,先返回缓存内容,再在后台更新。针对POST请求一般不缓存,避免重复提交或泄露敏感数据。
请求兜底是PWA离线体验的重要一环。即使接口没有缓存,页面也不应该直接抛出异常。可以在Service Worker中注册一个Catch Handler,当匹配某个路由的请求发生网络错误时,返回预置的离线响应。例如在Workbox中可以使用setCatchHandler设置一个兜底页面或JSON结构。React组件层面还可以结合navigator.onLine和online/offline事件,在离线时展示提示条,并暂停可能失败的请求。缓存接口数据时要注意缓存的准确性和新鲜度,可以通过ExpirationPlugin设置最大条目数和过期时间,避免无限制增长占用用户存储空间。
import { setCatchHandler } from 'workbox-routing';
import { precacheAndRoute } from 'workbox-precaching';
precacheAndRoute(self.__WB_MANIFEST);
setCatchHandler(async ({ request }) => {
if (request.destination === 'document') {
return Response.redirect('/offline.html');
}
if (request.destination === 'image') {
return new Response('', {
status: 503,
statusText: 'Offline'
});
}
return Response.error();
});
添加到桌面的Manifest配置与浏览器差异
添加到桌面能力依赖Web App Manifest文件。Manifest是一个JSON文件,里面声明了应用名称、短名称、图标、启动地址、主题色和显示模式。要让浏览器出现安装提示,Manifest至少需要满足以下条件:name或short_name、icons中包含192px和512px尺寸的图标、start_url、display为standalone或fullscreen。Chrome还会检查Service Worker是否已注册且能够控制页面,因为可安装PWA必须支持离线访问。在React项目的public目录放置manifest.webmanifest文件,并在index.html中通过 <link rel="manifest"> 引入。注意在HTML中引入资源时,link标签的rel属性不能省略。
iOS的添加到桌面体验与Android略有不同。苹果Safari不读取Manifest中的大部分字段,而是依赖页面内的apple-touch-icon和meta标签。要在iOS上支持添加到主屏幕,需要在index.html中加入以下meta标签,使页面在添加到桌面后以独立模式打开,并显示状态栏样式。iOS不会像Android那样主动弹出安装横幅,用户需要手动点击分享按钮并选择添加到主屏幕。对于React单页应用,还要注意iOS上从桌面图标启动时可能出现的缓存和导航行为差异。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="manifest" href="/manifest.webmanifest" /> <link rel="apple-touch-icon" href="/icons/icon-192x192.png" /> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-status-bar-style" content="default" /> <meta name="apple-mobile-web-app-title" content="PWA Demo" /> </head> <body> <div id="root"></div> </body> </html>
如果使用vite-plugin-pwa,可以开启registerType为autoUpdate并配置devOptions,在开发环境也生成Service Worker,不过开发环境的缓存行为可能与生产不同。正式验收添加到桌面时,应使用HTTPS或localhost,因为Service Worker只在安全上下文可用。完成Manifest和Service Worker后,可以在Chrome DevTools的Application面板检查Manifest字段和Service Worker缓存状态。点击Add to home screen按钮可以模拟安装流程,确认图标、名称和启动地址均符合预期。这样,一个React应用就从纯在线SPA变成了可安装、可离线访问的PWA。