uni-app IndexedDB:客户端数据库的跨端使用

uni-app IndexedDB:客户端数据库的跨端使用

【免费下载链接】uni-app A cross-platform framework using Vue.js 【免费下载链接】uni-app 项目地址: https://gitcode.com/dcloud/uni-app

引言:为什么需要客户端数据库?

在现代移动应用开发中,数据存储是一个核心需求。传统的uni.setStorageuni.getStorageAPI虽然简单易用,但存在明显限制:

  • 存储容量限制:通常只有几MB
  • 数据类型单一:只能存储字符串
  • 缺乏查询能力:无法进行复杂的数据检索
  • 无事务支持:缺乏数据一致性保障

IndexedDB(索引数据库)作为浏览器原生的大容量存储解决方案,完美解决了这些问题。在uni-app中,通过合理的跨端适配,我们可以充分利用IndexedDB的强大功能。

IndexedDB核心概念解析

数据库结构体系

mermaid

关键特性对比

特性LocalStorageIndexedDB
存储容量5-10MB50%磁盘空间
数据类型仅字符串任意JS对象
查询能力强大索引查询
事务支持完整ACID事务
性能同步阻塞异步非阻塞

uni-app中的IndexedDB集成方案

方案一:H5平台原生支持

在H5平台,uni-app直接使用浏览器原生的IndexedDB API:

// 打开或创建数据库
function openDatabase() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('uniAppDB', 1)
    
    request.onerror = () => reject(request.error)
    request.onsuccess = () => resolve(request.result)
    
    request.onupgradeneeded = (event) => {
      const db = event.target.result
      if (!db.objectStoreNames.contains('users')) {
        const store = db.createObjectStore('users', { keyPath: 'id' })
        store.createIndex('name', 'name', { unique: false })
        store.createIndex('email', 'email', { unique: true })
      }
    }
  })
}

方案二:跨端兼容封装

对于非H5平台,我们需要封装统一的API接口:

class UniIndexedDB {
  constructor() {
    this.isH5 = typeof window !== 'undefined' && window.indexedDB
  }
  
  async openDB(name, version, upgradeCallback) {
    if (this.isH5) {
      return this.openIndexedDB(name, version, upgradeCallback)
    } else {
      // 其他平台使用本地存储模拟
      return this.openFallbackDB(name)
    }
  }
  
  async openIndexedDB(name, version, upgradeCallback) {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(name, version)
      
      request.onerror = () => reject(request.error)
      request.onsuccess = () => resolve(request.result)
      
      request.onupgradeneeded = (event) => {
        upgradeCallback(event.target.result, event.oldVersion, event.newVersion)
      }
    })
  }
  
  openFallbackDB(name) {
    // 实现本地存储的模拟逻辑
    console.log(`使用本地存储模拟IndexedDB: ${name}`)
    return Promise.resolve({
      objectStore: () => new FallbackObjectStore()
    })
  }
}

实战:用户数据管理案例

数据库初始化配置

// database.js
export class UserDatabase {
  constructor() {
    this.dbName = 'userManagement'
    this.version = 2
    this.db = null
  }
  
  async init() {
    this.db = await this.openDatabase()
    return this
  }
  
  async openDatabase() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version)
      
      request.onerror = reject
      request.onsuccess = (event) => resolve(event.target.result)
      
      request.onupgradeneeded = (event) => {
        const db = event.target.result
        this.createStores(db, event.oldVersion)
      }
    })
  }
  
  createStores(db, oldVersion) {
    if (oldVersion < 1) {
      // 版本1:创建用户存储
      const userStore = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true })
      userStore.createIndex('email', 'email', { unique: true })
      userStore.createIndex('createdAt', 'createdAt', { unique: false })
    }
    
    if (oldVersion < 2) {
      // 版本2:添加用户配置存储
      const configStore = db.createObjectStore('user_config', { keyPath: 'userId' })
      configStore.createIndex('theme', 'theme', { unique: false })
      configStore.createIndex('language', 'language', { unique: false })
    }
  }
}

CRUD操作封装

// user-service.js
export class UserService {
  constructor(database) {
    this.db = database
  }
  
  // 添加用户
  async addUser(user) {
    return this.transaction('users', 'readwrite', (store) => {
      user.createdAt = new Date()
      user.updatedAt = new Date()
      return store.add(user)
    })
  }
  
  // 查询用户
  async getUserById(id) {
    return this.transaction('users', 'readonly', (store) => {
      return store.get(id)
    })
  }
  
  // 按邮箱查询
  async getUserByEmail(email) {
    return this.transaction('users', 'readonly', (store) => {
      const index = store.index('email')
      return index.get(email)
    })
  }
  
