IndexedDB
浏览器客户端存储解决方案,用于存储大量结构化数据。
基本信息
- 简介: 浏览器内置的 NoSQL 数据库
- 文档: https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API
- 特点: 支持事务、索引、大容量存储
使用场景
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. 实时协作应用
本地数据管理:
- 草稿自动保存
- 操作历史(撤销/重做)
- 冲突解决缓存
- 版本控制
对比其他存储方案
| 特性 | IndexedDB | localStorage | sessionStorage | Chrome Storage |
|---|---|---|---|---|
| 容量 | 数百 MB - GB | 5-10 MB | 5-10 MB | 5-10 MB |
| 数据类型 | 对象、Blob、File | 字符串 | 字符串 | JSON 对象 |
| 异步 | ✅ | ❌ | ❌ | ✅ |
| 索引查询 | ✅ | ❌ | ❌ | ❌ |
| 事务支持 | ✅ | ❌ | ❌ | ❌ |
| 持久化 | ✅ | ✅ | ❌ | ✅ |
| 跨标签共享 | ✅ | ✅ | ❌ | ✅ |
IDB 库
基本信息
- 简介: IndexedDB 的 Promise 封装库
- GitHub: https://github.com/jakearchibald/idb
- 特点: 简化 API、TypeScript 支持、体积小
安装
bash
pnpm add idbbash
bun add idbbash
npm install idbbash
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
- 打开 Chrome DevTools
- 切换到 Application 标签
- 左侧找到 Storage → IndexedDB
- 查看数据库、对象存储和数据
常用操作
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. 性能考虑
- 大量数据使用游标
- 批量操作使用事务
- 避免频繁打开/关闭数据库