Skip to content

数据存储

在uni-app开发中,数据存储是应用程序保存和读取本地数据的重要功能。本文将详细介绍uni-app中的数据存储方法、存储限制、加密存储以及最佳实践。

基础存储API

uni-app提供了一系列API用于数据存储,这些API在各平台表现一致,简化了跨平台开发。

uni.setStorage 与 uni.setStorageSync

用于将数据存储在本地缓存中指定的key中,会覆盖掉原来该key对应的内容。

异步方法

javascript
uni.setStorage({
  key: 'userInfo',
  data: {
    name: '张三',
    age: 25,
    gender: '男'
  },
  success: function() {
    console.log('存储成功')
  },
  fail: function(err) {
    console.error('存储失败:', err)
  },
  complete: function() {
    console.log('操作完成')
  }
})

同步方法

javascript
try {
  uni.setStorageSync('userInfo', {
    name: '张三',
    age: 25,
    gender: '男'
  })
  console.log('存储成功')
} catch (e) {
  console.error('存储失败:', e)
}

uni.getStorage 与 uni.getStorageSync

用于从本地缓存中获取指定key对应的内容。

异步方法

javascript
uni.getStorage({
  key: 'userInfo',
  success: function(res) {
    console.log('获取成功:', res.data)
  },
  fail: function(err) {
    console.error('获取失败:', err)
  }
})

同步方法

javascript
try {
  const value = uni.getStorageSync('userInfo')
  if (value) {
    console.log('获取成功:', value)
  } else {
    console.log('数据不存在')
  }
} catch (e) {
  console.error('获取失败:', e)
}

uni.removeStorage 与 uni.removeStorageSync

用于从本地缓存中移除指定key。

异步方法

javascript
uni.removeStorage({
  key: 'userInfo',
  success: function() {
    console.log('删除成功')
  },
  fail: function(err) {
    console.error('删除失败:', err)
  }
})

同步方法

javascript
try {
  uni.removeStorageSync('userInfo')
  console.log('删除成功')
} catch (e) {
  console.error('删除失败:', e)
}

uni.clearStorage 与 uni.clearStorageSync

用于清理本地数据缓存。

异步方法

javascript
uni.clearStorage()

同步方法

javascript
try {
  uni.clearStorageSync()
  console.log('清除成功')
} catch (e) {
  console.error('清除失败:', e)
}

uni.getStorageInfo 与 uni.getStorageInfoSync

用于获取当前storage的相关信息。

异步方法

javascript
uni.getStorageInfo({
  success: function(res) {
    console.log('keys:', res.keys) // 当前storage中所有的key
    console.log('currentSize:', res.currentSize) // 当前占用的空间大小, 单位:kb
    console.log('limitSize:', res.limitSize) // 限制的空间大小, 单位:kb
  },
  fail: function(err) {
    console.error('获取失败:', err)
  }
})

同步方法

javascript
try {
  const res = uni.getStorageInfoSync()
  console.log('keys:', res.keys)
  console.log('currentSize:', res.currentSize)
  console.log('limitSize:', res.limitSize)
} catch (e) {
  console.error('获取失败:', e)
}

存储限制

不同平台对存储空间的限制不同,开发者需要注意:

平台存储限制
微信小程序10MB
支付宝小程序10MB
百度小程序10MB
字节跳动小程序10MB
H5根据浏览器不同而不同,一般为5MB左右
App无明确限制,但建议控制在50MB以内

为了避免超出存储限制,建议:

  1. 定期清理不必要的缓存数据
  2. 对大型数据进行分片存储
  3. 使用压缩算法减小数据体积
  4. 将不常用的大型数据存储在服务器端

数据类型支持

uni-app的存储API支持以下数据类型:

  • String(字符串)
  • Number(数字)
  • Boolean(布尔值)
  • Object(对象)
  • Array(数组)
  • Date(日期)
  • null

注意:存储复杂数据类型(如对象、数组)时,会自动进行JSON序列化和反序列化。但是,某些特殊对象(如函数、正则表达式、Error对象等)在序列化过程中会丢失信息。

存储封装与管理

为了更好地管理本地存储,建议封装一个存储管理模块:

javascript
// storage.js

/**
 * 存储管理类
 */
class Storage {
  /**
   * 设置存储
   * @param {string} key 键名
   * @param {any} data 数据
   * @param {number} expires 过期时间(毫秒),不设置则永不过期
   */
  set(key, data, expires) {
    const item = {
      data,
      expires: expires ? Date.now() + expires : null
    }
    
    try {
      uni.setStorageSync(key, JSON.stringify(item))
      return true
    } catch (e) {
      console.error('存储失败:', e)
      return false
    }
  }
  
  /**
   * 获取存储
   * @param {string} key 键名
   * @param {any} defaultValue 默认值,当获取失败或已过期时返回
   * @returns {any} 存储的数据或默认值
   */
  get(key, defaultValue = null) {
    try {
      const itemStr = uni.getStorageSync(key)
      
      if (!itemStr) {
        return defaultValue
      }
      
      const item = JSON.parse(itemStr)
      
      // 检查是否过期
      if (item.expires && item.expires < Date.now()) {
        this.remove(key)
        return defaultValue
      }
      
      return item.data
    } catch (e) {
      console.error('获取失败:', e)
      return defaultValue
    }
  }
  