  // 更新用户
  async updateUser(id, updates) {
    return this.transaction('users', 'readwrite', async (store) => {
      const user = await store.get(id)
      if (!user) throw new Error('User not found')
      
      Object.assign(user, updates, { updatedAt: new Date() })
      return store.put(user)
    })
  }
  
  // 事务封装
  async transaction(storeName, mode, operation) {
    const transaction = this.db.transaction([storeName], mode)
    const store = transaction.objectStore(storeName)
    
    return new Promise((resolve, reject) => {
      transaction.oncomplete = () => resolve()
      transaction.onerror = () => reject(transaction.error)
      
      const request = operation(store)
      if (request) {
        request.onsuccess = () => resolve(request.result)
        request.onerror = () => reject(request.error)
      }
    })
  }
}

高级查询示例

// 复杂查询功能
async queryUsers(options = {}) {
  const {
    page = 1,
    pageSize = 20,
    sortBy = 'createdAt',
    sortOrder = 'desc',
    filters = {}
  } = options
  
  return this.transaction('users', 'readonly', (store) => {
    const index = store.index(sortBy)
    const range = IDBKeyRange.lowerBound(0)
    const direction = sortOrder === 'desc' ? 'prev' : 'next'
    
    return new Promise((resolve) => {
      const results = []
      let count = 0
      let skipped = 0
      
      index.openCursor(range, direction).onsuccess = (event) => {
        const cursor = event.target.result
        if (!cursor) {
          resolve({ data: results, total: count, page, pageSize })
          return
        }
        
        count++
        
        // 应用过滤器
        const user = cursor.value
        let match = true
        for (const [key, value] of Object.entries(filters)) {
          if (user[key] !== value) {
            match = false
            break
          }
        }
        
        // 分页逻辑
        if (match) {
          skipped++
          if (skipped > (page - 1) * pageSize && results.length < pageSize) {
            results.push(user)
          }
        }
        
        if (results.length < pageSize) {
          cursor.continue()
        }
      }
    })
  })
}

性能优化策略

批量操作处理

// batch-operations.js
export class BatchProcessor {
  constructor(db, storeName, batchSize = 100) {
    this.db = db
    this.storeName = storeName
    this.batchSize = batchSize
    this.pendingOperations = []
  }
  
  async add(operation) {
    this.pendingOperations.push(operation)
    if (this.pendingOperations.length >= this.batchSize) {
      await this.flush()
    }
  }
  
  async flush() {
    if (this.pendingOperations.length === 0) return
    
    const operations = [...this.pendingOperations]
    this.pendingOperations = []
    
    return this.db.transaction([this.storeName], 'readwrite', (store) => {
      return Promise.all(operations.map(op => {
        return new Promise((resolve, reject) => {
          const request = op(store)
          request.onsuccess = resolve
          request.onerror = reject
        })
      }))
    })
  }
  
  // 使用示例
  async processUsers(users) {
    const batch = new BatchProcessor(this.db, 'users')
    
    for (const user of users) {
      await batch.add((store) => store.put(user))
    }
    
    await batch.flush()
  }
}

索引优化建议

mermaid

错误处理与调试

健壮的错误处理机制

// error-handler.js
export class DBErrorHandler {
  static handleError(error, context = '') {
    console.error(`Database error [${context}]:`, error)
    
    switch (error.name) {
      case 'NotFoundError':
        return this.handleNotFound(error, context)
      case 'ConstraintError':
        return this.handleConstraint(error, context)
      case 'QuotaExceededError':
        return this.handleQuotaExceeded(error, context)
      default:
        return this.handleUnknown(error, context)
    }
  }
  
  static handleNotFound(error, context) {
    return {
      code: 'NOT_FOUND',
      message: `请求的数据不存在: ${context}`,
      originalError: error
    }
  }
  
  static handleConstraint(error, context) {
    return {
      code: 'CONSTRAINT_VIOLATION',
      message: `数据约束冲突: ${context}`,
      originalError: error
    }
  }
  
  static handleQuotaExceeded(error, context) {
    // 自动清理旧数据
    this.cleanupOldData()
    return {
      code: 'STORAGE_FULL',
      message: '存储空间不足,已自动清理',
      originalError: error
    }
  }
  
  static async cleanupOldData() {
    // 实现数据清理逻辑
    console.log('执行存储空间清理')
  }
}

// 使用示例
try {
  await userService.addUser(userData)
} catch (error) {
  const handledError = DBErrorHandler.handleError(error, '添加用户')
  uni.showToast({ title: handledError.message, icon: 'none' })
}

调试工具与技巧

