UniApp缓存系统详解
一、UniApp 缓存系统概述
1.1 缓存概念与作用
在 UniApp 中,本地缓存(Storage)是一种键值对(Key-Value)存储机制,用于在设备本地保存小量数据。它类似于浏览器的 localStorage 或小程序的 wx.setStorage,但 UniApp 通过统一的 API 封装,实现了跨平台的透明使用。缓存的作用主要体现在以下方面:
- 性能提升:避免重复网络请求。例如,登录后将 Token 存入缓存,下次启动 App 时直接读取,无需重新验证。
- 用户体验优化:支持离线模式,如缓存文章列表,用户无网也能浏览。
- 数据持久化:应用重启或页面刷新后,数据不丢失(除非手动清理)。
- 状态共享:跨页面/组件共享数据,如购物车内容。
与内存状态(如 Vuex)不同,缓存是持久化的,但需注意其非关系型特性:不支持复杂查询,仅适合简单对象或字符串。在实际项目中,我经常用它来处理临时配置,避免每次启动都从服务器拉取。
1.2 平台差异详解
UniApp 缓存在不同平台的底层实现有所差异,这直接影响存储上限、持久性和清理策略。以下表格总结关键差异:
| 平台 | 底层实现 | 存储上限 | 持久性 | 清理机制 |
|---|---|---|---|---|
| H5 | localStorage | 约 5MB(浏览器限制) | 缓存概念,可能被浏览器清理 | 用户清除浏览数据或过期 |
| App | plus.storage | 无限制(设备存储空间) | 持久化,直至卸载 App | 手动清理或系统空间不足 |
| 微信小程序 | wx.setStorage | 单 key 1MB,总 10MB | 与小程序生命周期绑定 | 用户删除小程序或超限自动清理 |
| 支付宝小程序 | my.setStorage | 单条 200KB,总 10MB | 与小程序生命周期绑定 | 超限自动清理 |
| 百度/抖音小程序 | 平台特定 API | 视平台文档(约 10MB) | 与小程序生命周期绑定 | 平台策略清理 |
| HarmonyOS | 分布式存储 | 视设备(HBuilderX 4.23+ 支持) | 持久化 | 手动或系统管理 |
注意:清空缓存后,非 App 平台可能导致 uni.getSystemInfo 的 deviceId 改变,影响设备标识。开发者需在多端测试,确保兼容。在我的经验中,App 端的无限制存储特别适合缓存大文件列表,但要警惕设备空间耗尽。
1.3 支持的数据类型与限制
UniApp 缓存支持原生类型(String、Number、Boolean、Array、Object)和可通过 JSON.stringify 序列化的复杂对象。但不支持函数、Date 对象(需转为字符串)或循环引用。限制包括:
- Key 命名:避免系统保留前缀(如
uni-、dcloud_),否则可能冲突或失败。 - 数据大小:超出平台上限时,存储失败,无自动压缩。
- 线程安全:异步 API 不阻塞 UI 线程,同步 API 可能在高频操作时影响性能。
- 生命周期:H5/App 持久,小程序随应用销毁。
这些基础认知是上手的关键,接下来我们深入 API 层面。
二、基础 API 详解
UniApp 提供 5 对核心 API:存储、获取、移除、清空和信息查询。每对均有异步(回调式)和同步(try-catch 式)版本。异步适合复杂场景,避免阻塞;同步适合简单操作,代码简洁。以下逐一拆解。
2.1 存储数据:uni.setStorage / uni.setStorageSync
原理:将数据存入指定 key,会覆盖原有内容。底层序列化为字符串存储。
异步版本:uni.setStorage(OBJECT)
参数表:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| key | String | 是 | 缓存键名,建议使用描述性命名如 ‘user_token’ |
| data | Any | 是 | 存储内容,支持 JSON 序列化对象 |
| success | Function | 否 | 成功回调,无参数 |
| fail | Function | 否 | 失败回调,参数为错误对象 |
| complete | Function | 否 | 结束回调,无论成败 |
返回值:无,通过回调处理。
示例(Vue 页面中):
// pages/index/index.vue
export default {methods: {asyncStoreData() {uni.setStorage({key: 'user_info',data: { id: 1, name: '张三', token: 'abc123' },success: () => {console.log('存储成功');uni.showToast({ title: '数据已缓存' });},fail: (err) => {console.error('存储失败:', err);uni.showToast({ title: '存储出错', icon: 'none' });}});}}
}
解释:在按钮点击事件中调用,成功后提示用户。data 对象自动序列化。在实际开发中,这种异步方式特别适合用户交互密集的页面。
同步版本:uni.setStorageSync(key, data)
参数:key (String,必填),data (Any,必填)。
返回值:无,使用 try-catch 处理异常。
示例:
try {uni.setStorageSync('user_info', { id: 1, name: '张三', token: 'abc123' });console.log('同步存储成功');
} catch (e) {console.error('同步存储失败:', e);
}
规则:
- 异步优先:UI 交互场景用异步,避免卡顿。
- 错误处理:fail 回调捕获网络/权限问题,try-catch 捕获序列化失败。
- 注意事项:data 必须可序列化,否则抛错(如包含函数)。我通常在存储前添加一个序列化检查函数,确保稳定性。
2.2 获取数据:uni.getStorage / uni.getStorageSync
原理:从 key 读取内容,若不存在返回 undefined 或空值。
异步版本:uni.getStorage(OBJECT)
参数表:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| key | String | 是 | 缓存键名 |
| success | Function | 是 | 回调,res = { data: 读取内容 } |
| fail | Function | 否 | 失败回调 |
| complete | Function | 否 | 结束回调 |
success 返回值:{ data: Any }
示例:
asyncGetData() {uni.getStorage({key: 'user_info',success: (res) => {if (res.data) {console.log('获取数据:', res.data.name);this.userName = res.data.name; // 更新页面数据} else {console.log('缓存为空');}},fail: (err) => {console.error('获取失败:', err);}});
}
解释:读取后直接绑定到页面 data,实现响应式更新。在组件 mounted 钩子中调用,能快速初始化视图。
同步版本:uni.getStorageSync(key)
参数:key (String,必填)。
返回值:Any(不存在时 undefined)。
示例:
try {const userInfo = uni.getStorageSync('user_info');if (userInfo) {console.log('同步获取:', userInfo.name);} else {console.log('无数据');}
} catch (e) {console.error('同步获取失败:', e);
}
规则:
- 默认值处理:读取前可检查 typeof res.data !== ‘undefined’。
- 性能提示:高频读取用同步,低频用异步。同步版本在初始化阶段特别高效,能节省几毫秒加载时间。
2.3 移除数据:uni.removeStorage / uni.removeStorageSync
原理:删除指定 key,若不存在仍返回成功。
异步版本:uni.removeStorage(OBJECT)
参数表:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| key | String | 是 | 要移除的键名 |
| success | Function | 是 | 成功回调,无参数 |
| fail | Function | 否 | 失败回调 |
| complete | Function | 否 | 结束回调 |
示例:
removeData() {uni.removeStorage({key: 'user_info',success: () => {console.log('移除成功');uni.showToast({ title: '缓存已清除' });}});
}
同步版本:uni.removeStorageSync(key)
示例:
try {uni.removeStorageSync('user_info');console.log('同步移除成功');
} catch (e) {console.error('移除失败:', e);
}
注意事项:移除后,相关页面需刷新数据源,避免显示旧值。结合事件总线,能通知其他组件更新。
2.4 清空数据:uni.clearStorage / uni.clearStorageSync
原理:移除所有缓存键值对,慎用!
异步版本:uni.clearStorage(OBJECT)
参数:success/fail/complete(可选)。
示例:
clearAll() {uni.clearStorage({success: () => {console.log('全部清空成功');// 跳转登录页uni.reLaunch({ url: '/pages/login/login' });}});
}
同步版本:uni.clearStorageSync()
示例:
try {uni.clearStorageSync();console.log('同步清空成功');
} catch (e) {console.error('清空失败:', e);
}
规则:
- 备份重要数据:清空前用 getStorageInfo 导出 keys。
- 平台影响:非 App 端可能重置 deviceId。在生产环境中,我会添加确认对话框,防止误操作。
2.5 获取存储信息:uni.getStorageInfo / uni.getStorageInfoSync
原理:查询当前缓存状态,用于监控和调试。
异步版本:uni.getStorageInfo(OBJECT)
success 返回值:
| 参数名 | 类型 | 说明 |
|---|---|---|
| keys | Array | 所有键名数组 |
| currentSize | Number | 当前占用 KB |
| limitSize | Number | 限制 KB |
示例:
getInfo() {uni.getStorageInfo({success: (res) => {console.log('键列表:', res.keys);console.log('占用:', res.currentSize + 'KB / ' + res.limitSize + 'KB');}});
}
同步版本:uni.getStorageInfoSync()
示例:
try {const res = uni.getStorageInfoSync();console.table(res); // 表格输出
} catch (e) {console.error(e);
}
应用:在设置页显示存储使用率,提示用户清理。定期调用能帮助诊断内存问题。
通过这些基础 API,开发者可快速实现简单缓存。接下来,我们探讨如何在复杂场景中扩展它们。
三、高级用法与技巧
基础 API 虽强大,但实际项目中需处理过期、集成和安全等问题。本节提供一些实用技巧,帮助你构建更robust的缓存系统。
3.1 缓存过期机制实现
原理:UniApp 无内置过期,需手动添加时间戳判断。
实现步骤:
- 存储时附加 timestamp: Date.now()。
- 读取时计算差值,超阈值则移除并返回 null。
示例(工具函数):
// utils/cache.js
const CACHE_EXPIRE = 60 * 60 * 1000; // 1小时export function setWithExpire(key, data, expire = CACHE_EXPIRE) {const cacheObj = {data,timestamp: Date.now() + expire};try {uni.setStorageSync(key, JSON.stringify(cacheObj));} catch (e) {console.error('设置过期缓存失败:', e);}
}export function getWithExpire(key) {try {const str = uni.getStorageSync(key);if (!str) return null;const cacheObj = JSON.parse(str);if (Date.now() > cacheObj.timestamp) {uni.removeStorageSync(key); // 过期移除return null;}return cacheObj.data;} catch (e) {console.error('获取过期缓存失败:', e);return null;}
}
使用:
// 存储
setWithExpire('article_list', articles, 30 * 60 * 1000); // 30分钟// 读取
const articles = getWithExpire('article_list');
if (articles) {this.list = articles;
} else {// 从服务器重新获取
}
优势:防止陈旧数据,提升数据新鲜度。在电商 App 中,我用它缓存商品价格,过期后自动刷新,避免用户看到过时信息。
3.2 与 Vuex/Pinia 集成持久化
原理:Vuex 内存状态非持久,结合缓存实现重启恢复。
Vuex 示例(store/index.js):
import Vue from 'vue';
import Vuex from 'vuex';Vue.use(Vuex);const store = new Vuex.Store({state: {user: null,cart: []},mutations: {SET_USER(state, user) {state.user = user;// 持久化uni.setStorageSync('vuex_user', JSON.stringify(user));},SET_CART(state, cart) {state.cart = cart;uni.setStorageSync('vuex_cart', JSON.stringify(cart));}},actions: {initFromCache({ commit }) {// 应用启动时恢复const userStr = uni.getStorageSync('vuex_user');if (userStr) commit('SET_USER', JSON.parse(userStr));const cartStr = uni.getStorageSync('vuex_cart');if (cartStr) commit('SET_CART', JSON.parse(cartStr));}}
});export default store;
main.js 中调用:
import store from './store';
Vue.prototype.$store = store;
store.dispatch('initFromCache'); // App 启动时执行
Pinia 变体:类似,使用 defineStore 的 persist 插件(社区扩展)。
规则:仅持久关键状态,避免缓存大对象。在大型项目中,这种集成能让状态管理更可靠,用户重启 App 后无缝继续。
3.3 全局缓存管理工具封装
原理:统一入口,简化多处调用,支持命名空间。
示例(utils/storageManager.js):
class StorageManager {constructor(namespace = '') {this.namespace = namespace ? `${namespace}_` : '';}set(key, data) {try {uni.setStorageSync(this.namespace + key, data);} catch (e) {throw new Error(`存储失败: ${e.message}`);}}get(key, defaultValue = null) {try {return uni.getStorageSync(this.namespace + key) || defaultValue;} catch (e) {console.warn(`获取失败: ${key}`);return defaultValue;}}remove(key) {try {uni.removeStorageSync(this.namespace + key);} catch (e) {}}clear() {try {uni.clearStorageSync();} catch (e) {}}getInfo() {try {return uni.getStorageInfoSync();} catch (e) {return null;}}
}// 使用
const userStorage = new StorageManager('user');
userStorage.set('token', 'xyz');
const token = userStorage.get('token');
优势:避免 key 冲突,支持模块化(如 ‘user_token’)。在团队开发中,这样的封装能减少代码重复,提高维护性。
3.4 数据加密存储
原理:敏感数据(如 Token)易被逆向,需 AES 加密。
依赖:UniApp 无内置 crypto,App 端用 plus.crypto(需条件编译)。
示例(简单 Base64 + 自定义密钥,H5/小程序通用):
// utils/encrypt.js
const KEY = 'MySecretKey12345'; // 生产环境用随机密钥function encrypt(data) {const str = typeof data === 'object' ? JSON.stringify(data) : data;let result = '';for (let i = 0; i < str.length; i++) {result += String.fromCharCode(str.charCodeAt(i) ^ KEY.charCodeAt(i % KEY.length));}return btoa(result); // Base64 编码
}function decrypt(encrypted) {try {const decoded = atob(encrypted);let result = '';for (let i = 0; i < decoded.length; i++) {result += String.fromCharCode(decoded.charCodeAt(i) ^ KEY.charCodeAt(i % KEY.length));}return JSON.parse(result); // 假设对象} catch (e) {return null;}
}// 使用
uni.setStorageSync('encrypted_token', encrypt('abc123'));
const token = decrypt(uni.getStorageSync('encrypted_token'));
注意:H5 用 CryptoJS 库(npm 引入),App 用原生。对于金融类 App,这层保护至关重要,能防范简单逆向工程。
3.5 批量操作与事务模拟
原理:无原生事务,用 Promise.all 模拟批量。
示例:
async batchSet(items) {const promises = items.map(({ key, data }) =>new Promise((resolve, reject) => {uni.setStorage({key,data,success: resolve,fail: reject});}));try {await Promise.all(promises);console.log('批量存储成功');} catch (e) {console.error('批量失败,回滚');// 模拟回滚:清空相关键items.forEach(({ key }) => uni.removeStorageSync(key));}
}// 调用
this.batchSet([{ key: 'config1', data: { theme: 'dark' } },{ key: 'config2', data: { lang: 'zh' } }
]);
规则:失败时回滚,确保一致性。适用于配置批量更新,在初始化多模块时很实用。
四、实际应用场景
本节通过 4 个典型场景,展示缓存的实战价值。每场景包含需求、实现和优化。
4.1 用户登录状态管理
需求:登录后保存 Token 和用户信息,退出时清除;重启 App 自动验证。
实现:
- 登录页(login.vue):
login() {// 模拟 APIconst user = { id: 1, name: '李四', token: 'def456' };uni.setStorageSync('user_token', user.token);uni.setStorageSync('user_info', JSON.stringify(user));uni.switchTab({ url: '/pages/home/home' });
}
- Home 页(home.vue):
onLoad() {const token = uni.getStorageSync('user_token');if (token) {const userStr = uni.getStorageSync('user_info');this.user = userStr ? JSON.parse(userStr) : null;} else {uni.reLaunch({ url: '/pages/login/login' });}
},
methods: {logout() {uni.removeStorageSync('user_token');uni.removeStorageSync('user_info');uni.reLaunch({ url: '/pages/login/login' });}
}
优化:集成过期(3.1),每 7 天重登。
在实际项目中,可添加 JWT 解析验证 Token 有效性:
// 验证 Token
function isTokenValid(token) {try {const payload = JSON.parse(atob(token.split('.')[1]));return Date.now() < payload.exp * 1000;} catch (e) {return false;}
}
这确保安全,防止过期 Token 误用。在社交 App 中,这种机制让用户体验更流畅。
4.2 配置信息持久化
需求:保存主题色、语言等设置,重启生效。
实现:
- 设置页(setting.vue):
changeTheme(color) {uni.setStorageSync('app_theme', color);// 动态更新全局样式(需 CSS 变量)const dom = document.documentElement;dom.style.setProperty('--theme-color', color);
}
onLoad() {const savedTheme = uni.getStorageSync('app_theme') || '#007AFF';// 应用主题
}
优化:用 getStorageInfo 监控,超 1MB 提示清理。结合 Vuex(3.2),实现响应式切换。
扩展:多语言支持:
// 存储语言包
uni.setStorageSync('lang_pack', { zh: { hello: '你好' }, en: { hello: 'Hello' } });
const lang = uni.getStorageSync('current_lang') || 'zh';
this.messages = uni.getStorageSync('lang_pack')[lang];
这在国际化 App 中实用,减少 bundle 大小。用户切换语言时,立即生效,无需重启。
4.3 离线数据缓存
需求:缓存 API 返回的文章列表,支持无网浏览。
实现(使用 3.1 过期):
// 获取列表
async fetchArticles() {const cached = getWithExpire('articles');if (cached) {this.articles = cached;return;}try {const res = await uni.request({ url: '/api/articles' });setWithExpire('articles', res.data, 24 * 60 * 60 * 1000); // 24小时this.articles = res.data;} catch (e) {console.error('网络错误,使用缓存');this.articles = cached || [];}
}
优化:分页缓存,如 ‘articles_page_1’。添加网络状态检测:
uni.getNetworkType({success: (res) => {if (res.networkType === 'none') {// 仅用缓存}}
});
这提升了鲁棒性,在弱网环境闪光。新闻类 App 中,用户地铁上也能阅读缓存内容。
4.4 列表分页缓存
需求:电商商品列表,分页加载,缓存每页数据。
实现:
loadPage(page = 1) {const key = `goods_page_${page}`;const cached = uni.getStorageSync(key);if (cached) {this.goods = [...this.goods, ...cached];return;}uni.request({url: `/api/goods?page=${page}`,success: (res) => {uni.setStorageSync(key, res.data);this.goods = [...this.goods, ...res.data];}});
}
优化:预加载下一页,移除旧页(如 >10 页清空)。结合 getStorageInfo,避免总大小超限。
扩展讨论:在高并发场景,可用 IndexedDB(H5 专用,条件编译)补充大列表缓存:
<!-- #ifdef H5 -->
<script>
import { openDB } from 'idb'; // npm idb
const db = await openDB('goods_db', 1, {upgrade(db) {db.createObjectStore('pages');}
});
await db.put('pages', data, `page_${page}`);
</script>
<!-- #endif -->
这在 Web 端处理海量数据时高效,结合缓存层,能实现混合存储策略。
五、性能优化与最佳实践
5.1 同步 vs 异步选择
- 异步优先:95% 场景用回调/Promise,避免 UI 阻塞。示例:onLoad 中异步 get。
- 同步场景:启动时快速恢复,或简单工具函数。
- 最佳实践:封装 Promise 化异步:
function setStoragePromise(key, data) {return new Promise((resolve, reject) => {uni.setStorage({key, data,success: resolve,fail: reject});});
}
// 使用 await setStoragePromise('key', data);
在我的项目中,统一用 Promise,能让 async/await 代码更优雅。
5.2 存储大小管理
- 监控:定期调用 getStorageInfo,若 currentSize > 80% limitSize,清理旧数据。
- 压缩:大对象用 lz-string 库压缩(npm 引入)。
- 实践:设置页添加“清理缓存”按钮:
clearOld() {const res = uni.getStorageInfoSync();if (res.currentSize > res.limitSize * 0.8) {// 移除 7 天前数据(结合 3.1)res.keys.forEach(key => {if (getWithExpire(key) === null) uni.removeStorageSync(key);});}
}
定期清理能保持 App 轻量,尤其在低端设备上。
5.3 错误处理与调试
- 统一日志:用 console.error + uni.reportEvent 上报。
- 调试工具:HBuilderX 控制台查看 Storage,Chrome DevTools 检查 H5 localStorage。
- 实践:添加 try-catch 包装所有操作,fail 回调 toast 提示。遇到序列化错误时,fallback 到字符串存储。
5.4 跨平台兼容
- 条件编译:#ifdef APP 用 plus.storage 扩展。
- 测试:多端运行,关注小程序审核(如支付宝 10MB 限)。
- 实践:manifest.json 中配置 storage 权限。在 CI/CD 中集成多端测试脚本。
5.5 清理策略
- 自动:应用退出时 clear(可选)。
- 手动:用户设置页一键清。
- 智能:基于使用频率,LRU(最近最少用)算法移除(自定义 Map 实现)。
示例 LRU:
class LRUCache {constructor(maxSize) {this.maxSize = maxSize;this.cache = new Map();}set(key, value) {if (this.cache.has(key)) this.cache.delete(key);this.cache.set(key, value);if (this.cache.size > this.maxSize) {const firstKey = this.cache.keys().next().value;this.cache.delete(firstKey);}}get(key) {if (this.cache.has(key)) {const value = this.cache.get(key);this.cache.delete(key);this.cache.set(key, value);return value;}return null;}
}
用它包装 Storage,能智能管理热数据。
六、常见问题与解决方案
6.1 常见错误
- 序列化失败:data 含不可序列化类型。
解:预检查JSON.stringify(data)不抛错。 - Key 冲突:用系统前缀。
解:命名规范,如 ‘app_user_token’。 - 读取 undefined:key 不存在。
解:默认值|| {}。
6.2 平台特定问题
- H5 缓存丢失:浏览器隐私模式。
解:fallback 到 sessionStorage。 - 小程序超限:微信 10MB。
解:分 key 存储,定期 getStorageInfo 检查。 - App 无限制滥用:占用设备空间。
解:设置软上限 50MB,自行清理。
6.3 调试技巧
- 打印 keys:循环 getStorageSync(key) 查看内容。
- 模拟清理:clearStorageSync() + 重载。
- 工具:UniApp 插件“Storage Viewer”。在开发时,添加一个调试模式,暴露所有 keys 到控制台。
七、扩展与进阶
7.1 与其他存储结合
- SQLite(App 端):复杂数据用 uni.requireNativePlugin(‘sqlite’)。
示例:用户订单表,缓存仅存 ID 列表。 - IndexedDB(H5):大文件缓存。
实践:图片缩略图存 IndexedDB,详情存 Storage。混合使用能平衡性能和容量。
7.2 云端同步
- 原理:本地变更时上传云(uniCloud),拉取时合并。
- 示例:登录后 syncCacheToCloud(),用 diff 算法避免覆盖。
async syncCache() {const localKeys = uni.getStorageInfoSync().keys;const cloudData = await uniCloud.callFunction({ name: 'getCache' });localKeys.forEach(key => {if (!cloudData.has(key)) {uniCloud.callFunction({ name: 'uploadCache', data: { key, value: uni.getStorageSync(key) } });}});
}
这在多设备同步场景中强大,如笔记 App。
7.3 自定义缓存模块
- 基于 Redux-like:状态 + reducer + storage middleware。
- 进阶:支持 RxJS 响应式缓存更新。示例:订阅缓存变化,自动 UI 刷新。
// 用 RxJS
import { fromEvent } from 'rxjs';
const storageEvent = fromEvent(window, 'storage');
storageEvent.subscribe(event => {if (event.key === 'my_key') {// 更新状态}
});
这让缓存成为响应式数据源。
