1. 为什么你的uniapp需要数据缓存?
如果你刚开始接触uniapp开发,可能会觉得数据缓存这个概念有点抽象。别急,我打个比方你就明白了。想象一下你每天上班都要用到的工牌,你肯定不会每天早上去人事部重新办一张,而是把它放在一个固定的地方,比如家里的玄关或者随身包里,要用的时候直接拿。uniapp里的数据缓存,就是这个“固定的地方”。
在移动端或小程序开发里,数据缓存太重要了。它能帮你把一些不经常变化、但又需要频繁使用的数据“存”在用户的设备本地。比如用户的登录状态(token)、个人偏好设置(主题、语言)、上次浏览的商品列表,甚至是表单的草稿。每次用户打开你的应用,你直接从本地读取这些信息,应用瞬间就“活”过来了,体验非常流畅。如果每次都去服务器请求,不仅慢,还浪费流量,在弱网环境下体验更是灾难。
我刚开始做项目时也犯过懒,所有数据都实时请求,结果用户一进页面就转圈圈,被吐槽得不行。后来老老实实把用户信息、城市列表这些基础数据做了缓存,应用的响应速度立竿见影。uniapp提供了一套非常完整的缓存API,从存、取、删到一键清空,都有同步和异步两种方式,基本覆盖了所有开发场景。接下来,我就带你从最基础的存储开始,一步步把这些API的用法、坑点以及我实战中总结的经验,掰开揉碎了讲给你听。
2. 第一步:如何把数据稳稳地“存”进去?
存储数据是缓存操作的起点。uniapp提供了两种方式:uni.setStorage(异步)和 uni.setStorageSync(同步)。别看只是“同步”和“异步”两个词的差别,用错了场景,效果天差地别。
2.1 异步存储:uni.setStorage(OBJECT)
异步存储,顾名思义,就是“不同步”。当你调用这个API时,代码不会傻等着它存完,而是立刻继续执行后面的语句。等数据真正存好了,它会通过你预先设置好的回调函数来通知你。这就像你去快递柜寄件,扫码下单后,柜门弹开,你把东西放进去关上,就可以走了(代码继续执行)。至于快递柜什么时候完成内部处理和上传数据(存储完成),系统稍后会发条短信(回调函数)告诉你“寄存成功”。
它的标准写法是这样的:
uni.setStorage({
key: 'user_token', // 存储的键名,相当于一个标签
data: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', // 要存的数据,可以是字符串、数字、对象、数组等
success: function () {
console.log('用户token存储成功!');
// 存储成功后,可以在这里进行后续操作,比如跳转页面
},
fail: function (err) {
console.error('存储失败:', err);
// 失败处理,比如提示用户
},
complete: function () {
console.log('存储操作完成(无论成功或失败都会执行)');
}
});
我踩过的坑和经验:
key是字符串:这是你取回数据的唯一凭证,最好用有意义的英文命名,比如user_profile、app_settings。别用中文,容易出编码问题。data几乎什么都能存:字符串、数字、布尔值、对象、数组都行。但要注意,如果你存一个对象,取出来的时候它还是对象,不需要你手动JSON.parse,uniapp内部帮你处理了。不过,我建议对于复杂对象,存之前用JSON.stringify()转成字符串也没问题,更可控。- 异步回调的时机:
success回调不代表数据已经100%物理写入磁盘(在某些平台如小程序中,它只是表示调用成功并进入了系统队列)。所以,如果你在success里立刻进行极度依赖此数据的操作,理论上仍有极小风险。但对于绝大多数业务场景(如存token后跳转),这个时机是完全够用的。 - 适合的场景:当你存储数据的操作不阻塞后续UI渲染或用户交互时。比如用户登录成功后存储token,然后跳转到首页。存储过程在后台悄悄进行,不影响用户看到跳转动画。
2.2 同步存储:uni.setStorageSync(KEY, DATA)
同步存储就“霸道”多了。代码执行到这里,必须等数据完完整整地存好了,它才会放行,继续执行下一行。这就像在银行柜台存钱,你必须等柜员清点完毕、盖章确认、把回执交给你,整个流程结束后,你才能离开柜台去做下一件事。
它的写法非常简洁:
try {
uni.setStorageSync('user_profile', {
name: '张三',
age: 28,
avatar: 'https://example.com/avatar.jpg'
});
console.log('用户资料同步存储成功!');
// 存储成功后,紧接着的代码可以放心使用这个数据
const profile = uni.getStorageSync('user_profile');
console.log(profile.name); // 输出:张三
} catch (e) {
// 错误处理
console.error('同步存储发生错误:', e);
}
关键点和注意事项:
try...catch必不可少:同步API一旦出错(比如存储空间已满、传入非法数据等),会直接抛出异常,如果不捕获,会导致整个程序崩溃。用try...catch包裹是良好的编程习惯。- 会阻塞主线程:这是同步API最大的特点,也是最大的风险点。如果存储的数据量很大(比如好几兆的文本),或者设备存储速度很慢,用户就会明显感觉到界面“卡住”了,点击没反应。所以,绝对不要在渲染页面、响应用户点击等UI线程的核心操作中,使用同步方式存储大数据。
- 适合的场景:在应用启动初始化、数据量很小(如一个标志位
is_first_launch)、或者某些必须确保存储顺序的逻辑中。代码逻辑更直观,没有回调地狱。
异步 vs 同步,到底怎么选?
简单来说,记住一个原则:大多数时候用异步,只在确定数据量小且不介意短暂阻塞时用同步。异步更安全,不会卡住界面;同步代码更简洁。在实际项目中,我90%的情况都用 uni.setStorage,只有存一些简单的开关状态时,才会考虑用同步。
3. 第二步:如何准确地把数据“取”出来?
存好了数据,接下来就是关键时刻——取用。取不出来,或者取错了,前面都白干。uniapp对应的获取API是 uni.getStorage 和 uni.getStorageSync。
3.1 异步获取:uni.getStorage(OBJECT)
和异步存储一样,获取数据也是“等通知”模式。你发起一个获取请求,然后代码继续跑,等系统找到数据后,通过回调函数交给你。
uni.getStorage({
key: 'shopping_cart',
success: function (res) {
// res.data 就是你当初存进去的数据
console.log('购物车数据:', res.data);
// 拿到数据后,更新页面状态
this.cartList = res.data || [];
},
fail: function (err) {
// 如果key不存在,或者其他错误,会进入这里
console.log('未找到购物车数据或读取失败', err);
this.cartList = []; // 初始化为空数组
}
});
实战中的细节:
res.data的类型:它和你存进去时的类型保持一致。如果你存的是对象,取出来直接就是对象,可以直接res.data.property访问属性。- 处理“空”状态:这是最容易出错的地方。如果指定的
key从来没有被存储过,或者已经被移除,success回调依然会执行,但res.data会是一个空字符串''或其他形式的空值(不同平台底层实现略有差异)。所以,永远不要直接假设res.data有效,一定要做判断。success: function (res) { if (res.data) { // 有数据,正常处理 } else { // 数据为空,进行初始化 } } fail回调:通常只在发生真正的错误时触发,比如传入的key格式非法、存储系统异常等。单纯的“key不存在”不一定会走fail,所以数据校验逻辑主要放在success里。
3.2 同步获取:uni.getStorageSync(KEY)
同步获取直接返回数据,一步到位。
try {
const cityList = uni.getStorageSync('city_list');
if (cityList && cityList.length > 0) {
// 缓存中有城市数据,直接使用
this.cityData = cityList;
console.log('使用本地城市缓存');
} else {
// 缓存中没有,去网络请求
this.fetchCityDataFromServer();
}
} catch (e) {
console.error('读取缓存时发生错误:', e);
// 出错也降级为网络请求
this.fetchCityDataFromServer();
}
同步获取的黄金法则:
- 一定要判空:
const value = uni.getStorageSync('key')。如果key不存在,value可能是undefined、null或空字符串。后续操作前必须判断。 - 配合
try...catch:虽然读取失败的概率比写入低,但为了代码健壮性,尤其在一些关键流程(如读取登录态判断是否跳转登录页),加上错误捕获是稳妥的做法。 - 性能考虑:同步读取虽然会阻塞,但时间极短(毫秒级),对性能影响微乎其微。因此,在页面初始化、组件创建等需要立即使用缓存数据的地方,同步获取是更常见、也更推荐的选择,因为它能让代码逻辑更清晰、更线性。
一个综合案例:用户登录态校验 假设我们有一个需求:应用启动时,检查本地是否有登录token,有则直接进入首页,无则跳转登录页。
// 在App.vue的onLaunch中,使用同步获取更合适
onLaunch: function() {
try {
const token = uni.getStorageSync('user_token');
if (token) {
// 有token,验证其有效性(可能需要向服务器发一个轻量请求)
this.checkTokenValid(token).then(valid => {
if (valid) {
uni.switchTab({ url: '/pages/home/home' });
} else {
uni.redirectTo({ url: '/pages/login/login' });
}
});
} else {
// 无token,直接去登录
uni.redirectTo({ url: '/pages/login/login' });
}
} catch (e) {
console.error('启动时读取token失败', e);
// 读取出错,保守起见,导向登录页
uni.redirectTo({ url: '/pages/login/login' });
}
}
在这个场景里,用同步获取可以让后续的跳转逻辑基于一个确定的结果来执行,代码流程一目了然。
4. 第三步:如何管理缓存——移除与清空
数据缓存不是只存不删的仓库,需要定期清理和管理。比如用户退出登录,就需要移除他的个人token;或者应用版本升级,数据结构变了,需要清空所有旧缓存。uniapp提供了针对单个key的移除和全部清空两种方法。
4.1 移除指定缓存:uni.removeStorage 与 uni.removeStorageSync
当你不再需要某个特定的缓存项时,就用移除操作。
异步移除:
// 用户点击退出登录时
handleLogout() {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
// 异步移除token
uni.removeStorage({
key: 'user_token',
success: () => {
console.log('token移除成功');
// 移除成功后,跳转到登录页
uni.reLaunch({ url: '/pages/login/login' });
},
fail: (err) => {
console.error('token移除失败', err);
// 即使移除失败,也强制跳转,但可以给个提示
uni.showToast({ title: '退出异常', icon: 'none' });
uni.reLaunch({ url: '/pages/login/login' });
}
});
}
}
});
}
异步移除通常用在用户主动触发的、需要明确知晓结果的操作中,比如退出登录。你可以根据移除成功或失败,给用户不同的反馈。
同步移除:
// 在某个工具函数中,清理过期的临时数据
function clearExpiredTempData() {
const tempKeys = ['temp_form_data', 'draft_post', 'search_history_temp'];
tempKeys.forEach(key => {
try {
uni.removeStorageSync(key);
console.log(`已移除临时数据: ${key}`);
} catch (e) {
// 这里错误通常可以忽略,或者记录日志
console.warn(`移除 ${key} 时发生小问题:`, e);
}
});
}
同步移除代码简洁,适合在明确的、无需用户感知的后台清理任务中批量执行。同样,用 try...catch 包裹可以避免因某个key移除失败而中断整个清理流程。
注意一个“坑”:移除一个不存在的 key,不会报错。无论是异步还是同步,都会正常执行。所以你不必在移除前先检查该key是否存在。
4.2 清空所有缓存:uni.clearStorage 与 uni.clearStorageSync
这是“核弹”级别的操作,会清除所有通过 uni.setStorage API 存储的数据,请慎用。常见场景是:用户主动清理缓存、应用重大版本更新需要重置数据、或者在测试环境下。
异步清空:
// 设置在应用的“清理缓存”功能里
clearAllCache() {
uni.showModal({
title: '警告',
content: '此操作将清除所有本地数据(如登录状态、个人设置),确定继续?',
success: (res) => {
if (res.confirm) {
uni.showLoading({ title: '清理中...' });
uni.clearStorage({
success: () => {
uni.hideLoading();
uni.showToast({ title: '缓存清理成功', icon: 'success' });
// 清空后,通常需要重启应用或跳转到初始状态
setTimeout(() => {
uni.reLaunch({ url: '/pages/index/index' });
}, 1500);
},
fail: (err) => {
uni.hideLoading();
uni.showToast({ title: '清理失败', icon: 'none' });
console.error('清空缓存失败:', err);
}
});
}
}
});
}
同步清空:
// 在应用版本升级检测逻辑中
if (newVersion !== oldVersion) {
console.log('检测到新版本,清空旧缓存...');
try {
uni.clearStorageSync();
console.log('旧缓存已清空');
// 然后可以存储新版本号,并初始化新数据
uni.setStorageSync('app_version', newVersion);
this.initAppData();
} catch (e) {
console.error('清空缓存时发生错误,可能影响新版本使用', e);
}
}
重要警告:
- 不可逆操作:
clearStorage会清除全部缓存,包括用户的登录凭证、个人配置等。除非是明确的设计需求,否则不要轻易在程序逻辑中自动调用它。 - 区分缓存:uniapp的缓存系统是独立的,它不会清除你通过其他方式(如
uni.downloadFile下载的文件)存储的内容。那些有自己独立的管理方式。 - 用户提示:如果功能面向用户,务必在清空前给出明确、强烈的确认提示,防止误操作导致用户数据丢失。
5. 实战进阶:封装一个健壮的缓存管理工具
直接调用原生API虽然简单,但在大型项目中,散落的缓存操作代码会难以维护,也容易出错。我习惯封装一个统一的缓存工具类,好处是:统一错误处理、添加通用逻辑(如自动序列化/反序列化)、方便后期扩展和替换底层实现。
下面分享一个我项目中常用的缓存工具 storage.js 的简化版:
// utils/storage.js
const storage = {
// ==================== 同步方法 ====================
/**
* 同步设置缓存
* @param {string} key 键
* @param {any} data 值,如果是对象或数组会自动序列化
* @returns {boolean} 是否成功
*/
setSync(key, data) {
try {
// 对于非基本类型,可以统一转成JSON字符串存储,确保一致性
const saveData = typeof data === 'object' ? JSON.stringify(data) : data;
uni.setStorageSync(key, saveData);
return true;
} catch (e) {
console.error(`[Storage] 设置缓存 "${key}" 失败:`, e);
// 可以在这里上报错误日志
return false;
}
},
/**
* 同步获取缓存
* @param {string} key 键
* @param {any} defaultValue 获取失败或为空时的默认值
* @returns {any} 缓存的值或默认值
*/
getSync(key, defaultValue = null) {
try {
const value = uni.getStorageSync(key);
// 处理取出的值:如果是JSON字符串,尝试解析
if (value && typeof value === 'string') {
try {
return JSON.parse(value);
} catch (e) {
// 解析失败,说明它本来就是字符串,直接返回
return value;
}
}
// 如果value是null、undefined、''等,返回默认值
return value != null ? value : defaultValue;
} catch (e) {
console.error(`[Storage] 获取缓存 "${key}" 失败:`, e);
return defaultValue;
}
},
/**
* 同步移除缓存
* @param {string} key 键
* @returns {boolean} 是否成功(即使key不存在也返回true)
*/
removeSync(key) {
try {
uni.removeStorageSync(key);
return true;
} catch (e) {
console.error(`[Storage] 移除缓存 "${key}" 失败:`, e);
return false;
}
},
// ==================== 异步方法 (返回Promise) ====================
/**
* 异步设置缓存 (Promise版本)
* @param {string} key 键
* @param {any} data 值
* @returns {Promise<boolean>}
*/
set(key, data) {
return new Promise((resolve, reject) => {
const saveData = typeof data === 'object' ? JSON.stringify(data) : data;
uni.setStorage({
key,
data: saveData,
success: () => resolve(true),
fail: (err) => {
console.error(`[Storage] 异步设置 "${key}" 失败:`, err);
reject(err);
}
});
});
},
/**
* 异步获取缓存 (Promise版本)
* @param {string} key 键
* @param {any} defaultValue 默认值
* @returns {Promise<any>}
*/
get(key, defaultValue = null) {
return new Promise((resolve) => {
uni.getStorage({
key,
success: (res) => {
let finalData = res.data;
if (finalData && typeof finalData === 'string') {
try {
finalData = JSON.parse(finalData);
} catch (e) {
// 保持为字符串
}
}
resolve(finalData != null ? finalData : defaultValue);
},
fail: (err) => {
console.warn(`[Storage] 异步获取 "${key}" 失败(可能不存在):`, err);
resolve(defaultValue); // 异步获取失败,也返回默认值,不reject
}
});
});
},
/**
* 异步移除缓存 (Promise版本)
* @param {string} key 键
* @returns {Promise<boolean>}
*/
remove(key) {
return new Promise((resolve) => {
uni.removeStorage({
key,
success: () => resolve(true),
fail: (err) => {
console.error(`[Storage] 异步移除 "${key}" 失败:`, err);
resolve(false); // 移除失败也返回false,不阻断流程
}
});
});
},
/**
* 清空所有缓存
* @returns {Promise<boolean>}
*/
clear() {
return new Promise((resolve) => {
uni.clearStorage({
success: () => resolve(true),
fail: (err) => {
console.error('[Storage] 清空所有缓存失败:', err);
resolve(false);
}
});
});
}
};
export default storage;
这个工具类怎么用?
-
引入工具:
import storage from '@/utils/storage.js'; -
同步方法使用示例:
// 存 const isSuccess = storage.setSync('theme', 'dark'); // 取 const user = storage.getSync('user_info', { name: 'Guest' }); // 删 storage.removeSync('temp_data'); -
异步方法使用示例 (配合async/await):
async function loginAndSaveToken(token) { try { await storage.set('auth_token', token); console.log('Token保存成功'); // 保存成功后继续其他操作 await fetchUserProfile(); } catch (error) { console.error('登录流程出错', error); } }
封装带来的好处:
- 一致性:所有缓存操作都有统一的日志输出和错误处理逻辑。
- 安全性:通过
try...catch和Promise的catch,避免了未处理的异常导致程序崩溃。 - 易用性:自动处理了对象的序列化和反序列化,调用者无需关心
JSON.stringify和JSON.parse。 - 可维护性:如果未来uniapp缓存API有变化,或者你想增加额外功能(比如给缓存加过期时间),只需要修改这个工具类即可,所有调用它的地方都自动生效。
6. 避坑指南与最佳实践
掌握了基本操作和封装技巧,最后再分享几个我踩过坑才总结出来的实战经验,能帮你省下不少调试时间。
1. 注意存储大小限制 不同平台对本地缓存有大小限制(通常小程序是10MB)。虽然不大可能存满,但如果你缓存了大量图片的Base64编码或历史记录,就需要警惕。可以在设置缓存前判断一下数据大小:
function getDataSize(data) {
const str = typeof data === 'string' ? data : JSON.stringify(data);
return new Blob([str]).size; // 近似字节大小
}
const dataSize = getDataSize(bigData);
if (dataSize > 5 * 1024 * 1024) { // 比如限制单条不超过5MB
console.warn('缓存数据过大,可能导致存储失败');
// 可以考虑压缩、分片或放弃缓存
}
2. 缓存数据的版本管理 如果你的应用数据结构会随着版本升级而变化,直接读取旧缓存可能会出错。一个简单的办法是给缓存数据加上版本号。
// 存储时带上版本
storage.setSync('user_data_v1.2', { ...userData });
// 读取时检查版本
const savedVersion = '1.2';
const currentVersion = '1.3';
if (savedVersion !== currentVersion) {
// 版本不同,清除旧缓存
storage.removeSync('user_data_v1.2');
// 然后从服务器获取新格式的数据
}
3. 敏感信息不要明文缓存 切记,千万不要将用户的密码、支付密码等极度敏感的信息明文存储在本地缓存中。缓存相对容易通过设备文件系统被探查。Token之类的凭证可以存,但如果是特别敏感的数据,建议使用更安全的存储方式,或者只在内存中暂存。
4. 异步操作的顺序问题 如果你连续进行多个异步缓存操作(比如先删A,再存B,最后读C),由于它们是异步的,执行完成的顺序无法保证。如果操作之间有依赖关系,必须用回调或Promise链来确保顺序。
// 错误示范:顺序无法保证
uni.removeStorage({ key: 'old' });
uni.setStorage({ key: 'new', data: 'value' });
const val = uni.getStorageSync('new'); // 这里可能读不到!
// 正确做法:用Promise链或async/await
async function updateData() {
await storage.remove('old');
await storage.set('new', 'value');
const val = await storage.get('new'); // 这里一定能读到
}
5. 分清“移除”和“清空”的使用场景
removeStorage:用于业务逻辑。如用户退出登录移除token,商品移除收藏。clearStorage:用于管理或重置。如用户手动点击“清理缓存”,开发者调试时重置应用状态。
把这些点记在心里,你在使用uniapp数据缓存时就能避开大多数常见的陷阱。说到底,缓存的核心目的是为了提升体验,在速度、空间和安全性之间找到一个平衡点。多思考“这个数据真的需要缓存吗?”、“缓存多久合适?”,你的应用会变得更加稳健和高效。
4733



被折叠的 条评论
为什么被折叠?



