Skip to content

IndexedDB

浏览器客户端存储解决方案,用于存储大量结构化数据。

基本信息

使用场景

1. 大量结构化数据存储

适合存储超过 localStorage 限制(5-10MB)的数据:

  • 存储数百 MB 甚至 GB 级别的数据
  • 支持复杂的对象和数组
  • 适合需要本地持久化的应用数据
typescript
// 存储大量商品数据
const products = [
  { id: 1, name: '商品1', price: 99, images: [...] },
  { id: 2, name: '商品2', price: 199, images: [...] },
  // ... 数千条数据
]

2. 离线应用(PWA)

实现完整的离线功能:

  • 缓存 API 响应数据
  • 存储用户操作记录
  • 离线队列管理
  • 数据同步
typescript
// 离线时保存操作
async function saveOfflineAction(action) {
  const db = await openDB('app-db')
  await db.add('sync-queue', {
    action,
    timestamp: Date.now(),
    synced: false
  })
}

3. 高性能查询需求

支持索引,可快速查询大量数据:

  • 按多个字段搜索
  • 范围查询
  • 排序和分页
  • 比遍历数组效率高得多
typescript
// 按价格范围查询
const products = await db.getAllFromIndex(
  'products',
  'price',
  IDBKeyRange.bound(100, 500)
)

4. 文件和二进制数据

存储各种类型的文件:

  • 图片、音频、视频
  • 用户上传的文件
  • Blob 和 ArrayBuffer
  • Base64 数据
typescript
// 存储图片
await db.put('images', {
  id: 1,
  name: 'avatar.jpg',
  blob: imageBlob,
  size: imageBlob.size
})

5. 浏览器扩展数据持久化

扩展开发中的数据存储:

  • 扩展配置和状态
  • 缓存网络请求
  • 比 Chrome Storage API 容量更大
  • 支持复杂数据结构

6. 实时协作应用

本地数据管理:

  • 草稿自动保存
  • 操作历史(撤销/重做)
  • 冲突解决缓存
  • 版本控制

对比其他存储方案

特性IndexedDBlocalStoragesessionStorageChrome Storage
容量数百 MB - GB5-10 MB5-10 MB5-10 MB
数据类型对象、Blob、File字符串字符串JSON 对象
异步
索引查询
事务支持
持久化
跨标签共享

IDB 库

基本信息

安装

bash
pnpm add idb
bash
bun add idb
bash
npm install idb
bash
yarn add idb

基础用法

创建数据库

typescript
import { openDB } from 'idb'

const db = await openDB('my-database', 1, {
  upgrade(db) {
    // 创建对象存储(表)
    const store = db.createObjectStore('users', {
      keyPath: 'id',
      autoIncrement: true
    })
    
    // 创建索引
    store.createIndex('email', 'email', { unique: true })
    store.createIndex('age', 'age')
    store.createIndex('name', 'name')
  }
})

增删改查

typescript
// 添加数据
await db.add('users', {
  name: '张三',
  email: 'zhangsan@example.com',
  age: 25
})

// 批量添加
const tx = db.transaction('users', 'readwrite')
await Promise.all([
  tx.store.add({ name: '李四', email: 'lisi@example.com', age: 30 }),
  tx.store.add({ name: '王五', email: 'wangwu@example.com', age: 28 }),
  tx.done
])

// 获取单条数据
const user = await db.get('users', 1)

// 获取所有数据
const allUsers = await db.getAll('users')

// 通过索引查询
const user = await db.getFromIndex('users', 'email', 'zhangsan@example.com')

// 更新数据
await db.put('users', {
  id: 1,
  name: '张三',
  email: 'zhangsan@example.com',
  age: 26
})

// 删除数据
await db.delete('users', 1)

// 清空表
await db.clear('users')

高级查询

typescript
// 范围查询
const youngUsers = await db.getAllFromIndex(
  'users',
  'age',
  IDBKeyRange.upperBound(30)
)

// 区间查询
const users = await db.getAllFromIndex(
  'users',
  'age',
  IDBKeyRange.bound(20, 30)
)

// 游标遍历
let cursor = await db.transaction('users').store.openCursor()
while (cursor) {
  console.log(cursor.key, cursor.value)
  cursor = await cursor.continue()
}

// 条件过滤
const activeUsers = []
let cursor = await db.transaction('users').store.openCursor()
while (cursor) {
  if (cursor.value.active) {
    activeUsers.push(cursor.value)
  }
  cursor = await cursor.continue()
}