  /**
   * 移除存储
   * @param {string} key 键名
   * @returns {boolean} 是否成功
   */
  remove(key) {
    try {
      uni.removeStorageSync(key)
      return true
    } catch (e) {
      console.error('删除失败:', e)
      return false
    }
  }
  
  /**
   * 清空所有存储
   * @returns {boolean} 是否成功
   */
  clear() {
    try {
      uni.clearStorageSync()
      return true
    } catch (e) {
      console.error('清空失败:', e)
      return false
    }
  }
  
  /**
   * 获取所有键名
   * @returns {Array<string>} 键名数组
   */
  keys() {
    try {
      const res = uni.getStorageInfoSync()
      return res.keys || []
    } catch (e) {
      console.error('获取键名失败:', e)
      return []
    }
  }
  
  /**
   * 获取存储信息
   * @returns {Object} 存储信息
   */
  info() {
    try {
      return uni.getStorageInfoSync()
    } catch (e) {
      console.error('获取存储信息失败:', e)
      return {
        keys: [],
        currentSize: 0,
        limitSize: 0
      }
    }
  }
  
  /**
   * 检查键是否存在
   * @param {string} key 键名
   * @returns {boolean} 是否存在
   */
  has(key) {
    try {
      const res = uni.getStorageInfoSync()
      return res.keys.includes(key)
    } catch (e) {
      console.error('检查键失败:', e)
      return false
    }
  }
  
  /**
   * 获取存储大小(KB)
   * @returns {number} 存储大小
   */
  size() {
    try {
      const res = uni.getStorageInfoSync()
      return res.currentSize || 0
    } catch (e) {
      console.error('获取存储大小失败:', e)
      return 0
    }
  }
  
  /**
   * 获取存储限制大小(KB)
   * @returns {number} 限制大小
   */
  limitSize() {
    try {
      const res = uni.getStorageInfoSync()
      return res.limitSize || 0
    } catch (e) {
      console.error('获取限制大小失败:', e)
      return 0
    }
  }
  
  /**
   * 批量设置存储
   * @param {Object} data 键值对对象
   * @param {number} expires 过期时间(毫秒),不设置则永不过期
   * @returns {boolean} 是否全部成功
   */
  setAll(data, expires) {
    if (!data || typeof data !== 'object') {
      return false
    }
    
    let success = true
    
    Object.keys(data).forEach(key => {
      const result = this.set(key, data[key], expires)
      if (!result) {
        success = false
      }
    })
    
    return success
  }
  
  /**
   * 批量获取存储
   * @param {Array<string>} keys 键名数组
   * @returns {Object} 键值对对象
   */
  getAll(keys) {
    if (!Array.isArray(keys)) {
      return {}
    }
    
    const result = {}
    
    keys.forEach(key => {
      result[key] = this.get(key)
    })
    
    return result
  }
  
  /**
   * 批量移除存储
   * @param {Array<string>} keys 键名数组
   * @returns {boolean} 是否全部成功
   */
  removeAll(keys) {
    if (!Array.isArray(keys)) {
      return false
    }
    
    let success = true
    
    keys.forEach(key => {
      const result = this.remove(key)
      if (!result) {
        success = false
      }
    })
    
    return success
  }
  
  /**
   * 清理过期数据
   * @returns {number} 清理的数量
   */
  clearExpired() {
    let count = 0
    
    try {
      const keys = this.keys()
      
      keys.forEach(key => {
        try {
          const itemStr = uni.getStorageSync(key)
          
          if (itemStr) {
            const item = JSON.parse(itemStr)
            
            if (item.expires && item.expires < Date.now()) {
              this.remove(key)
              count++
            }
          }
        } catch (e) {
          // 忽略解析错误
        }
      })
    } catch (e) {
      console.error('清理过期数据失败:', e)
    }
    
    return count
  }
}

// 创建存储实例
const storage = new Storage()

export default storage

使用存储管理模块:

javascript
import storage from '@/utils/storage'

// 设置数据(1小时过期)
storage.set('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', 3600 * 1000)

// 设置永久数据
storage.set('settings', { theme: 'dark', fontSize: 16 })

// 获取数据
const token = storage.get('token')
const settings = storage.get('settings', { theme: 'light', fontSize: 14 }) // 提供默认值

// 检查数据是否存在
if (storage.has('userInfo')) {
  console.log('用户信息已存在')
}

// 移除数据
storage.remove('token')

// 批量操作
storage.setAll({
  name: '张三',
  age: 25,
  gender: '男'
})

const userInfo = storage.getAll(['name', 'age', 'gender'])
console.log(userInfo) // { name: '张三', age: 25, gender: '男' }

// 清理过期数据
const count = storage.clearExpired()
console.log(`清理了${count}条过期数据`)

// 获取存储信息
const info = storage.info()
console.log(`当前使用:${info.currentSize}KB,限制:${info.limitSize}KB`)

加密存储

对于敏感数据(如用户密码、令牌等),建议进行加密存储。以下是一个简单的加密存储示例:

javascript
// crypto-storage.js

import CryptoJS from 'crypto-js' // 需要安装crypto-js库:npm install crypto-js

/**
 * 加密存储类
 */