// debug-utils.js
export class DBDebugger {
  static enableDebugging() {
    // 重写原生方法添加日志
    const originalOpen = indexedDB.open
    indexedDB.open = function(name, version) {
      console.log(`Opening database: ${name}, version: ${version}`)
      const request = originalOpen.call(this, name, version)
      
      request.onerror = (e) => console.error('Open error:', e.target.error)
      request.onsuccess = (e) => console.log('Open success:', e.target.result)
      request.onupgradeneeded = (e) => console.log('Upgrade needed:', e.oldVersion, '→', e.newVersion)
      
      return request
    }
  }
  
  static async inspectDatabase(name) {
    const databases = await indexedDB.databases()
    const dbInfo = databases.find(db => db.name === name)
    
    console.log('Database info:', dbInfo)
    return dbInfo
  }
  
  static async exportData(storeName) {
    return new Promise((resolve) => {
      const request = indexedDB.open(this.dbName)
      request.onsuccess = (event) => {
        const db = event.target.result
        const transaction = db.transaction([storeName], 'readonly')
        const store = transaction.objectStore(storeName)
        const data = []
        
        store.openCursor().onsuccess = (e) => {
          const cursor = e.target.result
          if (cursor) {
            data.push(cursor.value)
            cursor.continue()
          } else {
            resolve(data)
          }
        }
      }
    })
  }
}

跨端兼容方案

平台检测与适配

// platform-adapter.js
export class PlatformAdapter {
  static getPlatform() {
    // uni-app平台检测
    const { platform } = uni.getSystemInfoSync()
    return platform
  }
  
  static supportsIndexedDB() {
    const platform = this.getPlatform()
    return platform === 'web' || platform === 'h5'
  }
  
  static getStorageAdapter() {
    if (this.supportsIndexedDB()) {
      return new IndexedDBAdapter()
    } else {
      // 其他平台使用优化的本地存储方案
      return new FallbackStorageAdapter()
    }
  }
}

class IndexedDBAdapter {
  // IndexedDB具体实现
  async setItem(key, value) {
    const db = await this.getDB()
    return db.transaction(['storage'], 'readwrite', (store) => {
      return store.put({ key, value, timestamp: Date.now() })
    })
  }
  
  async getItem(key) {
    const db = await this.getDB()
    return db.transaction(['storage'], 'readonly', (store) => {
      return store.get(key).then(result => result?.value)
    })
  }
}

class FallbackStorageAdapter {
  // 本地存储模拟实现
  async setItem(key, value) {
    try {
      uni.setStorageSync(key, JSON.stringify({
        value,
        timestamp: Date.now()
      }))
    } catch (error) {
      throw new Error('Storage quota exceeded')
    }
  }
  
  async getItem(key) {
    const data = uni.getStorageSync(key)
    if (!data) return null
    
    try {
      const parsed = JSON.parse(data)
      return parsed.value
    } catch {
      return data
    }
  }
}

最佳实践总结

数据模型设计原则

mermaid

容量管理与监控

// storage-monitor.js
export class StorageMonitor {
  static async getUsage() {
    if (!navigator.storage) return null
    
    try {
      const estimate = await navigator.storage.estimate()
      return {
        usage: estimate.usage,
        quota: estimate.quota,
        percentage: (estimate.usage / estimate.quota * 100).toFixed(1)
      }
    } catch (error) {
      console.warn('Storage estimation not supported')
      return null
    }
  }
  
  static async checkQuota() {
    const usage = await this.getUsage()
    if (!usage) return true
    
    const warningThreshold = 0.8 // 80%使用率警告
    if (usage.percentage > warningThreshold * 100) {
      console.warn(`存储使用率过高: ${usage.percentage}%`)
      return false
    }
    
    return true
  }
  
  static setupMonitoring(interval = 300000) { // 5分钟检查一次
    setInterval(async () => {
      const hasSpace = await this.checkQuota()
      if (!hasSpace) {
        this.triggerCleanup()
      }
    }, interval)
  }
  
  static triggerCleanup() {
    // 触发数据清理逻辑
    console.log('存储空间不足,触发清理流程')
  }
}

结语

IndexedDB在uni-app中的使用虽然需要面对跨端兼容的挑战,但其带来的性能提升和数据管理能力是值得投入的。通过本文介绍的方案,你可以:

  1. 实现大容量数据存储:突破本地存储的限制
  2. 支持复杂数据操作:享受完整的数据库功能
  3. 保证跨端一致性:统一的API接口设计
  4. 获得性能优化:异步操作和批量处理

记住,良好的数据模型设计和错误处理机制是成功使用IndexedDB的关键。在实际项目中,建议先从关键业务数据开始试点,逐步推广到整个应用的数据管理体系中。

【免费下载链接】uni-app A cross-platform framework using Vue.js 【免费下载链接】uni-app 项目地址: https://gitcode.com/dcloud/uni-app

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值