19 KiB
19 KiB
IndexedDB 与 localStorage 对比分析
📋 概述
本文档详细对比 IndexedDB 和 localStorage 两种浏览器存储方案,并分析在 XYZW 控制台项目中的应用场景。
📊 核心特性对比
| 特性 | localStorage | IndexedDB | 优势方 |
|---|---|---|---|
| 存储容量 | 5-10MB | 50MB-无限制* | 🏆 IndexedDB |
| 数据类型 | 仅字符串 | 任意类型(对象、ArrayBuffer等) | 🏆 IndexedDB |
| API 复杂度 | 简单(同步) | 复杂(异步) | 🏆 localStorage |
| 性能(大数据) | 慢 | 快 | 🏆 IndexedDB |
| 性能(小数据) | 快 | 慢 | 🏆 localStorage |
| 事务支持 | ❌ | ✅ | 🏆 IndexedDB |
| 索引和查询 | ❌ | ✅ | 🏆 IndexedDB |
| 浏览器支持 | 所有浏览器 | 现代浏览器 | 🏆 localStorage |
| 持久化 | 永久 | 永久 | 🤝 平局 |
| 同源策略 | 遵守 | 遵守 | 🤝 平局 |
*注:IndexedDB 的实际容量因浏览器而异,通常至少 50MB,部分浏览器可达几百 MB 或更多。
🔧 技术细节对比
1. 存储容量限制
localStorage
浏览器限制:
├─ Chrome/Edge: 5-10MB (每个域名)
├─ Firefox: 10MB
├─ Safari: 5MB
└─ 移动浏览器: 2.5-5MB
超出限制时:
└─ 抛出 QuotaExceededError 异常 ❌
IndexedDB
浏览器限制:
├─ Chrome/Edge: 可用磁盘空间的 60%
│ ├─ 持久存储模式: 无限制*
│ └─ 临时存储模式: ~50MB 起步
├─ Firefox: 可用磁盘空间的 50%
├─ Safari: 1GB 起步
└─ 移动浏览器: 50-200MB
超出限制时:
└─ 提示用户授权更多空间 ⚠️
实际案例:
- localStorage: 存储 90 个 Token (~5MB) → 可能超限 ❌
- IndexedDB: 存储 1000+ 个 Token (~50MB+) → 完全没问题 ✅
2. 数据类型支持
localStorage
// ❌ 只能存储字符串
localStorage.setItem('key', 'value') // ✅ 可以
localStorage.setItem('key', 123) // ⚠️ 自动转为 '123'
localStorage.setItem('key', {a: 1}) // ⚠️ 自动转为 '[object Object]'
// 需要手动序列化/反序列化
const data = { name: 'token', value: 123 }
localStorage.setItem('key', JSON.stringify(data)) // 存储
const parsed = JSON.parse(localStorage.getItem('key')) // 读取
// ArrayBuffer 需要转换
const arrayBuffer = new Uint8Array([1, 2, 3]).buffer
const base64 = arrayBufferToBase64(arrayBuffer) // 转 Base64
localStorage.setItem('key', base64)
IndexedDB
// ✅ 可以直接存储任意类型
db.put({
name: 'token',
value: 123, // ✅ 数字
data: { nested: true }, // ✅ 对象
buffer: arrayBuffer, // ✅ ArrayBuffer(无需转换)
date: new Date(), // ✅ 日期
blob: new Blob([...]) // ✅ Blob
})
// 读取时直接获得原始类型
const result = await db.get('token')
console.log(result.buffer) // ArrayBuffer,不是字符串
当前项目中的影响:
- localStorage: binFileContent (ArrayBuffer) → 必须转 Base64 → 占用空间增加 33%
- IndexedDB: binFileContent (ArrayBuffer) → 直接存储 → 节省 33% 空间
3. API 复杂度
localStorage - 简单同步 API
// ✅ 超简单,同步操作
localStorage.setItem('token', 'value') // 存储
const token = localStorage.getItem('token') // 读取
localStorage.removeItem('token') // 删除
localStorage.clear() // 清空
// 可以直接在任何地方使用
function saveToken(token) {
localStorage.setItem('gameToken', token) // 立即完成
console.log('已保存') // 马上执行
}
IndexedDB - 复杂异步 API
// ❌ 较复杂,需要打开数据库、创建事务
// 1. 打开数据库
const request = indexedDB.open('GameDB', 1)
request.onupgradeneeded = (event) => {
const db = event.target.result
// 创建对象存储(类似表)
const store = db.createObjectStore('tokens', { keyPath: 'id' })
store.createIndex('name', 'name', { unique: false })
}
request.onsuccess = (event) => {
const db = event.target.result
// 2. 创建事务
const transaction = db.transaction(['tokens'], 'readwrite')
const store = transaction.objectStore('tokens')
// 3. 执行操作
const request = store.add({ id: 1, name: 'token', value: '...' })
request.onsuccess = () => {
console.log('已保存') // 异步完成
}
request.onerror = () => {
console.error('保存失败')
}
}
request.onerror = (event) => {
console.error('打开数据库失败')
}
使用封装库简化 IndexedDB:
// 使用 idb 库(推荐)
import { openDB } from 'idb'
// 简化后的 API,类似 localStorage
const db = await openDB('GameDB', 1, {
upgrade(db) {
db.createObjectStore('tokens', { keyPath: 'id' })
}
})
// 存储
await db.put('tokens', { id: 1, name: 'token', value: '...' })
// 读取
const token = await db.get('tokens', 1)
// 删除
await db.delete('tokens', 1)
4. 性能对比
小数据量 (< 1MB)
localStorage: ⚡⚡⚡⚡⚡ 极快(同步)
IndexedDB: ⚡⚡⚡ 较快(异步开销)
结论:localStorage 更快
大数据量 (> 5MB)
localStorage: ⚡ 很慢(阻塞主线程)
IndexedDB: ⚡⚡⚡⚡⚡ 极快(异步,不阻塞)
结论:IndexedDB 更快
批量操作
localStorage:
├─ 存储 100 个对象: ~500ms
└─ 每次操作都阻塞主线程 ❌
IndexedDB:
├─ 存储 100 个对象: ~50ms (事务批量提交)
└─ 异步执行,不阻塞主线程 ✅
当前项目的影响:
- 批量上传 100 个 Token:
- localStorage: 可能卡顿,影响 UI 响应 ❌
- IndexedDB: 流畅,不影响 UI ✅
5. 查询和索引
localStorage
// ❌ 不支持索引,必须遍历所有数据
const tokens = JSON.parse(localStorage.getItem('gameTokens') || '[]')
// 查找特定服务器的 Token
const serverTokens = tokens.filter(t => t.server === '8551服')
// 遍历所有 Token,O(n) 复杂度
// 按名称排序
const sorted = tokens.sort((a, b) => a.name.localeCompare(b.name))
// 必须先读取全部数据
IndexedDB
// ✅ 支持索引,快速查询
const db = await openDB('GameDB', 1, {
upgrade(db) {
const store = db.createObjectStore('tokens', { keyPath: 'id' })
store.createIndex('server', 'server') // 创建索引
store.createIndex('name', 'name')
}
})
// 使用索引快速查找
const serverTokens = await db.getAllFromIndex('tokens', 'server', '8551服')
// 使用索引,O(log n) 复杂度 ⚡
// 范围查询
const range = IDBKeyRange.bound('8551服', '8560服')
const rangeTokens = await db.getAllFromIndex('tokens', 'server', range)
// 游标遍历(支持分页)
const cursor = await db.transaction('tokens').store.openCursor()
6. 事务支持
localStorage
// ❌ 不支持事务,无法保证原子性
try {
const tokens = JSON.parse(localStorage.getItem('gameTokens') || '[]')
tokens.push(newToken1)
localStorage.setItem('gameTokens', JSON.stringify(tokens))
// 如果这里出错,前面的数据已经保存了,无法回滚 ❌
tokens.push(newToken2)
localStorage.setItem('gameTokens', JSON.stringify(tokens))
} catch (error) {
// 无法回滚,数据可能不一致
}
IndexedDB
// ✅ 支持事务,保证原子性
const transaction = db.transaction(['tokens'], 'readwrite')
const store = transaction.objectStore('tokens')
try {
await store.add(newToken1)
await store.add(newToken2)
// 如果任何操作失败,所有操作都会回滚 ✅
await transaction.complete
} catch (error) {
// 事务自动回滚,数据保持一致
console.error('事务失败,已回滚')
}
🎯 当前项目的使用情况
localStorage 的使用
// src/stores/tokenStore.js
export const useTokenStore = defineStore('token', () => {
const gameTokens = ref([])
// 加载所有 Token
const loadTokens = () => {
const stored = localStorage.getItem('gameTokens')
gameTokens.value = stored ? JSON.parse(stored) : []
}
// 保存 Token
const addToken = (token) => {
gameTokens.value.push(token)
// 每次都保存整个数组
localStorage.setItem('gameTokens', JSON.stringify(gameTokens.value))
}
})
// src/stores/localTokenManager.js
export const useLocalTokenStore = defineStore('localToken', () => {
const gameTokens = ref({})
const addGameToken = (roleId, tokenInfo) => {
gameTokens.value[roleId] = tokenInfo
// 每次都保存整个对象
localStorage.setItem('gameTokens', JSON.stringify(gameTokens.value))
}
})
// src/views/TokenImport.vue
// 保存 bin 文件内容
const storedBinFiles = JSON.parse(localStorage.getItem('storedBinFiles') || '{}')
storedBinFiles[binFileId] = {
id: binFileId,
name: fileName,
content: base64Content, // ⚠️ ArrayBuffer 转 Base64,占用更多空间
...
}
localStorage.setItem('storedBinFiles', JSON.stringify(storedBinFiles))
存在的问题
- 容量限制 - 100+ Token 容易超出 5-10MB 限制 ❌
- 性能问题 - 每次保存都序列化整个数组/对象 ❌
- 类型转换 - ArrayBuffer 必须转 Base64,浪费 33% 空间 ❌
- 无法查询 - 查找特定 Token 需要遍历全部数据 ❌
🚀 迁移到 IndexedDB 的方案
方案设计
// utils/indexedDBManager.js
import { openDB } from 'idb'
class TokenDBManager {
constructor() {
this.dbName = 'XYZWGameDB'
this.version = 1
this.db = null
}
// 初始化数据库
async init() {
this.db = await openDB(this.dbName, this.version, {
upgrade(db) {
// Token 存储
if (!db.objectStoreNames.contains('tokens')) {
const tokenStore = db.createObjectStore('tokens', { keyPath: 'id' })
tokenStore.createIndex('name', 'name')
tokenStore.createIndex('server', 'server')
tokenStore.createIndex('importMethod', 'importMethod')
tokenStore.createIndex('createdAt', 'createdAt')
}
// Bin 文件存储
if (!db.objectStoreNames.contains('binFiles')) {
const binStore = db.createObjectStore('binFiles', { keyPath: 'id' })
binStore.createIndex('roleName', 'roleName')
binStore.createIndex('fileName', 'fileName')
}
// 批量任务进度存储
if (!db.objectStoreNames.contains('taskProgress')) {
const taskStore = db.createObjectStore('taskProgress', { keyPath: 'tokenId' })
taskStore.createIndex('status', 'status')
}
}
})
}
// 保存 Token
async saveToken(token) {
return await this.db.put('tokens', token)
}
// 批量保存 Token
async saveTokens(tokens) {
const tx = this.db.transaction('tokens', 'readwrite')
await Promise.all(tokens.map(token => tx.store.put(token)))
await tx.done
}
// 获取 Token
async getToken(id) {
return await this.db.get('tokens', id)
}
// 获取所有 Token
async getAllTokens() {
return await this.db.getAll('tokens')
}
// 按服务器查询
async getTokensByServer(server) {
return await this.db.getAllFromIndex('tokens', 'server', server)
}
// 删除 Token
async deleteToken(id) {
return await this.db.delete('tokens', id)
}
// 保存 Bin 文件(ArrayBuffer 直接存储,无需转换)
async saveBinFile(binFile) {
return await this.db.put('binFiles', {
id: binFile.id,
name: binFile.name,
roleName: binFile.roleName,
content: binFile.arrayBuffer, // ✅ 直接存储 ArrayBuffer
createdAt: new Date(),
lastUsed: new Date()
})
}
// 获取 Bin 文件
async getBinFile(id) {
const binFile = await this.db.get('binFiles', id)
return binFile ? binFile.content : null // 直接返回 ArrayBuffer
}
// 保存批量任务进度
async saveTaskProgress(tokenId, progress) {
return await this.db.put('taskProgress', {
tokenId,
...progress,
updatedAt: new Date()
})
}
// 获取所有进度
async getAllTaskProgress() {
return await this.db.getAll('taskProgress')
}
// 清理已完成的进度
async cleanupCompletedProgress() {
const tx = this.db.transaction('taskProgress', 'readwrite')
const completed = await tx.store.index('status').getAll('completed')
await Promise.all(completed.map(p => tx.store.delete(p.tokenId)))
await tx.done
}
}
export const tokenDB = new TokenDBManager()
使用示例
// 在应用启动时初始化
import { tokenDB } from '@/utils/indexedDBManager'
// main.js
async function initApp() {
await tokenDB.init()
// ... 其他初始化
}
// TokenImport.vue - 保存 Token
const saveToken = async (tokenData) => {
// ✅ 直接存储,无需 JSON.stringify
await tokenDB.saveToken({
id: tokenData.id,
name: tokenData.name,
token: tokenData.token,
wsUrl: tokenData.wsUrl,
binFileContent: tokenData.arrayBuffer, // ✅ 直接存储 ArrayBuffer
rawData: tokenData.rawToken,
server: tokenData.server,
importMethod: 'bin',
createdAt: new Date(),
lastRefreshed: new Date()
})
}
// 批量保存(使用事务,更快)
const saveTokensBatch = async (tokens) => {
await tokenDB.saveTokens(tokens) // 一次性提交
}
// tokenStore.js - 加载 Token
const loadTokens = async () => {
gameTokens.value = await tokenDB.getAllTokens()
}
// 查询特定服务器的 Token
const loadServerTokens = async (server) => {
const tokens = await tokenDB.getTokensByServer(server)
return tokens
}
// 保存 Bin 文件
const saveBinFile = async (binFile) => {
await tokenDB.saveBinFile({
id: binFile.id,
name: binFile.name,
roleName: binFile.roleName,
arrayBuffer: binFile.arrayBuffer // ✅ 不需要转 Base64
})
}
// 获取 Bin 文件用于重连
const getBinFileForRefresh = async (binFileId) => {
const arrayBuffer = await tokenDB.getBinFile(binFileId)
// 直接使用 ArrayBuffer,无需转换
return arrayBuffer
}
📊 迁移收益分析
存储容量提升
| 项目 | localStorage | IndexedDB | 提升 |
|---|---|---|---|
| 单个 Token | 55KB | 38KB | 31% ↓ |
| 100 个 Token | 5.5MB | 3.8MB | 31% ↓ |
| 最大容量 | 5-10MB | 50MB-无限 | 5-50x ↑ |
| 可存储 Token 数 | ~90-180 | ~1300-无限 | 7-∞x ↑ |
性能提升
| 操作 | localStorage | IndexedDB | 提升 |
|---|---|---|---|
| 批量保存 100 个 Token | ~500ms | ~50ms | 10x ⚡ |
| 查询特定服务器 | ~100ms (遍历) | ~5ms (索引) | 20x ⚡ |
| 页面加载时间 | ~200ms (阻塞) | ~10ms (异步) | 20x ⚡ |
功能增强
| 功能 | localStorage | IndexedDB |
|---|---|---|
| 查询和过滤 | ❌ 需要遍历 | ✅ 索引查询 |
| 分页加载 | ❌ 全量加载 | ✅ 游标分页 |
| 事务保证 | ❌ 无 | ✅ 有 |
| 二进制支持 | ❌ 需转换 | ✅ 直接存储 |
| 大数据量 | ❌ 性能差 | ✅ 性能好 |
🎯 混合存储策略(推荐)
结合两者优势,实现最佳用户体验:
策略设计
// 小数据 → localStorage(快速访问)
// 大数据 → IndexedDB(大容量)
class HybridStorageManager {
constructor() {
this.localStorage = {
// 当前选中的 Token(快速访问)
selectedTokenId: null,
// 用户偏好设置(小数据)
userSettings: {},
// 最近使用的 Token ID 列表(快速显示)
recentTokenIds: []
}
this.indexedDB = {
// 所有 Token 完整数据
tokens: [],
// Bin 文件内容(大数据)
binFiles: [],
// 批量任务进度(可能很多)
taskProgress: []
}
}
// 保存 Token
async saveToken(token) {
// 1. 完整数据存到 IndexedDB
await tokenDB.saveToken(token)
// 2. ID 添加到最近使用列表(localStorage)
const recentIds = JSON.parse(localStorage.getItem('recentTokenIds') || '[]')
recentIds.unshift(token.id)
localStorage.setItem('recentTokenIds', JSON.stringify(recentIds.slice(0, 10)))
}
// 获取最近使用的 Token(首页显示)
async getRecentTokens() {
const recentIds = JSON.parse(localStorage.getItem('recentTokenIds') || '[]')
return await Promise.all(recentIds.map(id => tokenDB.getToken(id)))
}
// 获取所有 Token(按需加载)
async getAllTokens() {
return await tokenDB.getAllTokens()
}
}
使用场景
| 场景 | 存储方式 | 原因 |
|---|---|---|
| 用户设置 | localStorage | 小数据,需要同步访问 |
| 选中的 Token | localStorage | 需要立即获取,频繁访问 |
| 最近使用列表 | localStorage | 小数据,首页显示 |
| 所有 Token | IndexedDB | 大数据,按需加载 |
| Bin 文件内容 | IndexedDB | 大数据,二进制,按需加载 |
| 批量任务进度 | IndexedDB | 可能很多,支持查询 |
⚠️ 注意事项
IndexedDB 的限制
- 异步 API - 需要使用 async/await 或 Promise
- 浏览器兼容性 - IE 10+ 支持,但 API 有差异
- 调试困难 - Chrome DevTools 支持有限
- 学习曲线 - API 比 localStorage 复杂
迁移风险
- 数据迁移 - 需要将现有 localStorage 数据迁移到 IndexedDB
- 代码改动 - 所有存储相关代码需要改为异步
- 向后兼容 - 需要支持旧版本数据格式
建议的迁移步骤
- ✅ 阶段1: 封装 IndexedDB 工具类(完成)
- ✅ 阶段2: 实现数据迁移脚本
- ✅ 阶段3: 逐步迁移各个模块(先 bin 文件,再 Token)
- ✅ 阶段4: 保留 localStorage 作为降级方案
🎉 总结
localStorage 适合
- ✅ 小数据 (< 1MB)
- ✅ 简单数据 (字符串、小对象)
- ✅ 需要同步访问
- ✅ 用户设置、偏好
IndexedDB 适合
- ✅ 大数据 (> 5MB)
- ✅ 复杂数据 (二进制、大对象)
- ✅ 需要查询和索引
- ✅ 批量操作
- ✅ Token 完整数据、Bin 文件
推荐方案
混合存储策略:
- localStorage 存储小量、频繁访问的数据
- IndexedDB 存储大量、完整的数据
- 两者配合,发挥各自优势
📚 参考资源
- MDN - localStorage
- MDN - IndexedDB
- idb 库 - IndexedDB 封装库
- 浏览器存储配额