最近在做一些优化,关于页面如何实现秒开,对图片做了懒加载,还有一个重要的优化点,就是实现离线缓存,这样在没有网络的情况下依然可以访问页面,另外在有网络的情况下访问缓存,加载速度会大幅提升
PWA(Progressive Web App,渐进式 Web 应用) 是一种结合了 Web 和原生应用优势的新型应用模式。使用现代 Web 技术,让网站具备类似原生应用的用户体验
他的功能有很多,先介绍两个,其余的后续研究一下再补充
📲 可安装 - 添加到主屏幕,全屏体验
📶 离线可用 - 在网络不稳定或离线时也能使用
目前该系统添加了安装以及离线缓存功能,该文章的重点在于离线缓存
讲离线缓存之前绕不开的就是Service Worker
在没有 SW 之前,浏览器请求资源是直线的:浏览器 -> 网络 -> 服务器。
有了 SW 之后,请求变成了折线:
离线缓存不只是把文件存起来,更重要的是怎么拿。在 sw.js 配置中,用到了几种经典策略:
PWA 离线缓存,本质上是理解如何通过 Service Worker (SW) 在浏览器和网络之间架设一个“智能代理层”
以Nuxt项目为例,讲解一下如何实现离线缓存
安装该插件,也是Nuxt官方推荐使用,以实现PWA离线缓存,文档 https://nuxt.com/modules/vite-pwa-nuxt
在config的配置文件中添加配置:
import { defineNuxtConfig } from 'nuxt/config'
export default defineNuxtConfig({
modules: [
'@vite-pwa/nuxt'
],
//里面进行相关配置
pwa: {
/* PWA options */
}
})
从workbox中引入核心模块,Workbox 是 Google 开发的一套 JavaScript 库,旨在简化 Service Worker 的开发
// 当安装 @vite-pwa/nuxt 时,它已经作为“底层依赖”自动帮你下载好了这些以 workbox- 开头的包
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
import { registerRoute, NavigationRoute } from 'workbox-routing';
import { NetworkFirst, CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { CacheableResponsePlugin } from 'workbox-cacheable-response'; // 注意这里
import { ExpirationPlugin } from 'workbox-expiration';
// self 指代的是 Service Worker 的全局作用域对象
// 强制让当前正在安装的 Service Worker 跳过“等待”状态,直接上位
self.addEventListener('install', () => self.skipWaiting());
// 新激活的 SW 会立刻接管现有的网页,不需要用户手动刷新
self.addEventListener('activate', (event) => event.waitUntil(clients.claim()));
//构建工具注入静态资源列表,SW 会在安装时下载这些资源并永久存入 Cache Storage
//根据你在 nuxt.config.ts 中的 pwa 配置自动扫描出来的:
//扫描范围:它会去扫描 .output/public 文件夹。
//过滤条件:取决于你的 globPatterns 配置(如 **/*.{js,css,html,png})。
//排除条件:取决于你的 globIgnores(如你配置的 **/_nuxt/builds/meta/dev.json 会被踢出清单,不会出现在这里)
precacheAndRoute(self.__WB_MANIFEST || []);
cleanupOutdatedCaches();它会监听网页发出的每一个请求。你可以告诉它:“嘿,只要是去 /api/ 的请求,或者所有的 .png 图片,你都把它拦下来。”
一旦拦截成功,它会按照你指定的“套路”(策略)去处理。比如:
通常接受三个参数:
registerRoute(
match, // 1. 匹配条件:谁该被拦截?
handler, // 2. 处理器/策略:拦下来后怎么处理?
method // 3. (可选) 请求方法:是拦截 GET 还是 POST?(默认 GET)
);当你的浏览器发起任何请求(比如加载一张图片、跳转一个页面或请求一个接口)时,这个请求都会经过 registerRoute。Workbox 会把这个请求封装成两个对象传给你:request 和 url
1.request 是一个原生的 Request 对象。它代表了浏览器想做什么以及怎么做
request.destination:最实用的属性。它告诉 Service Worker 这个请求是用来干什么的。
2.url 是一个标准的 URL 对象。它代表了这个请求去哪里
是 Workbox 中最常用、也是最能平衡“内容新鲜度”与“离线可用性”的策略
核心逻辑是:“先联网要最新的,如果网不行,再回仓库拿旧的。”
NetworkFirst 是最“昂贵”的策略(因为它总是产生网络延迟),所以必须用在刀刃上:
深入:手机显示有信号但实际网速极慢,NetworkFirst 策略会卡死用户,应该如何处理?
Workbox 路由系统中专门为**“页面跳转”**(即导航请求)定制的高级工具
它能精准识别“改变页面主体”的行为,并提供最高优先级的离线保护
A. 消除“小恐龙”:实现离线兜底 (Offline Fallback)
B. 解决“刷新即死”:SPA 的刷新支持
registerRoute(
({ request, url }) =>
request.destination === 'script' ||
url.pathname.includes('/_payload.json') ||
url.pathname.startsWith('/_nuxt/'),
new NetworkFirst({
cacheName: 'nuxt-assets',
networkTimeoutSeconds: 2,
plugins: [
new CacheableResponsePlugin({ statuses: [200] }), // 只缓存状态码为 200 的正常响应
]
})
)
// 导航请求:解决“无法访问此网站” ERR_FAILED 问题
const navigationHandler = new NetworkFirst({
cacheName: 'pages-cache',
networkTimeoutSeconds: 2,
plugins: [
new CacheableResponsePlugin({ statuses: [200] })
]
});
registerRoute(
new NavigationRoute(async (params) => {
try {
const response = await navigationHandler.handle(params);
// 只要缓存里有这个页面的备份,就直接返回
if (response) return response;
throw new Error('Response not found');
} catch (error) {
// 离线访问一个“从来没打开过”且“没有预缓存”的新页面时,显示 offline.html
// 这里的兜底逻辑顺序:offline.html -> index.html (SPA入口) -> 抛出错误
return (await caches.match('/offline.html')) ||
(await caches.match('/')) ||
new Response('Offline content not available', { status: 503 });
}
})
);它是一个“过滤器”,用来决定从网络获取的哪些响应,才值得被存入缓存。
Service Worker 的 fetch 事件会拦截所有请求,但并不是所有网络响应都应该被缓存,CacheableResponsePlugin 让你能精确控制只有符合特定条件的响应,才被允许进入缓存
基于状态码过滤:
// 只缓存成功的响应
new CacheableResponsePlugin({ statuses: [200] })
// 也缓存 304 Not Modified (通常配合其他策略)
new CacheableResponsePlugin({ statuses: [200, 304] })
// 关键:允许缓存跨域请求返回的“不透明响应” (status: 0)
new CacheableResponsePlugin({ statuses: [0, 200] })应用实例:
你的图片托管在阿里云 CDN(img.blackztt.cn),这是一个跨域资源。出于安全考虑,浏览器对跨域请求获取的图片,默认返回一个“不透明响应” (Opaque Response)。
Workbox 的默认行为:
为了数据安全,Workbox 的所有缓存策略默认情况下不会缓存 status 不是 200 的响应。因此,这些来自 CDN 的图片,虽然显示正常,但从未被写入缓存。
解决方案:
通过 CacheableResponsePlugin,明确告诉 Workbox:“status: 0 的不透明响应对我来说也是有效的,请缓存它。”
如果说 NetworkFirst 是为了保证“绝对新”,CacheFirst 是为了“绝对快”,那么 StaleWhileRevalidate 就是在不牺牲用户体验速度的情况下,尽可能保证数据更新
| 维度 | NetworkFirst | CacheFirst | StaleWhile Revalidate |
|---|---|---|---|
| 首选来源 | 网络 | 缓存 | 缓存 (立即返回) |
| 备选来源 | 缓存 | 网络 (仅当缓存无) | 网络 (后台异步) |
| 用户感知 | 慢 (取决于网速) | 极快 | 极快 |
| 数据新鲜度 | 最新 | 可能非常陈旧 | 次新 (落后一个版本) |
| 离线体验 | 优 | 优 | 优 |
//在断网时依然能保持页面切换逻辑
registerRoute(
({ url }) => url.pathname.includes('/_nuxt/builds/'),
// 使用 StaleWhileRevalidate,保证离线能用,有网自动更新
new StaleWhileRevalidate({
cacheName: 'nuxt-manifest-cache',
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] })
]
})
);
// API 请求:网络优先 (修改为 NetworkFirst 实例,避免手动 fetch)
registerRoute(
({ url }) => url.pathname.includes('/api/'),
new NetworkFirst({
cacheName: 'api-cache',
// 网络优先,有网优先返回新数据,等待4s没网自动切换本地缓存
networkTimeoutSeconds: 4,
plugins: [
new CacheableResponsePlugin({ statuses: [200] }),
//防止你的 API 缓存无限膨胀。它只会保留最近请求的 50 条记录,设定有效期为 24 小时
new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 }),
{
cacheWillUpdate: async ({ response }) => {
// 只有 200 的 API 响应才允许存入缓存,防止 502/503 污染缓存
return response.status === 200 ? response : null;
}
}
]
})
)| 资源类型 | 典型例子 | 变化频率 | 离线必需 | 推荐策略 |
|---|---|---|---|---|
| HTML 页面 | index.html, about.html | 中 | ✅ 是 | Network First |
| CSS/JS 代码 | main.css, app.js | 低 | ✅ 是 | Cache First (带版本号) |
| 图片/字体 | logo.png, icon.woff2 | 极低 | ⚠️ 可选 | Stale-While-Revalidate |
| API 数据 | /api/user, /api/posts | 高 | ❌ 否 | Network First |
| 第三方资源 | CDN 上的库 | 极低 | ❌ 否 | Cache Only (预缓存) |
| 离线回退页 | offline.html | 极低 | ✅ 是 | Cache Only |
为什么:HTML 是应用的入口,必须是最新版本。但如果网络失败,应该显示缓存的旧页面而不是报错。
为什么:这类资源通常带哈希版本号(如 main.a1b2c3.js),一旦缓存可永久使用。新版本通过 HTML 页面更新触发。
为什么:让用户立即看到旧图片,后台悄悄更新缓存。兼顾速度和新鲜度。
为什么:数据需要新鲜,但离线时也要显示旧数据(读操作)。写操作(POST/PUT)不应该缓存。
为什么:这些资源几乎不变,应该完全依赖缓存,避免网络请求。