class CryptoStorage {
  constructor(secretKey = 'default-secret-key') {
    this.secretKey = secretKey
  }
  
  /**
   * 加密数据
   * @param {any} data 要加密的数据
   * @returns {string} 加密后的字符串
   */
  encrypt(data) {
    const dataStr = JSON.stringify(data)
    return CryptoJS.AES.encrypt(dataStr, this.secretKey).toString()
  }
  
  /**
   * 解密数据
   * @param {string} ciphertext 加密的字符串
   * @returns {any} 解密后的数据
   */
  decrypt(ciphertext) {
    try {
      const bytes = CryptoJS.AES.decrypt(ciphertext, this.secretKey)
      const dataStr = bytes.toString(CryptoJS.enc.Utf8)
      return JSON.parse(dataStr)
    } catch (e) {
      console.error('解密失败:', e)
      return null
    }
  }
  
  /**
   * 设置加密存储
   * @param {string} key 键名
   * @param {any} data 数据
   * @param {number} expires 过期时间(毫秒),不设置则永不过期
   */
  set(key, data, expires) {
    const item = {
      data,
      expires: expires ? Date.now() + expires : null
    }
    
    const encryptedData = this.encrypt(item)
    
    try {
      uni.setStorageSync(`encrypted_${key}`, encryptedData)
      return true
    } catch (e) {
      console.error('加密存储失败:', e)
      return false
    }
  }
  
  /**
   * 获取加密存储
   * @param {string} key 键名
   * @param {any} defaultValue 默认值,当获取失败或已过期时返回
   * @returns {any} 存储的数据或默认值
   */
  get(key, defaultValue = null) {
    try {
      const encryptedData = uni.getStorageSync(`encrypted_${key}`)
      
      if (!encryptedData) {
        return defaultValue
      }
      
      const item = this.decrypt(encryptedData)
      
      if (!item) {
        return defaultValue
      }
      
      // 检查是否过期
      if (item.expires && item.expires < Date.now()) {
        this.remove(key)
        return defaultValue
      }
      
      return item.data
    } catch (e) {
      console.error('获取加密存储失败:', e)
      return defaultValue
    }
  }
  
  /**
   * 移除加密存储
   * @param {string} key 键名
   * @returns {boolean} 是否成功
   */
  remove(key) {
    try {
      uni.removeStorageSync(`encrypted_${key}`)
      return true
    } catch (e) {
      console.error('删除加密存储失败:', e)
      return false
    }
  }
  
  /**
   * 检查加密键是否存在
   * @param {string} key 键名
   * @returns {boolean} 是否存在
   */
  has(key) {
    try {
      const res = uni.getStorageInfoSync()
      return res.keys.includes(`encrypted_${key}`)
    } catch (e) {
      console.error('检查加密键失败:', e)
      return false
    }
  }
  
  /**
   * 更改密钥(会重新加密所有数据)
   * @param {string} newSecretKey 新密钥
   * @returns {boolean} 是否成功
   */
  changeSecretKey(newSecretKey) {
    if (!newSecretKey) {
      return false
    }
    
    try {
      const res = uni.getStorageInfoSync()
      const encryptedKeys = res.keys.filter(key => key.startsWith('encrypted_'))
      
      // 获取所有加密数据
      const allData = {}
      encryptedKeys.forEach(key => {
        const originalKey = key.replace('encrypted_', '')
        const encryptedData = uni.getStorageSync(key)
        const item = this.decrypt(encryptedData)
        
        if (item) {
          allData[originalKey] = {
            data: item.data,
            expires: item.expires
          }
        }
      })
      
      // 更改密钥
      const oldSecretKey = this.secretKey
      this.secretKey = newSecretKey
      
      // 重新加密所有数据
      Object.keys(allData).forEach(key => {
        const item = allData[key]
        const encryptedData = this.encrypt({
          data: item.data,
          expires: item.expires
        })
        
        uni.setStorageSync(`encrypted_${key}`, encryptedData)
      })
      
      return true
    } catch (e) {
      // 恢复旧密钥
      this.secretKey = oldSecretKey
      console.error('更改密钥失败:', e)
      return false
    }
  }
}

// 创建加密存储实例
const cryptoStorage = new CryptoStorage('your-secret-key')

export default cryptoStorage

使用加密存储:

javascript
import cryptoStorage from '@/utils/crypto-storage'

// 存储敏感数据(7天过期)
cryptoStorage.set('password', '123456', 7 * 24 * 3600 * 1000)
cryptoStorage.set('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...')

// 获取敏感数据
const password = cryptoStorage.get('password')
const token = cryptoStorage.get('token')

// 更改密钥
cryptoStorage.changeSecretKey('new-secret-key')

文件存储

对于大型数据或二进制数据,可以使用uni-app提供的文件系统API进行存储:

保存文件

javascript
// 保存文件到本地
uni.saveFile({
  tempFilePath: tempFilePath, // 需要保存的文件的临时路径
  success: function(res) {
    const savedFilePath = res.savedFilePath
    console.log('文件保存成功:', savedFilePath)
    
    // 可以将文件路径存储起来,以便后续使用
    uni.setStorageSync('savedFilePath', savedFilePath)
  },
  fail: function(err) {
    console.error('文件保存失败:', err)
  }
})

