PWA(Progressive Web App)赋予了网页类应用接近原生应用的体验,而缓存策略是 PWA 核心能力的关键支撑。基于 Uniapp 开发 PWA 应用时,合理设计 Service Worker 缓存策略,能大幅提升应用加载速度、实现离线可用,同时避免缓存冗余和数据过期问题。本文将从预缓存、运行时缓存、缓存清理三个核心维度,深度拆解 Uniapp 中 PWA 缓存的落地实践方案。
一、基础准备:Uniapp 开启 PWA 与 Service Worker 基础配置
在开始缓存策略实践前,需先完成 Uniapp 项目的 PWA 基础配置,确保 Service Worker 能正常注册和运行。
1. 项目配置开启 PWA
修改 Uniapp 项目根目录的 manifest.json 文件,在 h5 节点下配置 PWA 相关参数:
{
"h5": {
"pwa": {
"enable": true, // 开启 PWA 功能
"serviceWorker": {
"enable": true, // 启用 Service Worker
"path": "/service-worker.js" // 指定 Service Worker 文件路径
},
"manifest": {
"name": "Uniapp PWA 示例", // 应用名称
"short_name": "PWA Demo", // 短名称
"start_url": "/", // 启动路径
"display": "standalone", // 全屏/独立窗口显示
"background_color": "#ffffff", // 背景色
"theme_color": "#409eff" // 主题色
}
}
}
}
2. Service Worker 注册基础
Uniapp 开启 serviceWorker.enable 后,会自动注册 service-worker.js,但需确保该文件放置在 H5 编译输出目录的根目录(Uniapp 项目中可放在 static 目录,编译后会拷贝到 H5 根目录)。
二、核心实践一:预缓存(Precache)—— 关键资源提前缓存
预缓存是指在 Service Worker 安装阶段,主动缓存应用的核心静态资源(如首页、主样式、基础 JS、图标等),确保应用首次加载后,核心资源可离线访问。
1. 预缓存实现思路
- 在 Service Worker 安装事件(install)中,通过 caches.open 创建缓存仓库,指定唯一缓存名称(建议包含版本号,方便后续清理);
- 使用 cache.addAll 批量缓存核心资源,需确保资源路径准确;
- 安装完成后触发 activate 事件,激活 Service Worker。
2. 代码实现
// static/service-worker.js
// 定义缓存名称(包含版本号,便于后续清理旧缓存)
const CACHE_NAME = 'uniapp-pwa-v1.0.0';
// 预缓存核心资源列表(根据实际项目调整)
const PRECACHE_ASSETS = [
'/', // 首页
'/index.html', // Uniapp 编译后的入口页
'/static/js/app.js', // 应用核心 JS
'/static/css/app.css', // 应用核心样式
'/static/logo.png', // 应用图标
'/manifest.json' // PWA 配置文件
];
// 1. 安装阶段:缓存核心资源
self.addEventListener('install', (event) => {
// 等待缓存完成后再激活
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('预缓存核心资源');
return cache.addAll(PRECACHE_ASSETS);
})
.then(() => {
// 跳过等待,直接激活新的 Service Worker
return self.skipWaiting();
})
.catch((err) => {
console.error('预缓存失败:', err);
})
);
});
// 2. 激活阶段:接管所有客户端,清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
Promise.all([
// 接管所有打开的页面
self.clients.claim(),
// 清理旧缓存
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((name) => {
// 删除非当前版本的缓存
if (name !== CACHE_NAME) {
console.log('清理旧缓存:', name);
return caches.delete(name);
}
})
);
})
])
);
});
3. 注意事项
- 预缓存资源列表需精简,仅包含核心、不常变更的资源(如首页、基础样式 / JS),避免缓存体积过大;
- 缓存名称必须包含版本号,这是后续清理旧缓存的关键;
- cache.addAll 要求所有资源都能成功请求(返回 200),否则整个预缓存会失败,需确保资源路径正确。
三、核心实践二:运行时缓存(Runtime Cache)—— 动态资源按需缓存
预缓存仅能覆盖核心静态资源,实际应用中还有大量动态资源(如接口数据、用户头像、动态加载的图片),需通过运行时缓存按需缓存,提升后续访问速度。
1. 运行时缓存设计思路
- 监听 fetch 事件,拦截所有网络请求;
- 对不同类型的资源(接口、静态资源、图片)设计差异化缓存策略:
- 静态资源(CSS/JS/ 图片):优先从缓存读取,缓存未命中则请求网络,并将新资源存入缓存;
- 接口数据:优先请求网络,网络失败则读取缓存(保证数据新鲜度);
- 不可缓存资源(如实时支付接口):直接走网络,不缓存。
2. 差异化缓存代码实现
// static/service-worker.js(续上文)
// 3. 拦截网络请求,实现运行时缓存
self.addEventListener('fetch', (event) => {
// 跳过跨域请求(如第三方接口,可根据实际情况调整)
if (new URL(event.request.url).origin !== self.location.origin) {
return;
}
// 针对不同资源类型,使用不同缓存策略
const request = event.request;
// 1. 接口请求(以 /api 开头):网络优先,离线降级
if (request.url.includes('/api/')) {
event.respondWith(
fetch(request)
.then((response) => {
// 仅缓存成功的响应
if (response && response.status === 200) {
// 克隆响应(响应流只能读取一次)
const responseClone = response.clone();
// 创建运行时缓存仓库
caches.open('uniapp-runtime-api-v1.0.0')
.then((cache) => {
// 将请求和响应存入缓存
cache.put(request, responseClone);
});
}
return response;
})
.catch(() => {
// 网络失败时,从缓存读取
return caches.match(request);
})
);
}
// 2. 静态资源(图片/CSS/JS):缓存优先,网络降级
else if (/\\.(png|jpg|jpeg|gif|css|js)$/i.test(request.url)) {
event.respondWith(
caches.match(request)
.then((cachedResponse) => {
// 缓存命中则直接返回,同时后台更新缓存(保证资源最新)
if (cachedResponse) {
// 后台请求最新资源并更新缓存
fetch(request).then((response) => {
caches.open('uniapp-runtime-static-v1.0.0')
.then((cache) => {
cache.put(request, response.clone());
});
});
return cachedResponse;
}
// 缓存未命中则请求网络,并存入缓存
return fetch(request).then((response) => {
const responseClone = response.clone();
caches.open('uniapp-runtime-static-v1.0.0')
.then((cache) => {
cache.put(request, responseClone);
});
return response;
});
})
);
}
// 3. 其他资源:直接走网络
else {
event.respondWith(fetch(request));
}
});
3. 关键要点
- 响应对象(Response)是流式数据,只能读取一次,需通过 clone() 克隆后再存入缓存;
- 运行时缓存建议按资源类型拆分缓存仓库(如 uniapp-runtime-api、uniapp-runtime-static),便于精细化清理;
- 接口缓存需谨慎,避免缓存敏感数据(如用户信息、订单数据),可通过接口请求头(如 Cache-Control)控制缓存时效。
四、核心实践三:缓存清理方案 —— 避免缓存冗余与过期
随着应用迭代,缓存资源会不断累积,若不及时清理,会导致用户设备存储空间占用过高,且可能出现旧资源无法更新的问题。以下是三种核心清理方案:
1. 版本号驱动清理(推荐)
这是最基础且高效的清理方式,核心逻辑是:缓存名称包含版本号,应用迭代时更新版本号,激活 Service Worker 时删除旧版本缓存(前文预缓存的 activate 事件已实现此逻辑)。
扩展优化:可将版本号配置在 manifest.json 或后端接口,实现动态版本管理:
// 从接口获取最新缓存版本
async function getLatestCacheVersion() {
try {
const res = await fetch('/api/cache-version');
const data = await res.json();
return data.version || 'v1.0.0';
} catch (err) {
return 'v1.0.0';
}
}
// 在 activate 事件中使用最新版本号清理
self.addEventListener('activate', async (event) => {
const latestVersion = await getLatestCacheVersion();
const currentCacheName = `uniapp-pwa-${latestVersion}`;
// 清理旧缓存逻辑(同前文)
});
2. 按缓存时效清理(适用于运行时缓存)
对于接口、图片等动态资源,可在存入缓存时记录时间,清理时删除超过指定时效的缓存:
// 扩展运行时缓存:存入时记录时间
function putWithTimestamp(cacheName, request, response) {
// 克隆响应并添加时间戳
const responseClone = response.clone();
const responseWithTime = new Response(JSON.stringify({
data: await responseClone.text(),
timestamp: Date.now()
}), {
headers: response.headers
});
return caches.open(cacheName).then(cache => {
return cache.put(request, responseWithTime);
});
}
// 清理过期缓存(如超过7天的接口缓存)
async function cleanExpiredCache(cacheName, expireTime = 7 * 24 * 60 * 60 * 1000) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
for (const key of keys) {
const cachedResponse = await cache.match(key);
const data = JSON.parse(await cachedResponse.text());
if (Date.now() – data.timestamp > expireTime) {
await cache.delete(key);
}
}
}
// 在 activate 或定时任务中执行清理
self.addEventListener('activate', (event) => {
event.waitUntil(
cleanExpiredCache('uniapp-runtime-api-v1.0.0')
);
});
3. 主动触发清理(用户交互 / 手动控制)
提供手动清理缓存的接口,通过 postMessage 实现页面与 Service Worker 的通信,触发缓存清理:
// 1. Service Worker 端:监听消息事件
self.addEventListener('message', (event) => {
if (event.data.type === 'CLEAR_CACHE') {
// 清理指定缓存仓库
caches.delete(event.data.cacheName).then(() => {
// 向页面发送清理完成通知
event.source.postMessage({ type: 'CACHE_CLEARED', success: true });
});
}
});
// 2. Uniapp 页面端(H5):发送清理缓存请求
// pages/index/index.vue
export default {
methods: {
clearCache() {
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
type: 'CLEAR_CACHE',
cacheName: 'uniapp-runtime-static-v1.0.0'
});
// 监听清理完成回调
navigator.serviceWorker.addEventListener('message', (event) => {
if (event.data.type === 'CACHE_CLEARED' && event.data.success) {
uni.showToast({ title: '缓存清理成功' });
}
});
}
}
}
}
五、Uniapp PWA 缓存踩坑与优化建议
1. 常见坑点
- 路径问题:Uniapp 编译后 H5 资源路径可能带基础路径(如 /sub-app/),需通过 process.env.BASE_URL 动态拼接缓存路径;
- 缓存更新不及时:Service Worker 安装后需等待旧页面关闭才能激活,可通过 skipWaiting() + clients.claim() 强制接管;
- 跨域资源缓存:跨域资源需服务器配置 Access-Control-Allow-Origin,否则无法缓存响应。
2. 优化建议
- 预缓存资源压缩:对 JS/CSS 进行压缩,图片使用 WebP 格式,减少缓存体积;
- 缓存优先级:核心页面(如首页)预缓存,非核心页面(如详情页)运行时缓存;
- 监控缓存状态:通过 navigator.serviceWorker 监听缓存命中 / 失败事件,统计缓存效率;
- 兼容低版本浏览器:通过 if ('serviceWorker' in navigator) 做降级处理,避免影响基础功能。
六、总结
Uniapp 开发 PWA 应用的缓存策略核心是分层设计:
合理的缓存策略能让 Uniapp PWA 应用在加载速度、离线体验上接近原生应用,同时兼顾资源更新与存储空间占用。实际落地时,需根据应用的资源类型、更新频率、用户场景调整缓存规则,而非照搬模板。