PWA 集成

离线数据缓存

IndexedDB 是 PWA 离线功能的核心数据层。

缓存 API 响应

typescript
// Service Worker 中缓存 API 数据
import { openDB } from 'idb'

async function cacheAPIResponse(url: string, data: any) {
  const db = await openDB('api-cache', 1, {
    upgrade(db) {
      db.createObjectStore('responses', { keyPath: 'url' })
    }
  })
  
  await db.put('responses', {
    url,
    data,
    timestamp: Date.now()
  })
}

async function getCachedResponse(url: string, maxAge = 3600000) {
  const db = await openDB('api-cache', 1)
  const cached = await db.get('responses', url)
  
  if (!cached) return null
  
  const age = Date.now() - cached.timestamp
  if (age > maxAge) {
    await db.delete('responses', url)
    return null
  }
  
  return cached.data
}

// 在 Service Worker 中使用
self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(
      fetch(event.request)
        .then(async response => {
          const data = await response.clone().json()
          await cacheAPIResponse(event.request.url, data)
          return response
        })
        .catch(async () => {
          const cached = await getCachedResponse(event.request.url)
          if (cached) {
            return new Response(JSON.stringify(cached), {
              headers: { 'Content-Type': 'application/json' }
            })
          }
          throw new Error('No cached data')
        })
    )
  }
})

离线操作队列

typescript
// 离线时保存操作
async function saveOfflineAction(action: any) {
  const db = await openDB('sync-queue', 1, {
    upgrade(db) {
      const store = db.createObjectStore('actions', {
        keyPath: 'id',
        autoIncrement: true
      })
      store.createIndex('synced', 'synced')
      store.createIndex('timestamp', 'timestamp')
    }
  })
  
  await db.add('actions', {
    action,
    timestamp: Date.now(),
    synced: false
  })
}

// 在线时同步
async function syncPendingActions() {
  const db = await openDB('sync-queue', 1)
  const pending = await db.getAllFromIndex('actions', 'synced', false)
  
  for (const item of pending) {
    try {
      await fetch('/api/sync', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(item.action)
      })
      
      // 标记为已同步
      await db.put('actions', { ...item, synced: true })
    } catch (error) {
      console.error('Sync failed:', error)
    }
  }
}

// 监听网络状态
window.addEventListener('online', syncPendingActions)

缓存策略实现

Cache First(缓存优先)

typescript
async function cacheFirst(request: Request) {
  const db = await openDB('cache-db', 1)
  
  // 先查缓存
  const cached = await db.get('cache', request.url)
  if (cached) return cached.data
  
  // 再请求网络
  const response = await fetch(request)
  const data = await response.clone().json()
  
  await db.put('cache', {
    url: request.url,
    data,
    timestamp: Date.now()
  })
  
  return data
}

Network First(网络优先)

typescript
async function networkFirst(request: Request) {
  const db = await openDB('cache-db', 1)
  
  try {
    const response = await fetch(request)
    const data = await response.clone().json()
    
    await db.put('cache', {
      url: request.url,
      data,
      timestamp: Date.now()
    })
    
    return data
  } catch {
    const cached = await db.get('cache', request.url)
    if (cached) return cached.data
    throw new Error('No data available')
  }
}

Stale While Revalidate(后台更新)

typescript
async function staleWhileRevalidate(request: Request) {
  const db = await openDB('cache-db', 1)
  const cached = await db.get('cache', request.url)
  
  // 后台更新
  fetch(request).then(async response => {
    const data = await response.clone().json()
    await db.put('cache', {
      url: request.url,
      data,
      timestamp: Date.now()
    })
  })
  
  // 立即返回缓存
  return cached ? cached.data : fetch(request).then(r => r.json())
}

实际应用示例

邮件客户端

typescript
import { openDB } from 'idb'

// 初始化数据库
async function initEmailDB() {
  return await openDB('email-app', 1, {
    upgrade(db) {
      // 邮件存储
      const emails = db.createObjectStore('emails', { keyPath: 'id' })
      emails.createIndex('folder', 'folder')
      emails.createIndex('read', 'read')
      emails.createIndex('timestamp', 'timestamp')
      
      // 附件存储
      const attachments = db.createObjectStore('attachments', { keyPath: 'id' })
      attachments.createIndex('emailId', 'emailId')
    }
  })
}

// 缓存邮件列表
async function cacheEmails(emails: any[]) {
  const db = await initEmailDB()
  const tx = db.transaction('emails', 'readwrite')
  
  await Promise.all([
    ...emails.map(email => tx.store.put(email)),
    tx.done
  ])
}