获取保存的文件列表

javascript
uni.getSavedFileList({
  success: function(res) {
    console.log('文件列表:', res.fileList)
    // res.fileList是一个数组,包含filePath和createTime等信息
  },
  fail: function(err) {
    console.error('获取文件列表失败:', err)
  }
})

获取文件信息

javascript
uni.getSavedFileInfo({
  filePath: savedFilePath,
  success: function(res) {
    console.log('文件大小:', res.size)
    console.log('创建时间:', res.createTime)
  },
  fail: function(err) {
    console.error('获取文件信息失败:', err)
  }
})

删除文件

javascript
uni.removeSavedFile({
  filePath: savedFilePath,
  success: function() {
    console.log('文件删除成功')
  },
  fail: function(err) {
    console.error('文件删除失败:', err)
  }
})

文件管理器

对于App平台,可以使用更强大的文件管理器API:

javascript
// 仅在App平台可用
// #ifdef APP-PLUS
const fs = uni.getFileSystemManager()

// 写入文件
fs.writeFile({
  filePath: `${uni.env.USER_DATA_PATH}/test.txt`,
  data: 'Hello, uni-app!',
  encoding: 'utf8',
  success: function() {
    console.log('写入成功')
  },
  fail: function(err) {
    console.error('写入失败:', err)
  }
})

// 读取文件
fs.readFile({
  filePath: `${uni.env.USER_DATA_PATH}/test.txt`,
  encoding: 'utf8',
  success: function(res) {
    console.log('文件内容:', res.data)
  },
  fail: function(err) {
    console.error('读取失败:', err)
  }
})

// 删除文件
fs.unlink({
  filePath: `${uni.env.USER_DATA_PATH}/test.txt`,
  success: function() {
    console.log('删除成功')
  },
  fail: function(err) {
    console.error('删除失败:', err)
  }
})
// #endif

IndexedDB存储

在H5平台,可以使用IndexedDB进行大容量数据存储:

javascript
// indexed-db.js

/**
 * IndexedDB存储类(仅H5平台可用)
 */
class IndexedDBStorage {
  constructor(dbName = 'uniapp-db', version = 1) {
    this.dbName = dbName
    this.version = version
    this.db = null
    
    // 仅在H5平台初始化
    // #ifdef H5
    this.init()
    // #endif
  }
  
  /**
   * 初始化数据库
   * @returns {Promise} 初始化Promise
   */
  init() {
    return new Promise((resolve, reject) => {
      // 检查浏览器是否支持IndexedDB
      if (!window.indexedDB) {
        reject(new Error('浏览器不支持IndexedDB'))
        return
      }
      
      const request = window.indexedDB.open(this.dbName, this.version)
      
      request.onerror = (event) => {
        console.error('IndexedDB打开失败:', event)
        reject(event)
      }
      
      request.onsuccess = (event) => {
        this.db = event.target.result
        console.log('IndexedDB打开成功')
        resolve(this.db)
      }
      
      request.onupgradeneeded = (event) => {
        const db = event.target.result
        
        // 创建存储对象
        if (!db.objectStoreNames.contains('keyvaluepairs')) {
          db.createObjectStore('keyvaluepairs', { keyPath: 'key' })
        }
      }
    })
  }
  
  /**
   * 确保数据库已连接
   * @returns {Promise} 数据库连接Promise
   */
  ensureDB() {
    if (this.db) {
      return Promise.resolve(this.db)
    }
    return this.init()
  }
  
  /**
   * 设置数据
   * @param {string} key 键名
   * @param {any} data 数据
   * @param {number} expires 过期时间(毫秒),不设置则永不过期
   * @returns {Promise} 操作Promise
   */
  set(key, data, expires) {
    return this.ensureDB().then(db => {
      return new Promise((resolve, reject) => {
        const transaction = db.transaction(['keyvaluepairs'], 'readwrite')
        const store = transaction.objectStore('keyvaluepairs')
        
        const item = {
          key,
          data,
          expires: expires ? Date.now() + expires : null,
          timestamp: Date.now()
        }
        
        const request = store.put(item)
        
        request.onsuccess = () => {
          resolve(true)
        }
        
        request.onerror = (event) => {
          console.error('存储数据失败:', event)
          reject(event)
        }
      })
    })
  }
  
  /**
   * 获取数据
   * @param {string} key 键名
   * @param {any} defaultValue 默认值,当获取失败或已过期时返回
   * @returns {Promise} 包含数据的Promise
   */
  get(key, defaultValue = null) {
    return this.ensureDB().then(db => {
      return new Promise((resolve, reject) => {
        const transaction = db.transaction(['keyvaluepairs'], 'readonly')
        const store = transaction.objectStore('keyvaluepairs')
        const request = store.get(key)
        
        request.onsuccess = (event) => {
          const item = event.target.result
          
          if (!item) {
            resolve(defaultValue)
            return
          }
          
          // 检查是否过期
          if (item.expires && item.expires < Date.now()) {
            this.remove(key).then(() => {
              resolve(defaultValue)
            }).catch(() => {
              resolve(defaultValue)
            })
            return
          }
          
          resolve(item.data)
        }
        
        request.onerror = (event) => {
          console.error('获取数据失败:', event)
          reject(event)
        }
      })
    })
  }
  
