Files
xyzw_web_helper/MD说明文件夹/IndexedDB与localStorage对比分析.md
2025-10-17 20:56:50 +08:00

19 KiB
Raw Blame History

IndexedDB 与 localStorage 对比分析

📋 概述

本文档详细对比 IndexedDBlocalStorage 两种浏览器存储方案,并分析在 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服')
// 遍历所有 TokenO(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))

存在的问题

  1. 容量限制 - 100+ Token 容易超出 5-10MB 限制
  2. 性能问题 - 每次保存都序列化整个数组/对象
  3. 类型转换 - ArrayBuffer 必须转 Base64浪费 33% 空间
  4. 无法查询 - 查找特定 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 的限制

  1. 异步 API - 需要使用 async/await 或 Promise
  2. 浏览器兼容性 - IE 10+ 支持,但 API 有差异
  3. 调试困难 - Chrome DevTools 支持有限
  4. 学习曲线 - API 比 localStorage 复杂

迁移风险

  1. 数据迁移 - 需要将现有 localStorage 数据迁移到 IndexedDB
  2. 代码改动 - 所有存储相关代码需要改为异步
  3. 向后兼容 - 需要支持旧版本数据格式

建议的迁移步骤

  1. 阶段1: 封装 IndexedDB 工具类(完成)
  2. 阶段2: 实现数据迁移脚本
  3. 阶段3: 逐步迁移各个模块(先 bin 文件,再 Token
  4. 阶段4: 保留 localStorage 作为降级方案

🎉 总结

localStorage 适合

  • 小数据 (< 1MB)
  • 简单数据 (字符串、小对象)
  • 需要同步访问
  • 用户设置、偏好

IndexedDB 适合

  • 大数据 (> 5MB)
  • 复杂数据 (二进制、大对象)
  • 需要查询和索引
  • 批量操作
  • Token 完整数据、Bin 文件

推荐方案

混合存储策略

  • localStorage 存储小量、频繁访问的数据
  • IndexedDB 存储大量、完整的数据
  • 两者配合,发挥各自优势

📚 参考资源