1. 为什么需要本地存储?

在移动应用开发中,本地存储就像是一个随身携带的小本子。想象一下,每次打开应用都要重新登录有多麻烦?有了本地存储,我们可以把用户登录状态、个性化设置等关键信息保存在设备上,下次打开应用时直接读取,大大提升用户体验。

Uniapp提供了两套本地存储API:异步的setStorage/getStorage和同步的setStorageSync/getStorageSync。我在开发一个跨平台电商应用时就遇到过选择困难:商品详情页需要快速读取用户收藏状态(适合同步),而用户登录信息需要安全校验(适合异步)。这两套API看似功能相同,但底层实现和适用场景却大不相同。

2. 异步存储:setStorage与getStorage详解

2.1 异步存储的工作原理

异步存储就像点外卖:下单后你可以继续做其他事情,等外卖到了会通知你。setStorage的典型用法是这样的:

uni.setStorage({
  key: 'userToken',
  data: 'abcd1234',
  success: () => {
    console.log('令牌存储成功');
  },
  fail: (err) => {
    console.error('存储失败', err);
  }
});

我在实际项目中发现三个关键点:

  1. 回调函数是异步执行的,不会阻塞页面渲染
  2. 适合存储较大数据(比如用户浏览历史)
  3. 错误处理更完善,可以通过fail回调捕获异常

2.2 异步读取的实战技巧

getStorage有个容易踩坑的地方:回调函数的执行时机。有次我做表单草稿保存功能,这样写就出问题了:

let draftData;
uni.getStorage({
  key: 'formDraft',
  success: (res) => {
    draftData = res.data; // 这里赋值时,后面的代码已经执行了
  }
});
console.log(draftData); // 这里会输出undefined

正确的做法是把后续逻辑都放在success回调里,或者用Promise封装:

function getStoragePromise(key) {
  return new Promise((resolve, reject) => {
    uni.getStorage({
      key,
      success: resolve,
      fail: reject
    });
  });
}

// 使用时
const draftData = await getStoragePromise('formDraft');

3. 同步存储:setStorageSync与getStorageSync深度解析

3.1 同步方法的特点

同步方法就像在食堂打饭:必须排队等到饭菜到手才能离开。它的代码更简洁:

// 存储
uni.setStorageSync('theme', 'dark');

// 读取
const theme = uni.getStorageSync('theme');

但我在性能测试时发现:同步操作会阻塞JS主线程。当存储5MB以上的数据时,界面会出现明显卡顿。因此同步方法最适合:

  • 高频读写的小数据(如用户偏好设置)
  • 应用启动时必须立即获取的数据
  • 简单的状态标记存储

3.2 同步读取的异常处理

同步方法没有回调函数,错误处理需要配合try-catch:

try {
  const token = uni.getStorageSync('userToken');
  if(!token) throw new Error('令牌不存在');
  // 后续逻辑...
} catch (e) {
  console.error('读取令牌失败', e);
  uni.showToast({ title: '请重新登录' });
}

有个实际案例:某次APP更新后,旧版存储的数据格式不兼容新版,导致同步读取直接崩溃。后来我们增加了数据校验逻辑:

function safeGetSync(key) {
  try {
    const data = uni.getStorageSync(key);
    // 验证数据格式
    if(key === 'userInfo' && !data?.userId) {
      return null;
    }
    return data;
  } catch {
    return null;
  }
}

4. 异步与同步的抉择指南

4.1 性能对比实测

我用同一台测试机做了组对比实验(存储100KB数据,循环100次):

操作类型平均耗时主线程阻塞
setStorage12ms
setStorageSync8ms轻微
getStorage10ms
getStorageSync6ms轻微

看起来同步更快?但实际开发中要考虑更多因素:

  • 异步操作的并行优势
  • 错误恢复能力
  • 代码可维护性

4.2 典型场景推荐方案

根据我的项目经验,这些场景推荐这样选:

必须用异步的场景:

  1. 启动时加载用户配置(需要网络请求配合)
  2. 表单数据自动保存(需要错误重试机制)
  3. 需要加密存储的敏感数据

适合同步的场景:

  1. 界面主题切换
  2. 全局loading状态
  3. 简单的AB测试标记
  4. 页面间临时参数传递

4.3 混合使用的最佳实践

很多时候需要混搭使用。比如用户登录流程:

// 同步快速检查登录状态
const token = uni.getStorageSync('token');
if(token) {
  // 异步验证令牌有效性
  checkToken(token).then(valid => {
    if(!valid) {
      uni.removeStorageSync('token'); // 同步清除
      showLoginModal();
    }
  });
} else {
  showLoginModal();
}

5. 实战Demo:购物车本地缓存方案

让我们实现一个完整的购物车本地存储方案:

// 购物车服务模块
const cartService = {
  // 异步保存(防抖处理)
  saveCart: debounce(async (cartItems) => {
    try {
      await new Promise((resolve, reject) => {
        uni.setStorage({
          key: 'cart',
          data: JSON.stringify(cartItems),
          success: resolve,
          fail: reject
        });
      });
      console.log('购物车保存成功');
    } catch (e) {
      console.error('保存失败', e);
      // 尝试降级方案
      try {
        uni.setStorageSync('cart_fallback', JSON.stringify(cartItems));
      } catch (syncErr) {
        uni.reportAnalytics('STORAGE_ERROR', {...});
      }
    }
  }, 500),
  
  // 同步读取(首屏快速展示)
  getCartSync: () => {
    try {
      const data = uni.getStorageSync('cart') || 
                   uni.getStorageSync('cart_fallback');
      return data ? JSON.parse(data) : [];
    } catch {
      return [];
    }
  },
  
  // 异步读取(确保最新)
  getCartAsync: () => {
    return new Promise((resolve) => {
      uni.getStorage({
        key: 'cart',
        success: (res) => {
          try {
            resolve(JSON.parse(res.data));
          } catch {
            resolve([]);
          }
        },
        fail: () => resolve(this.getCartSync())
      });
    });
  }
};

这个方案实现了:

  1. 首屏同步快速展示
  2. 修改时异步防抖保存
  3. 完整的错误降级处理
  4. 异常监控上报

6. 常见问题与性能优化

6.1 存储大小限制

不同平台限制不同:

  • 微信小程序:10MB
  • H5:取决于浏览器,通常5-10MB
  • App:理论上无限制,但建议不超过50MB

我遇到过H5端存储图片base64导致超出限额的案例,解决方案是:

  1. 定期清理过期数据
  2. 大数据改用文件系统存储
  3. 压缩存储内容

6.2 数据加密方案

敏感数据应该加密存储。推荐使用crypto-js:

import CryptoJS from 'crypto-js';

const SECRET_KEY = 'your-secret-key';

function encryptData(data) {
  return CryptoJS.AES.encrypt(
    JSON.stringify(data), 
    SECRET_KEY
  ).toString();
}

function decryptData(ciphertext) {
  const bytes = CryptoJS.AES.decrypt(ciphertext, SECRET_KEY);
  return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
}

// 使用示例
uni.setStorageSync('user', encryptData(userInfo));
const user = decryptData(uni.getStorageSync('user'));

6.3 多Tab数据同步

在H5环境中,多个浏览器Tab间的存储需要同步。可以通过监听storage事件实现:

window.addEventListener('storage', (event) => {
  if(event.key === 'cart') {
    // 更新本地购物车状态
    this.cartData = JSON.parse(event.newValue);
  }
});

在Uniapp中,还可以使用全局事件总线:

// 存储时触发事件
uni.$emit('storageUpdate', { key: 'cart', value: newCart });

// 其他页面监听
uni.$on('storageUpdate', ({key, value}) => {
  if(key === 'cart') {
    this.cartData = value;
  }
});

7. 跨平台兼容性处理

不同平台的存储实现有差异,需要特别注意:

  1. 微信小程序

    • 同步API性能最好
    • 清除缓存会同时清除storage
  2. H5

    • 受浏览器同源策略限制
    • 隐私模式可能无法使用storage
  3. App

    • 支持最大的存储空间
    • 可以配合SQLite做结构化存储

建议在应用启动时做能力检测:

function checkStorage() {
  try {
    const testKey = '__test__';
    uni.setStorageSync(testKey, '1');
    if(uni.getStorageSync(testKey) !== '1') {
      throw new Error('存储异常');
    }
    uni.removeStorageSync(testKey);
    return true;
  } catch {
    uni.showModal({
      title: '提示',
      content: '本地存储功能异常,部分功能可能受限',
      showCancel: false
    });
    return false;
  }
}

8. 高级技巧:存储监控与自动化

8.1 存储状态监控

可以通过拦截器监控所有存储操作:

const originalSet = uni.setStorage;
uni.setStorage = function(options) {
  console.log(`[Storage] 设置 ${options.key}`);
  performance.mark('storageStart');
  return originalSet.call(this, {
    ...options,
    complete: () => {
      performance.mark('storageEnd');
      performance.measure(
        'storageTime', 
        'storageStart', 
        'storageEnd'
      );
      options.complete?.();
    }
  });
};

8.2 自动过期机制

实现类似cookie的过期时间功能:

function setWithExpiry(key, value, expiryDays) {
  const item = {
    value,
    expiry: Date.now() + expiryDays * 86400000
  };
  uni.setStorageSync(key, JSON.stringify(item));
}

function getWithExpiry(key) {
  const itemStr = uni.getStorageSync(key);
  if(!itemStr) return null;
  
  const item = JSON.parse(itemStr);
  if(Date.now() > item.expiry) {
    uni.removeStorageSync(key);
    return null;
  }
  return item.value;
}

8.3 存储压缩技巧

对于大量重复数据,可以使用压缩算法:

import { compress, decompress } from 'lz-string';

function setCompressed(key, data) {
  uni.setStorageSync(key, compress(JSON.stringify(data)));
}

function getCompressed(key) {
  const data = uni.getStorageSync(key);
  return data ? JSON.parse(decompress(data)) : null;
}
Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