  /**
   * 移除数据
   * @param {string} key 键名
   * @returns {Promise} 操作Promise
   */
  remove(key) {
    return this.ensureDB().then(db => {
      return new Promise((resolve, reject) => {
        const transaction = db.transaction(['keyvaluepairs'], 'readwrite')
        const store = transaction.objectStore('keyvaluepairs')
        const request = store.delete(key)
        
        request.onsuccess = () => {
          resolve(true)
        }
        
        request.onerror = (event) => {
          console.error('删除数据失败:', event)
          reject(event)
        }
      })
    })
  }
  
  /**
   * 清空所有数据
   * @returns {Promise} 操作Promise
   */
  clear() {
    return this.ensureDB().then(db => {
      return new Promise((resolve, reject) => {
        const transaction = db.transaction(['keyvaluepairs'], 'readwrite')
        const store = transaction.objectStore('keyvaluepairs')
        const request = store.clear()
        
        request.onsuccess = () => {
          resolve(true)
        }
        
        request.onerror = (event) => {
          console.error('清空数据失败:', event)
          reject(event)
        }
      })
    })
  }
  
  /**
   * 获取所有键
   * @returns {Promise} 包含键数组的Promise
   */
  keys() {
    return this.ensureDB().then(db => {
      return new Promise((resolve, reject) => {
        const transaction = db.transaction(['keyvaluepairs'], 'readonly')
        const store = transaction.objectStore('keyvaluepairs')
        const request = store.getAllKeys()
        
        request.onsuccess = (event) => {
          resolve(event.target.result || [])
        }
        
        request.onerror = (event) => {
          console.error('获取键失败:', event)
          reject(event)
        }
      })
    })
  }
  
  /**
   * 获取所有数据
   * @returns {Promise} 包含所有数据的Promise
   */
  getAll() {
    return this.ensureDB().then(db => {
      return new Promise((resolve, reject) => {
        const transaction = db.transaction(['keyvaluepairs'], 'readonly')
        const store = transaction.objectStore('keyvaluepairs')
        const request = store.getAll()
        
        request.onsuccess = (event) => {
          const items = event.target.result || []
          const result = {}
          
          items.forEach(item => {
            // 检查是否过期
            if (item.expires && item.expires < Date.now()) {
              this.remove(item.key)
            } else {
              result[item.key] = item.data
            }
          })
          
          resolve(result)
        }
        
        request.onerror = (event) => {
          console.error('获取所有数据失败:', event)
          reject(event)
        }
      })
    })
  }
}

// 创建IndexedDB存储实例
// #ifdef H5
const indexedDBStorage = new IndexedDBStorage()
export default indexedDBStorage
// #endif

使用IndexedDB存储:

javascript
// #ifdef H5
import indexedDBStorage from '@/utils/indexed-db'

// 存储数据
indexedDBStorage.set('largeData', largeDataObject)
  .then(() => {
    console.log('大型数据存储成功')
  })
  .catch(err => {
    console.error('存储失败:', err)
  })

// 获取数据
indexedDBStorage.get('largeData')
  .then(data => {
    console.log('获取大型数据成功:', data)
  })
  .catch(err => {
    console.error('获取失败:', err)
  })
// #endif

数据同步与备份

为了确保用户数据安全,可以实现数据同步与备份功能:

云端同步

javascript
// sync.js

import request from '@/utils/request'
import storage from '@/utils/storage'

/**
 * 数据同步类
 */
class DataSync {
  constructor() {
    this.syncKeys = ['userSettings', 'favorites', 'history']
    this.lastSyncTime = storage.get('lastSyncTime', 0)
  }
  
  /**
   * 上传数据到云端
   * @returns {Promise} 同步Promise
   */
  uploadToCloud() {
    // 获取需要同步的数据
    const syncData = {}
    
    this.syncKeys.forEach(key => {
      syncData[key] = storage.get(key)
    })
    
    // 添加同步时间戳
    syncData.timestamp = Date.now()
    
    // 上传到服务器
    return request.post('/user/sync', syncData)
      .then(() => {
        // 更新最后同步时间
        this.lastSyncTime = syncData.timestamp
        storage.set('lastSyncTime', this.lastSyncTime)
        
        console.log('数据上传成功')
        return true
      })
      .catch(err => {
        console.error('数据上传失败:', err)
        throw err
      })
  }
  
  /**
   * 从云端下载数据
   * @returns {Promise} 同步Promise
   */
  downloadFromCloud() {
    // 获取服务器数据
    return request.get('/user/sync', { lastSyncTime: this.lastSyncTime })
      .then(data => {
        if (!data || !data.timestamp) {
          console.log('没有新数据需要同步')
          return false
        }
        
        // 检查服务器数据是否比本地新
        if (data.timestamp <= this.lastSyncTime) {
          console.log('本地数据已是最新')
          return false
        }
        
        // 更新本地数据
        Object.keys(data).forEach(key => {
          if (key !== 'timestamp' && this.syncKeys.includes(key)) {
            storage.set(key, data[key])
          }
        })
        
        // 更新最后同步时间
        this.lastSyncTime = data.timestamp
        storage.set('lastSyncTime', this.lastSyncTime)
        
        console.log('数据下载成功')
        return true
      })
      .catch(err => {
        console.error('数据下载失败:', err)
        throw err
      })
  }
  
