uni-app IndexedDB:客户端数据库的跨端使用
【免费下载链接】uni-app A cross-platform framework using Vue.js 项目地址: https://gitcode.com/dcloud/uni-app
引言:为什么需要客户端数据库?
在现代移动应用开发中,数据存储是一个核心需求。传统的uni.setStorage和uni.getStorageAPI虽然简单易用,但存在明显限制:
- 存储容量限制:通常只有几MB
- 数据类型单一:只能存储字符串
- 缺乏查询能力:无法进行复杂的数据检索
- 无事务支持:缺乏数据一致性保障
IndexedDB(索引数据库)作为浏览器原生的大容量存储解决方案,完美解决了这些问题。在uni-app中,通过合理的跨端适配,我们可以充分利用IndexedDB的强大功能。
IndexedDB核心概念解析
数据库结构体系
关键特性对比
| 特性 | LocalStorage | IndexedDB |
|---|---|---|
| 存储容量 | 5-10MB | 50%磁盘空间 |
| 数据类型 | 仅字符串 | 任意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()
}
}
索引优化建议
错误处理与调试
健壮的错误处理机制
// 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
}
}
}
最佳实践总结
数据模型设计原则
容量管理与监控
// 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中的使用虽然需要面对跨端兼容的挑战,但其带来的性能提升和数据管理能力是值得投入的。通过本文介绍的方案,你可以:
- 实现大容量数据存储:突破本地存储的限制
- 支持复杂数据操作:享受完整的数据库功能
- 保证跨端一致性:统一的API接口设计
- 获得性能优化:异步操作和批量处理
记住,良好的数据模型设计和错误处理机制是成功使用IndexedDB的关键。在实际项目中,建议先从关键业务数据开始试点,逐步推广到整个应用的数据管理体系中。
【免费下载链接】uni-app A cross-platform framework using Vue.js 项目地址: https://gitcode.com/dcloud/uni-app
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



