最近在做一些优化,关于页面如何实现秒开,对图片做了懒加载,还有一个重要的优化点,就是实现离线缓存,这样在没有网络的情况下依然可以访问页面,另外在有网络的情况下访问缓存,加载速度会大幅提升
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 });
}
})
);如果说 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 / API 接口 | NetworkFirst | 保证内容新鲜,断网时才用旧数据兜底。 |
| 图片 / 字体 / 库文件 | CacheFirst | 资源几乎不更新,直接读缓存性能最高。 |
| CSS / JS / 配置文件 | StaleWhileRevalidate | 保证加载速度(秒开),同时在后台更新以备下次使用。 |