  /**
   * 双向同步数据
   * @returns {Promise} 同步Promise
   */
  sync() {
    return this.downloadFromCloud()
      .then(() => {
        return this.uploadToCloud()
      })
      .then(() => {
        console.log('数据同步完成')
        return true
      })
      .catch(err => {
        console.error('数据同步失败:', err)
        throw err
      })
  }
  
  /**
   * 自动同步(定时执行)
   * @param {number} interval 同步间隔(毫秒)
   */
  startAutoSync(interval = 5 * 60 * 1000) {
    // 先执行一次同步
    this.sync()
      .catch(() => {
        // 忽略错误
      })
    
    // 设置定时同步
    setInterval(() => {
      this.sync()
        .catch(() => {
          // 忽略错误
        })
    }, interval)
  }
}

// 创建数据同步实例
const dataSync = new DataSync()

export default dataSync

使用数据同步:

javascript
import dataSync from '@/utils/sync'

// 手动同步
dataSync.sync()
  .then(() => {
    uni.showToast({
      title: '同步成功',
      icon: 'success'
    })
  })
  .catch(() => {
    uni.showToast({
      title: '同步失败',
      icon: 'none'
    })
  })

// 启动自动同步(每5分钟)
dataSync.startAutoSync()

本地备份与恢复

javascript
// backup.js

import storage from '@/utils/storage'

/**
 * 数据备份类
 */
class DataBackup {
  /**
   * 创建备份
   * @param {Array<string>} keys 要备份的键名数组,不提供则备份所有
   * @returns {Object} 备份数据
   */
  createBackup(keys) {
    try {
      // 获取所有键或指定键
      const backupKeys = keys || storage.keys()
      
      // 获取数据
      const backupData = {}
      
      backupKeys.forEach(key => {
        const value = storage.get(key)
        if (value !== null) {
          backupData[key] = value
        }
      })
      
      // 添加备份信息
      const backupInfo = {
        timestamp: Date.now(),
        version: '1.0.0',
        keys: Object.keys(backupData),
        count: Object.keys(backupData).length
      }
      
      const backup = {
        info: backupInfo,
        data: backupData
      }
      
      return backup
    } catch (e) {
      console.error('创建备份失败:', e)
      return null
    }
  }
  
  /**
   * 保存备份到文件
   * @param {Object} backup 备份数据
   * @returns {Promise} 操作Promise
   */
  saveBackupToFile(backup) {
    return new Promise((resolve, reject) => {
      try {
        if (!backup) {
          reject(new Error('备份数据为空'))
          return
        }
        
        // 转换为JSON字符串
        const backupStr = JSON.stringify(backup)
        
        // 仅在App平台可用
        // #ifdef APP-PLUS
        const fs = uni.getFileSystemManager()
        const backupPath = `${uni.env.USER_DATA_PATH}/backup_${backup.info.timestamp}.json`
        
        fs.writeFile({
          filePath: backupPath,
          data: backupStr,
          encoding: 'utf8',
          success: () => {
            console.log('备份文件保存成功:', backupPath)
            resolve(backupPath)
          },
          fail: (err) => {
            console.error('备份文件保存失败:', err)
            reject(err)
          }
        })
        // #endif
        
        // 在H5平台使用下载文件
        // #ifdef H5
        const blob = new Blob([backupStr], { type: 'application/json' })
        const url = URL.createObjectURL(blob)
        const a = document.createElement('a')
        a.href = url
        a.download = `backup_${backup.info.timestamp}.json`
        document.body.appendChild(a)
        a.click()
        document.body.removeChild(a)
        URL.revokeObjectURL(url)
        resolve(a.download)
        // #endif
        
        // 在小程序平台,可以将备份保存到本地存储
        // #ifdef MP
        const backupKey = `backup_${backup.info.timestamp}`
        storage.set(backupKey, backup)
        resolve(backupKey)
        // #endif
      } catch (e) {
        console.error('保存备份失败:', e)
        reject(e)
      }
    })
  }
  
  /**
   * 从文件加载备份
   * @param {string} filePath 文件路径
   * @returns {Promise} 包含备份数据的Promise
   */
  loadBackupFromFile(filePath) {
    return new Promise((resolve, reject) => {
      try {
        // 仅在App平台可用
        // #ifdef APP-PLUS
        const fs = uni.getFileSystemManager()
        
        fs.readFile({
          filePath,
          encoding: 'utf8',
          success: (res) => {
            try {
              const backup = JSON.parse(res.data)
              console.log('备份文件加载成功')
              resolve(backup)
            } catch (e) {
              console.error('解析备份文件失败:', e)
              reject(e)
            }
          },
          fail: (err) => {
            console.error('读取备份文件失败:', err)
            reject(err)
          }
        })
        // #endif
        
        // 在小程序平台,从本地存储加载备份
        // #ifdef MP
        const backup = storage.get(filePath)
        if (backup) {
          resolve(backup)
        } else {
          reject(new Error('未找到备份'))
        }
        // #endif
        
        // H5平台需要用户手动选择文件
        // #ifdef H5
        reject(new Error('H5平台请使用恢复备份方法'))
        // #endif
      } catch (e) {
        console.error('加载备份失败:', e)
        reject(e)
      }
    })
  }
  
