# 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 ```javascript // ❌ 只能存储字符串 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 ```javascript // ✅ 可以直接存储任意类型 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 ```javascript // ✅ 超简单,同步操作 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 ```javascript // ❌ 较复杂,需要打开数据库、创建事务 // 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**: ```javascript // 使用 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 ```javascript // ❌ 不支持索引,必须遍历所有数据 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 ```javascript // ✅ 支持索引,快速查询 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 ```javascript // ❌ 不支持事务,无法保证原子性 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 ```javascript // ✅ 支持事务,保证原子性 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 的使用 ```javascript // 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 的方案 ### 方案设计 ```javascript // 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() ``` ### 使用示例 ```javascript // 在应用启动时初始化 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 | |-----|-------------|-----------| | **查询和过滤** | ❌ 需要遍历 | ✅ 索引查询 | | **分页加载** | ❌ 全量加载 | ✅ 游标分页 | | **事务保证** | ❌ 无 | ✅ 有 | | **二进制支持** | ❌ 需转换 | ✅ 直接存储 | | **大数据量** | ❌ 性能差 | ✅ 性能好 | --- ## 🎯 混合存储策略(推荐) 结合两者优势,实现最佳用户体验: ### 策略设计 ```javascript // 小数据 → 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 存储大量、完整的数据 - 两者配合,发挥各自优势 --- ## 📚 参考资源 - [MDN - localStorage](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/localStorage) - [MDN - IndexedDB](https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API) - [idb 库](https://github.com/jakearchibald/idb) - IndexedDB 封装库 - [浏览器存储配额](https://web.dev/articles/storage-for-the-web)