UniApp 小程序本地缓存、持久化存储最佳实践
前端缓存用不好,小程序迟早要卡爆。本文聚焦 UniApp 框架,手把手教你用对 Storage、躲坑、提性能。
一、UniApp 存储全家桶(跨端兼容)
| | | | |
|---|
| uni.setStorage() | | | |
| uni.getStorage() | | | |
| uni.setStorageSync() | | | |
| uni.getStorageSync() | | | |
| uni.removeStorage() | | | |
| uni.clearStorage() | | | |
| uni.getStorageInfo() | | | |
// 异步 - 推荐uni.setStorage({ key: 'userInfo', data: { name: '张三', age: 25 }, success() { console.log('存储成功') }})// 同步uni.setStorageSync('token', 'abc123')// 读取uni.getStorage({ key: 'userInfo', success(res) { console.log(res.data) }})// 同步读取const token = uni.getStorageSync('token')// 删除单个uni.removeStorage({ key: 'tempData' })// 清空全部uni.clearStorage()// 获取存储信息(已用容量、keys列表等)uni.getStorageInfo({ success(res) { console.log('已用:', res.currentSize, 'KB') console.log('限制:', res.limitSize, 'KB') console.log('所有keys:', res.keys) }})
⚠️ 跨端容量差异
二、同步 vs 异步:什么时候用什么?
// ❌ 错误示范:同步阻塞渲染onLoad() { const data = uni.getStorageSync('hugeData') // 大数据会卡住页面 this.processData(data)}// ✅ 正确做法:异步不阻塞onLoad() { uni.getStorage({ key: 'hugeData', success: (res) => { this.processData(res.data) } })}// 或者用 Promise 封装(推荐)const getStorage = (key) => new Promise((resolve, reject) => { uni.getStorage({ key, success: res => resolve(res.data), fail: err => reject(err) })})// async/await 用起来更爽async onLoad() { const data = await getStorage('hugeData') this.processData(data)}
选型原则
三、Storage 进阶用法(UniApp 封装)
1. 存储对象/数组(必须 JSON 序列化)
// ❌ 错误:直接存对象uni.setStorageSync('user', { name: '张三' })const user = uni.getStorageSync('user')console.log(typeof user.name) // "undefined" - 对象变成了字符串 "[object Object]"// ✅ 正确:JSON.stringify / parseuni.setStorageSync('user', JSON.stringify({ name: '张三' }))const user = JSON.parse(uni.getStorageSync('user'))console.log(user.name) // "张三"
2. 统一工具类 storage.js(核心模板)
// storage.js - 跨端兼容封装const STORAGE_PREFIX = 'app_'/** * 存储(自动 JSON 序列化) */function set(key, value, expire = 0) { const data = { value, expire: expire > 0 ? Date.now() + expire : 0 // 0 表示永不过期 } // #ifdef MP-WEIXIN try { uni.setStorageSync(STORAGE_PREFIX + key, JSON.stringify(data)) } catch (e) { console.error('存储失败:', e) } // #endif // #ifdef H5 try { localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(data)) } catch (e) { console.error('localStorage 超出限制:', e) } // #endif // #ifdef APP-PLUS try { plus.storage.setItem(STORAGE_PREFIX + key, JSON.stringify(data)) } catch (e) { console.error('App存储失败:', e) } // #endif}/** * 读取(自动过期校验) */function get(key, defaultValue = null) { let raw = null // #ifdef MP-WEIXIN try { raw = uni.getStorageSync(STORAGE_PREFIX + key) } catch (e) {} // #endif // #ifdef H5 raw = localStorage.getItem(STORAGE_PREFIX + key) // #endif // #ifdef APP-PLUS try { raw = plus.storage.getItem(STORAGE_PREFIX + key) } catch (e) {} // #endif if (!raw) return defaultValue try { const data = JSON.parse(raw) // 过期校验 if (data.expire && Date.now() > data.expire) { remove(key) return defaultValue } return data.value } catch (e) { return defaultValue }}/** * 删除 */function remove(key) { // #ifdef MP-WEIXIN uni.removeStorage({ key: STORAGE_PREFIX + key }) // #endif // #ifdef H5 localStorage.removeItem(STORAGE_PREFIX + key) // #endif // #ifdef APP-PLUS plus.storage.removeItem(STORAGE_PREFIX + key) // #endif}/** * 清空用户数据(退出登录用) */function clearUserData() { const keys = getAllKeys() keys.forEach(key => { if (key.startsWith(STORAGE_PREFIX + 'user') || key.startsWith(STORAGE_PREFIX + 'token')) { remove(key.replace(STORAGE_PREFIX, '')) } })}/** * 获取所有 keys */function getAllKeys() { // #ifdef MP-WEIXIN try { const info = uni.getStorageInfoSync() return info.keys || [] } catch (e) { return [] } // #endif // #ifdef H5 return Object.keys(localStorage) // #endif // #ifdef APP-PLUS return plus.storage.getAllKeys() || [] // #endif}/** * 获取已用存储大小(KB) */function getUsedSize() { // #ifdef MP-WEIXIN try { const info = uni.getStorageInfoSync() return info.currentSize } catch (e) { return 0 } // #endif // #ifdef H5 let size = 0 for (let key in localStorage) { if (localStorage.hasOwnProperty(key)) { size += localStorage[key].length * 2 // 中文占2字节 } } return Math.ceil(size / 1024) // #endif return 0}export default { set, get, remove, clearUserData, getAllKeys, getUsedSize}
3. 存储监听器(变化时自动刷新)
// pages/index/index.vueexport default { data() { return { userInfo: null } }, onLoad() { // 初始化读取 this.loadUserInfo() // #ifdef MP-WEIXIN // 微信小程序使用 watch this.$watch('userInfo', (newVal) => { console.log('userInfo 变化了:', newVal) }) // #endif }, methods: { loadUserInfo() { // 读取缓存 const cache = storage.get('userInfo') if (cache) { this.userInfo = cache } // 异步加载最新 this.fetchUserInfo() }, async fetchUserInfo() { try { const res = await uni.request({ url: '/api/user/info' }) this.userInfo = res.data // 更新缓存 storage.set('userInfo', res.data) } catch (e) { console.error('获取用户信息失败', e) } } }}
四、缓存清理策略
1. 手动清理
// 清理单个storage.remove('tempData')// 清理全部uni.clearStorage()// 清理用户相关storage.clearUserData()
2. 自动过期(TTL)
// 存一个1小时后过期的数据storage.set('captcha', '1234', 60 * 60 * 1000)// 获取时自动判断过期,返回 nullconst captcha = storage.get('captcha')if (!captcha) { // 重新获取}
3. 容量监控 + 自动清理
// utils/cacheCleaner.jsconst MAX_SIZE_KB = 8 * 1024 // 8MB 预警export function autoCleanup() { const used = storage.getUsedSize() const limit = 10 * 1024 // 小程序总限额 10MB if (used > MAX_SIZE_KB) { console.warn(`存储容量告警: ${used}KB / ${limit}KB`) // 清理过期缓存 const keys = storage.getAllKeys() keys.forEach(key => { // 只清理非关键数据(临时、缓存类) if (key.includes('temp') || key.includes('cache')) { storage.remove(key) } }) }}// 在 App 启动时检查onLaunch() { autoCleanup()}
4. 退出登录清空
// 登录退出时调用functionlogout() { // 1. 调用后端退出接口 uni.request({ url: '/api/logout', method: 'POST' }) // 2. 清理本地用户数据 storage.clearUserData() // 3. 跳转登录页 uni.reLaunch({ url: '/pages/login/login' })}
五、用户信息缓存实战
1. 登录态 Token 存储
// 登录时存储async function login(phone, code) { try { const res = await uni.request({ url: '/api/login', method: 'POST', data: { phone, code } }) // 存储 token storage.set('token', res.data.token) storage.set('refreshToken', res.data.refreshToken) // 存储过期时间(假设token 7 天过期) const expireTime = Date.now() + 7 * 24 * 60 * 60 * 1000 storage.set('tokenExpire', expireTime) return res.data } catch (e) { console.error('登录失败', e) throw e }}// 请求拦截器自动带上 tokenuni.addInterceptor('request', { asyncinvoke(args) { const token = storage.get('token') if (token) { args.header = args.header || {} args.header['Authorization'] = `Bearer ${token}` } return args }})
2. 用户信息缓存 + 刷新机制
// 获取用户信息(离线优先)async function getUserInfo(forceRefresh = false) { // 1. 先取缓存(离线可用) const cache = storage.get('userInfo') if (cache && !forceRefresh) { return cache } // 2. 异步请求最新 try { const res = await uni.request({ url: '/api/user/info' }) // 3. 更新缓存 storage.set('userInfo', res.data) return res.data } catch (e) { // 4. 请求失败返回缓存(离线降级) if (cache) { console.warn('离线模式,返回缓存数据') return cache } throw e }}
3. App 端设备 ID 存储
// #ifdef APP-PLUSfunction getDeviceId() { let deviceId = storage.get('deviceId') if (!deviceId) { const info = plus.push.getClientInfo() deviceId = info.clientid || info.deviceid || generateUUID() storage.set('deviceId', deviceId) } return deviceId}// #endiffunction generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0 return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16) })}
六、UniApp 特有的持久化方案
1. App 端 SQLite(结构化大数据)
// App 端使用 SQLite 插件// 需要先在 manifest.json 配置第三方模块// sqlite.jslet db = nullexport function initDatabase() { // #ifdef APP-PLUS db = uni.requireNativePlugin('sqlite') db.open({ name: 'mydb', // 数据库名 path: '_doc/mydb.db' // 保存路径 }) // 创建表 db.execute({ sql: `CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, name TEXT, phone TEXT, avatar TEXT, update_time INTEGER )`, success: () => console.log('表创建成功'), fail: (e) => console.error('建表失败', e) }) // #endif}// 增删改查export function insertUser(user) { // #ifdef APP-PLUS db.execute({ sql: 'INSERT INTO users (id, name, phone, avatar, update_time) VALUES (?, ?, ?, ?, ?)', args: [user.id, user.name, user.phone, user.avatar, Date.now()], success: () => console.log('插入成功'), fail: (e) => console.error('插入失败', e) }) // #endif}export function queryUser(id) { return new Promise((resolve, reject) => { // #ifdef APP-PLUS db.select({ sql: 'SELECT * FROM users WHERE id = ?', args: [id], success: (res) => resolve(res), fail: (e) => reject(e) }) // #endif // #ifndef APP-PLUS resolve(null) // #endif })}
2. App 端原生键值对
// #ifdef APP-PLUS// plus.storage 是 App 原生 API,性能优于 Storage APIplus.storage.setItem('key', 'value')const val = plus.storage.getItem('key')plus.storage.removeItem('key')plus.storage.clear()plus.storage.getLength()plus.storage.getAllKeys()// #endif// 封装兼容写法function setItem(key, value) { // #ifdef APP-PLUS plus.storage.setItem(key, value) // #endif // #ifndef APP-PLUS uni.setStorageSync(key, value) // #endif}
3. H5 端 localStorage 兼容写法
// #ifdef H5function h5Set(key, value) { try { localStorage.setItem(key, JSON.stringify(value)) } catch (e) { // quota exceeded 超出配额 if (e.name === 'QuotaExceededError') { console.error('localStorage 已满,需要清理') // 清理旧缓存 cleanupOldData() // 重试 localStorage.setItem(key, JSON.stringify(value)) } }}// #endif
七、性能优化技巧
1. 懒加载:按需读取
// ❌ 一次全加载onLoad() { this.loadAllData() // 慢}// ✅ 按需加载onLoad(options) { if (options.type === 'profile') { this.loadUserProfile() // 只加载需要的 } else if (options.type === 'settings') { this.loadSettings() }}
2. 分片存储:大数据拆分
// 存储大数组时分片function setLargeData(key, data) { const CHUNK_SIZE = 100 // 每片100条 const chunks = Math.ceil(data.length / CHUNK_SIZE) storage.set(key + '_meta', { total: chunks, length: data.length }) for (let i = 0; i < chunks; i++) { const chunk = data.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE) storage.set(key + '_chunk_' + i, chunk) }}// 读取时合并function getLargeData(key) { const meta = storage.get(key + '_meta') if (!meta) return [] const result = [] for (let i = 0; i < meta.total; i++) { const chunk = storage.get(key + '_chunk_' + i) result.push(...chunk) } return result}
3. 内存 + Storage 二级缓存
// 内存缓存(当前页面生命周期内有效)const memoryCache = {}function getWithCache(key) { // 先查内存 if (memoryCache[key]) { console.log('命中内存缓存') return Promise.resolve(memoryCache[key]) } // 再查 Storage return storage.get(key).then(data => { if (data) { memoryCache[key] = data // 存入内存 } return data })}// 页面离开时清理内存缓存onUnload() { // 只清理非全局缓存 Object.keys(memoryCache).forEach(key => { if (!key.startsWith('global_')) { delete memoryCache[key] } })}
4. 小程序端定期清理
// #ifdef MP-WEIXIN// 在 pages.json 配置 "persistent": false 的页面自动被清理// 或者手动清理onShow() { this.checkAndCleanup()}methods: { checkAndCleanup() { // 检查存储使用量 uni.getStorageInfo({ success: (res) => { // 如果使用超过 80% if (res.currentSize / res.limitSize > 0.8) { this.cleanupOldCache() } } }) }, cleanupOldCache() { // 清理带过期时间但已过期的数据 const keys = storage.getAllKeys() let cleanedCount = 0 keys.forEach(key => { // 跳过关键数据 if (key.includes('token') || key.includes('userInfo')) return const value = storage.get(key) // 如果存储结构包含过期时间且已过期 if (value && value._expire && Date.now() > value._expire) { storage.remove(key) cleanedCount++ } }) console.log(`清理了 ${cleanedCount} 个过期缓存`) }}// #endif
八、避坑指南
| | |
|---|
| H5 存不进去 | localStorage 只有 5MB,超出就报错 | 清理旧数据 / 分片存储 / 删除不必要的 key |
| App 端存储失败 | | try-catch 包裹 + JSON.stringify 序列化 |
| 小程序容量超标 | | |
| 数据类型丢失 | 对象直接存,拿出来是字符串 [object Object] | 始终用 JSON.stringify() 存,JSON.parse() 取 |
| 退出登录没清干净 | | |
| 同步阻塞页面 | | |
| H5 隐私模式 | Safari/微信小程序 H5 无痕模式下 localStorage 不可用 | 改用 sessionStorage 或 try-catch 兜底 |
| 数据过期判断失效 | | 统一存储格式: { value, expire } |
| 多端 key 冲突 | | 加前缀区分:app_token vs h5_token |
关键代码检查清单
// ✅ 存之前uni.setStorage({ key: 'myKey', data: JSON.stringify(myObject), // 记得序列化})// ✅ 取出来const raw = uni.getStorageSync('myKey')if (raw) { const obj = JSON.parse(raw) // 记得反序列化}// ✅ 异常兜底try { uni.setStorageSync('key', JSON.stringify(data))} catch (e) { console.error('存储失败,可能超出容量', e) // 触发清理或提示用户}// ✅ 退出登录storage.clearUserData() // 不要只清 token
九、快速复制模板
storage.js 完整版
// storage.js - UniApp 跨端存储工具库// 支持:微信小程序 / H5 / Appconst PREFIX = 'myapp_'/** * 统一存储接口 * @param {string} key - 键名 * @param {any} value - 值(自动 JSON 序列化) * @param {number} expire - 过期时间(ms),0 永不过期 */function set(key, value, expire = 0) { const data = { value, expire: expire > 0 ? Date.now() + expire : 0 } const str = JSON.stringify(data) try { // #ifdef MP-WEIXIN uni.setStorageSync(PREFIX + key, str) // #endif // #ifdef H5 localStorage.setItem(PREFIX + key, str) // #endif // #ifdef APP-PLUS plus.storage.setItem(PREFIX + key, str) // #endif return true } catch (e) { console.error(`[Storage] set ${key} failed:`, e.message) return false }}/** * 统一读取接口 * @param {string} key - 键名 * @param {any} defaultValue - 默认值 */function get(key, defaultValue = null) { let str = null try { // #ifdef MP-WEIXIN str = uni.getStorageSync(PREFIX + key) // #endif // #ifdef H5 str = localStorage.getItem(PREFIX + key) // #endif // #ifdef APP-PLUS str = plus.storage.getItem(PREFIX + key) // #endif } catch (e) { console.error(`[Storage] get ${key} failed:`, e.message) return defaultValue } if (!str) return defaultValue try { const data = JSON.parse(str) // 过期检查 if (data.expire && Date.now() > data.expire) { remove(key) return defaultValue } return data.value } catch (e) { return defaultValue }}/** * 删除指定键 */function remove(key) { try { // #ifdef MP-WEIXIN uni.removeStorage({ key: PREFIX + key }) // #endif // #ifdef H5 localStorage.removeItem(PREFIX + key) // #endif // #ifdef APP-PLUS plus.storage.removeItem(PREFIX + key) // #endif } catch (e) { console.error(`[Storage] remove ${key} failed:`, e.message) }}/** * 清空所有带前缀的键 */function clear() { try { // #ifdef MP-WEIXIN uni.clearStorage() // #endif // #ifdef H5 Object.keys(localStorage) .filter(k => k.startsWith(PREFIX)) .forEach(k => localStorage.removeItem(k)) // #endif // #ifdef APP-PLUS plus.storage.clear() // #endif } catch (e) { console.error('[Storage] clear failed:', e.message) }}/** * 清理用户相关数据(退出登录用) */function clearUser() { const keys = getAllKeys() keys.filter(k => k.includes('user') || k.includes('token') || k.includes('profile') ).forEach(k => remove(k.replace(PREFIX, '')))}/** * 获取所有键名 */function getAllKeys() { try { // #ifdef MP-WEIXIN return uni.getStorageInfoSync().keys || [] // #endif // #ifdef H5 return Object.keys(localStorage) // #endif // #ifdef APP-PLUS return plus.storage.getAllKeys() || [] // #endif } catch (e) { return [] }}/** * 获取已用存储大小(KB) */function getUsedSize() { try { // #ifdef MP-WEIXIN return uni.getStorageInfoSync().currentSize || 0 // #endif // #ifdef H5 return Object.values(localStorage) .reduce((sum, v) => sum + v.length * 2, 0) / 1024 // #endif // #ifdef APP-PLUS return 0 // App 原生 API 不直接支持 // #endif } catch (e) { return 0 }}export default { set, get, remove, clear, clearUser, getAllKeys, getUsedSize}
pages/index/index.vue 使用示例
<template> <viewclass="container"> <text>用户名称: {{ userName }}</text> <button @click="saveData">保存数据</button> <button @click="loadData">读取数据</button> <button @click="clearData">清理数据</button> </view></template><script>import storage from '@/utils/storage.js'export default { data() { return { userName: '' } }, onLoad() { this.loadData() }, methods: { // 保存(带2小时过期) saveData() { storage.set('userName', '张三', 2 * 60 * 60 * 1000) uni.showToast({ title: '保存成功' }) }, // 读取 loadData() { this.userName = storage.get('userName', '未登录') }, // 清理用户数据 clearData() { storage.clearUser() this.userName = '未登录' uni.showToast({ title: '已清理' }) } }}</script>
总结
| |
|---|
| 存取值 | 始终 JSON.stringify / parse |
| 大数据 | |
| 敏感数据 | |
| 退出登录 | |
| 容量监控 | |
| 过期机制 | |
| 多端兼容 | #ifdef |
记住:Storage 不是数据库,别把它当 MySQL 用。大数据、关系型数据用 SQLite,配置类用 Storage,临时缓存注意过期和清理。