  /**
   * 恢复备份
   * @param {Object} backup 备份数据
   * @param {boolean} override 是否覆盖现有数据
   * @returns {boolean} 是否成功
   */
  restoreBackup(backup, override = false) {
    try {
      if (!backup || !backup.data || !backup.info) {
        console.error('备份数据无效')
        return false
      }
      
      const { data } = backup
      
      // 恢复数据
      Object.keys(data).forEach(key => {
        // 如果不覆盖且已存在,则跳过
        if (!override && storage.has(key)) {
          return
        }
        
        storage.set(key, data[key])
      })
      
      console.log('备份恢复成功')
      return true
    } catch (e) {
      console.error('恢复备份失败:', e)
      return false
    }
  }
  
  /**
   * H5平台从文件选择器恢复备份
   * @returns {Promise} 操作Promise
   */
  restoreBackupFromFileSelector() {
    return new Promise((resolve, reject) => {
      // 仅在H5平台可用
      // #ifdef H5
      try {
        // 创建文件输入元素
        const input = document.createElement('input')
        input.type = 'file'
        input.accept = 'application/json'
        
        input.onchange = (event) => {
          const file = event.target.files[0]
          
          if (!file) {
            reject(new Error('未选择文件'))
            return
          }
          
          const reader = new FileReader()
          
          reader.onload = (e) => {
            try {
              const backup = JSON.parse(e.target.result)
              
              // 恢复备份
              const result = this.restoreBackup(backup, true)
              
              if (result) {
                resolve(backup)
              } else {
                reject(new Error('恢复备份失败'))
              }
            } catch (err) {
              console.error('解析备份文件失败:', err)
              reject(err)
            }
          }
          
          reader.onerror = (err) => {
            console.error('读取备份文件失败:', err)
            reject(err)
          }
          
          reader.readAsText(file)
        }
        
        // 触发文件选择
        document.body.appendChild(input)
        input.click()
        document.body.removeChild(input)
      } catch (e) {
        console.error('恢复备份失败:', e)
        reject(e)
      }
      // #endif
      
      // 非H5平台
      // #ifndef H5
      reject(new Error('此方法仅在H5平台可用'))
      // #endif
    })
  }
  
  /**
   * 获取所有备份列表(小程序平台)
   * @returns {Array} 备份列表
   */
  getBackupList() {
    try {
      // 仅在小程序平台可用
      // #ifdef MP
      const keys = storage.keys()
      const backupKeys = keys.filter(key => key.startsWith('backup_'))
      
      const backupList = backupKeys.map(key => {
        const backup = storage.get(key)
        return {
          key,
          info: backup ? backup.info : null
        }
      }).filter(item => item.info)
      
      return backupList
      // #endif
      
      // 非小程序平台
      // #ifndef MP
      return []
      // #endif
    } catch (e) {
      console.error('获取备份列表失败:', e)
      return []
    }
  }
}

// 创建数据备份实例
const dataBackup = new DataBackup()

export default dataBackup

使用数据备份与恢复:

javascript
import dataBackup from '@/utils/backup'

// 创建并保存备份
function createBackup() {
  const backup = dataBackup.createBackup()
  
  if (backup) {
    dataBackup.saveBackupToFile(backup)
      .then(path => {
        uni.showToast({
          title: '备份成功',
          icon: 'success'
        })
        console.log('备份路径:', path)
      })
      .catch(err => {
        uni.showToast({
          title: '备份失败',
          icon: 'none'
        })
        console.error('备份失败:', err)
      })
  } else {
    uni.showToast({
      title: '创建备份失败',
      icon: 'none'
    })
  }
}

// 恢复备份(H5平台)
function restoreBackup() {
  // #ifdef H5
  dataBackup.restoreBackupFromFileSelector()
    .then(() => {
      uni.showToast({
        title: '恢复成功',
        icon: 'success'
      })
    })
    .catch(err => {
      uni.showToast({
        title: '恢复失败',
        icon: 'none'
      })
      console.error('恢复失败:', err)
    })
  // #endif
  
  // 小程序平台
  // #ifdef MP
  const backupList = dataBackup.getBackupList()
  
  if (backupList.length === 0) {
    uni.showToast({
      title: '没有可用的备份',
      icon: 'none'
    })
    return
  }
  
  // 显示备份列表
  uni.showActionSheet({
    itemList: backupList.map(item => {
      const date = new Date(item.info.timestamp)
      return `${date.toLocaleString()} (${item.info.count}项)`
    }),
    success: (res) => {
      const index = res.tapIndex
      const backup = storage.get(backupList[index].key)
      
      if (backup) {
        const result = dataBackup.restoreBackup(backup, true)
        
        if (result) {
          uni.showToast({
            title: '恢复成功',
            icon: 'success'
          })
        } else {
          uni.showToast({
            title: '恢复失败',
            icon: 'none'
          })
        }
      }
    }
  })
  // #endif
}

最佳实践

性能优化

  1. 批量操作:尽量减少存储操作次数,使用批量读写
  2. 延迟写入:对于频繁变化的数据,可以使用防抖或节流技术延迟写入
  3. 压缩数据:对于大型数据,可以使用压缩算法减小体积
  4. 分片存储:将大型数据分割成多个小块存储,避免超出单个键的大小限制