// 获取未读邮件
async function getUnreadEmails() {
  const db = await initEmailDB()
  return await db.getAllFromIndex('emails', 'read', false)
}

// 保存附件
async function saveAttachment(emailId: number, file: Blob) {
  const db = await initEmailDB()
  await db.add('attachments', {
    id: Date.now(),
    emailId,
    name: file.name,
    blob: file,
    size: file.size
  })
}

笔记应用

typescript
import { openDB } from 'idb'

// 初始化笔记数据库
async function initNotesDB() {
  return await openDB('notes-app', 1, {
    upgrade(db) {
      // 笔记存储
      const notes = db.createObjectStore('notes', { keyPath: 'id' })
      notes.createIndex('title', 'title')
      notes.createIndex('updatedAt', 'updatedAt')
      notes.createIndex('synced', 'synced')
      
      // 草稿存储
      db.createObjectStore('drafts', { keyPath: 'id' })
      
      // 历史记录
      const history = db.createObjectStore('history', {
        keyPath: 'id',
        autoIncrement: true
      })
      history.createIndex('noteId', 'noteId')
    }
  })
}

// 自动保存草稿
let saveTimer: number
async function autoSaveDraft(note: any) {
  clearTimeout(saveTimer)
  
  saveTimer = setTimeout(async () => {
    const db = await initNotesDB()
    await db.put('drafts', {
      ...note,
      lastModified: Date.now(),
      synced: false
    })
  }, 1000)
}

// 保存历史记录(撤销/重做)
async function saveHistory(noteId: number, content: string) {
  const db = await initNotesDB()
  await db.add('history', {
    noteId,
    content,
    timestamp: Date.now()
  })
  
  // 只保留最近 50 条历史
  const all = await db.getAllFromIndex('history', 'noteId', noteId)
  if (all.length > 50) {
    const toDelete = all.slice(0, all.length - 50)
    for (const item of toDelete) {
      await db.delete('history', item.id)
    }
  }
}

// 同步笔记
async function syncNotes() {
  const db = await initNotesDB()
  const unsyncedNotes = await db.getAllFromIndex('notes', 'synced', false)
  
  for (const note of unsyncedNotes) {
    try {
      await fetch('/api/notes', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(note)
      })
      
      note.synced = true
      await db.put('notes', note)
    } catch (error) {
      console.error('Sync failed:', error)
    }
  }
}

图片编辑器

typescript
import { openDB } from 'idb'

// 初始化编辑器数据库
async function initEditorDB() {
  return await openDB('image-editor', 1, {
    upgrade(db) {
      // 项目存储
      const projects = db.createObjectStore('projects', { keyPath: 'id' })
      projects.createIndex('name', 'name')
      projects.createIndex('updatedAt', 'updatedAt')
      
      // 图层存储
      const layers = db.createObjectStore('layers', { keyPath: 'id' })
      layers.createIndex('projectId', 'projectId')
      
      // 历史记录
      const history = db.createObjectStore('history', {
        keyPath: 'id',
        autoIncrement: true
      })
      history.createIndex('projectId', 'projectId')
    }
  })
}

// 保存项目
async function saveProject(project: any, imageBlob: Blob) {
  const db = await initEditorDB()
  await db.put('projects', {
    ...project,
    imageBlob,
    updatedAt: Date.now()
  })
}

// 保存编辑历史
async function saveEditHistory(projectId: number, snapshot: Blob) {
  const db = await initEditorDB()
  await db.add('history', {
    projectId,
    snapshot,
    timestamp: Date.now()
  })
}

// 撤销操作
async function undo(projectId: number) {
  const db = await initEditorDB()
  const history = await db.getAllFromIndex('history', 'projectId', projectId)
  
  if (history.length > 0) {
    const lastState = history[history.length - 1]
    return lastState.snapshot
  }
  
  return null
}

最佳实践

1. 版本管理

typescript
const db = await openDB('my-db', 2, {
  upgrade(db, oldVersion, newVersion, transaction) {
    // 版本 1 -> 2 的升级
    if (oldVersion < 1) {
      db.createObjectStore('users', { keyPath: 'id' })
    }
    
    if (oldVersion < 2) {
      const store = transaction.objectStore('users')
      store.createIndex('email', 'email', { unique: true })
    }
  }
})

2. 数据过期管理

