一、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 页面中):
javascript
// 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 处理异常。
示例:
javascript
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 }
示例:
javascript
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)。
示例:
javascript
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 | 否 | 结束回调 |
示例:
javascript
removeData() {
uni.removeStorage({
key: 'user_info',
success: () => {
console.log('移除成功');
uni.showToast({ title: '缓存已清除' });
}
});
}
同步版本:uni.removeStorageSync(key)
示例:
javascript
try {
uni.removeStorageSync('user_info');
console.log('同步移除成功');
} catch (e) {
console.error('移除失败:', e);
}
注意事项:移除后,相关页面需刷新数据源,避免显示旧值。结合事件总线,能通知其他组件更新。
2.4 清空数据:uni.clearStorage / uni.clearStorageSync
原理:移除所有缓存键值对,慎用!
异步版本:uni.clearStorage(OBJECT)
参数:success/fail/complete(可选)。
示例:
javascript
clearAll() {
uni.clearStorage({
success: () => {
console.log('全部清空成功');
// 跳转登录页
uni.reLaunch({ url: '/pages/login/login' });
}
});
}
同步版本:uni.clearStorageSync()
示例:
javascript
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 |
示例:
javascript
getInfo() {
uni.getStorageInfo({
success: (res) => {
console.log('键列表:', res.keys);
console.log('占用:', res.currentSize + 'KB / ' + res.limitSize + 'KB');
}
});
}
同步版本:uni.getStorageInfoSync()
示例:
javascript
try {
const res = uni.getStorageInfoSync();
console.table(res); // 表格输出
} catch (e) {
console.error(e);
}
应用:在设置页显示存储使用率,提示用户清理。定期调用能帮助诊断内存问题。
通过这些基础 API,开发者可快速实现简单缓存。接下来,我们探讨如何在复杂场景中扩展它们。
三、高级用法与技巧
基础 API 虽强大,但实际项目中需处理过期、集成和安全等问题。本节提供一些实用技巧,帮助你构建更robust的缓存系统。
3.1 缓存过期机制实现
原理:UniApp 无内置过期,需手动添加时间戳判断。
实现步骤:
- 存储时附加 timestamp: Date.now()。
- 读取时计算差值,超阈值则移除并返回 null。
示例(工具函数):
javascript
// 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;
}
}
使用:
javascript
// 存储
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):
javascript
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 中调用:
javascript
import store from './store';
Vue.prototype.$store = store;
store.dispatch('initFromCache'); // App 启动时执行
Pinia 变体:类似,使用 defineStore 的 persist 插件(社区扩展)。
规则:仅持久关键状态,避免缓存大对象。在大型项目中,这种集成能让状态管理更可靠,用户重启 App 后无缝继续。
3.3 全局缓存管理工具封装
原理:统一入口,简化多处调用,支持命名空间。
示例(utils/storageManager.js):
javascript
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/小程序通用):
javascript
// 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 模拟批量。
示例:
javascript
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):
javascript
login() {
// 模拟 API
const 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):
javascript
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 有效性:
javascript
// 验证 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):
javascript
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),实现响应式切换。
扩展:多语言支持:
javascript
// 存储语言包
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 过期):
javascript
// 获取列表
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'。添加网络状态检测:
javascript
uni.getNetworkType({
success: (res) => {
if (res.networkType === 'none') {
// 仅用缓存
}
}
});
这提升了鲁棒性,在弱网环境闪光。新闻类 App 中,用户地铁上也能阅读缓存内容。
4.4 列表分页缓存
需求:电商商品列表,分页加载,缓存每页数据。
实现:
javascript
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 专用,条件编译)补充大列表缓存:
vue
<!-- #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 化异步:
javascript
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 引入)。
- 实践:设置页添加"清理缓存"按钮:
javascript
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:
javascript
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 算法避免覆盖。
javascript
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 刷新。
javascript
// 用 RxJS
import { fromEvent } from 'rxjs';
const storageEvent = fromEvent(window, 'storage');
storageEvent.subscribe(event => {
if (event.key === 'my_key') {
// 更新状态
}
});
这让缓存成为响应式数据源。