javascript
// 延迟写入示例
function debounce(func, wait) {
  let timeout
  return function(...args) {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}

// 使用延迟写入保存用户设置
const saveSettings = debounce((settings) => {
  storage.set('settings', settings)
}, 500)

// 用户更改设置时调用
function updateSettings(key, value) {
  const settings = storage.get('settings', {})
  settings[key] = value
  saveSettings(settings)
}

安全建议

  1. 敏感数据加密:使用加密存储敏感信息
  2. 定期清理:定期清理过期或不必要的数据
  3. 数据验证:读取数据时进行验证,防止使用损坏的数据
  4. 错误处理:妥善处理存储操作中的异常
javascript
// 数据验证示例
function getUserInfo() {
  try {
    const userInfo = storage.get('userInfo')
    
    // 验证数据完整性
    if (!userInfo || !userInfo.id || !userInfo.name) {
      console.warn('用户信息不完整')
      return null
    }
    
    return userInfo
  } catch (e) {
    console.error('获取用户信息失败:', e)
    return null
  }
}

存储策略

  1. 分级存储:根据数据重要性和使用频率选择不同的存储方式

    • 关键数据:加密存储
    • 常用数据:本地存储
    • 大型数据:文件存储或IndexedDB
    • 临时数据:内存缓存
  2. 存储生命周期:为不同类型的数据设置合适的过期时间

    • 会话数据:应用关闭时清除
    • 缓存数据:设置合理的过期时间
    • 用户数据:长期保存,但提供清除选项
javascript
// 分级存储示例
class DataManager {
  constructor() {
    // 内存缓存
    this.cache = new Map()
    
    // 本地存储
    this.storage = storage
    
    // 加密存储
    this.secureStorage = cryptoStorage
    
    // 清理过期缓存
    setInterval(() => {
      this.cleanExpiredCache()
    }, 60000) // 每分钟清理一次
  }
  
  // 设置缓存(内存)
  setCache(key, data, expires = 60000) {
    this.cache.set(key, {
      data,
      expires: expires ? Date.now() + expires : null
    })
  }
  
  // 获取缓存
  getCache(key) {
    const item = this.cache.get(key)
    
    if (!item) {
      return null
    }
    
    if (item.expires && item.expires < Date.now()) {
      this.cache.delete(key)
      return null
    }
    
    return item.data
  }
  
  // 清理过期缓存
  cleanExpiredCache() {
    for (const [key, item] of this.cache.entries()) {
      if (item.expires && item.expires < Date.now()) {
        this.cache.delete(key)
      }
    }
  }
  
  // 设置数据(根据类型选择存储方式)
  setData(key, data, options = {}) {
    const { type = 'normal', expires } = options
    
    switch (type) {
      case 'cache':
        // 内存缓存
        this.setCache(key, data, expires)
        break
      case 'secure':
        // 加密存储
        this.secureStorage.set(key, data, expires)
        break
      case 'normal':
      default:
        // 普通存储
        this.storage.set(key, data, expires)
        break
    }
  }
  
  // 获取数据(按优先级查找)
  getData(key, defaultValue = null, type = 'normal') {
    // 先查找内存缓存
    const cachedData = this.getCache(key)
    if (cachedData !== null) {
      return cachedData
    }
    
    // 根据类型查找存储
    let data = null
    
    switch (type) {
      case 'secure':
        data = this.secureStorage.get(key, defaultValue)
        break
      case 'normal':
      default:
        data = this.storage.get(key, defaultValue)
        break
    }
    
    // 如果找到数据,更新缓存
    if (data !== null && data !== defaultValue) {
      this.setCache(key, data)
    }
    
    return data
  }
}

// 创建数据管理实例
const dataManager = new DataManager()

// 使用示例
dataManager.setData('tempData', { value: 123 }, { type: 'cache', expires: 30000 }) // 30秒过期
dataManager.setData('userData', { name: '张三' }, { type: 'normal' }) // 永不过期
dataManager.setData('password', '123456', { type: 'secure', expires: 7 * 24 * 3600 * 1000 }) // 7天过期

const tempData = dataManager.getData('tempData')
const userData = dataManager.getData('userData')
const password = dataManager.getData('password', null, 'secure')

总结

本文详细介绍了uni-app中的数据存储方法和最佳实践,包括:

  1. 基础存储API:uni.setStorage、uni.getStorage等方法的使用
  2. 存储限制:不同平台的存储空间限制及应对策略
  3. 存储封装与管理:封装存储管理模块,支持过期时间、批量操作等
  4. 加密存储:敏感数据的加密存储方案
  5. 文件存储:大型数据的文件系统存储方法
  6. IndexedDB存储:H5平台的大容量数据存储方案
  7. 数据同步与备份:实现云端同步和本地备份恢复功能
  8. 最佳实践:性能优化、安全建议和存储策略

通过合理使用这些存储方法和技巧,可以有效管理uni-app应用中的数据,提高应用性能和用户体验。在实际开发中,应根据数据特性和业务需求选择合适的存储方案,并注意数据安全和性能优化。

一次开发,多端部署 - 让跨平台开发更简单