typescript
async function cleanExpiredData(storeName: string, maxAge: number) {
  const db = await openDB('my-db', 1)
  const all = await db.getAll(storeName)
  const now = Date.now()
  
  for (const item of all) {
    if (now - item.timestamp > maxAge) {
      await db.delete(storeName, item.id)
    }
  }
}

// 定期清理
setInterval(() => {
  cleanExpiredData('cache', 7 * 24 * 60 * 60 * 1000) // 7 天
}, 24 * 60 * 60 * 1000) // 每天执行

3. 限制存储大小

typescript
async function limitStorageSize(storeName: string, maxItems: number) {
  const db = await openDB('my-db', 1)
  const all = await db.getAllFromIndex(storeName, 'timestamp')
  
  if (all.length > maxItems) {
    // 删除最旧的数据
    const toDelete = all.slice(0, all.length - maxItems)
    for (const item of toDelete) {
      await db.delete(storeName, item.id)
    }
  }
}

4. 错误处理

typescript
async function safeDBOperation<T>(
  operation: () => Promise<T>,
  fallback: T
): Promise<T> {
  try {
    return await operation()
  } catch (error) {
    console.error('IndexedDB error:', error)
    return fallback
  }
}

// 使用
const users = await safeDBOperation(
  () => db.getAll('users'),
  []
)

5. 事务管理

typescript
// 批量操作使用事务
async function batchUpdate(users: any[]) {
  const db = await openDB('my-db', 1)
  const tx = db.transaction('users', 'readwrite')
  
  await Promise.all([
    ...users.map(user => tx.store.put(user)),
    tx.done
  ])
}

6. 性能优化

typescript
// 使用游标进行大量数据处理
async function processLargeDataset(callback: (item: any) => void) {
  const db = await openDB('my-db', 1)
  const tx = db.transaction('users')
  let cursor = await tx.store.openCursor()
  
  while (cursor) {
    callback(cursor.value)
    cursor = await cursor.continue()
  }
}

// 分页查询
async function getPaginatedData(page: number, pageSize: number) {
  const db = await openDB('my-db', 1)
  const tx = db.transaction('users')
  let cursor = await tx.store.openCursor()
  
  const skip = page * pageSize
  const results = []
  let count = 0
  
  while (cursor && results.length < pageSize) {
    if (count >= skip) {
      results.push(cursor.value)
    }
    count++
    cursor = await cursor.continue()
  }
  
  return results
}

调试工具

Chrome DevTools

  1. 打开 Chrome DevTools
  2. 切换到 Application 标签
  3. 左侧找到 StorageIndexedDB
  4. 查看数据库、对象存储和数据

常用操作

typescript
// 查看所有数据库
indexedDB.databases().then(console.log)

// 删除数据库
await indexedDB.deleteDatabase('my-db')

// 查看存储配额
const estimate = await navigator.storage.estimate()
console.log(`已使用: ${estimate.usage} / ${estimate.quota}`)

注意事项

1. 浏览器兼容性

IndexedDB 兼容性非常好,所有现代浏览器都支持:

  • Chrome: 24+ (2013年)
  • Firefox: 16+ (2012年)
  • Safari: 10+ (2016年)
  • Edge: 12+ (2015年)
  • Opera: 15+ (2013年)
  • 移动端: iOS Safari 10+, Chrome Android, Samsung Internet

兼容性说明

  • 覆盖率: 全球 97%+ 的浏览器支持
  • 隐私模式: Safari 隐私模式下有存储限制,但功能正常
  • 推荐: 使用 idb 库获得更好的 API 体验和 TypeScript 支持

Safari 隐私模式

Safari 在隐私模式下,IndexedDB 可用但有限制:

  • 存储配额较小
  • 关闭浏览器后数据会被清除
  • 建议检测并提示用户
typescript
// 检测是否支持 IndexedDB
if (!('indexedDB' in window)) {
  console.warn('浏览器不支持 IndexedDB')
}

// 检测是否在隐私模式(Safari)
async function isPrivateMode() {
  try {
    const db = await openDB('test', 1)
    await db.close()
    return false
  } catch {
    return true
  }
}

2. 存储限制

  • Chrome: 可用磁盘空间的 60%
  • Firefox: 可用磁盘空间的 50%
  • Safari: 1GB(可请求更多)

3. 安全性

  • IndexedDB 不加密
  • 不要存储敏感信息(密码、令牌)
  • 遵循同源策略

4. 性能考虑

  • 大量数据使用游标
  • 批量操作使用事务
  • 避免频繁打开/关闭数据库

相关资源