使用 IndexedDB 保存和加载游戏状态
IndexedDB 是在浏览器中持久化游戏数据的最佳选择。它可以处理大量数据、支持离线使用,而且不会阻塞主线程。
1) 为什么选择 IndexedDB 而不是 localStorage?
| 功能 | localStorage | IndexedDB |
|---|---|---|
| 存储上限 | 约 5-10 MB | 50+ MB(通常可达 GB 级) |
| 数据类型 | 仅字符串 | 对象、Blob、数组 |
| 异步 | 否(会阻塞) | 是 |
| 索引查询 | 否 | 是 |
对于游戏,IndexedDB 几乎总是正确的选择。
2) 打开数据库
js
function openGameDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('MyGame', 1)
request.onerror = () => reject(request.error)
request.onsuccess = () => resolve(request.result)
request.onupgradeneeded = (event) => {
const db = event.target.result
// 创建存储区
if (!db.objectStoreNames.contains('saves')) {
db.createObjectStore('saves', { keyPath: 'slot' })
}
if (!db.objectStoreNames.contains('settings')) {
db.createObjectStore('settings', { keyPath: 'key' })
}
}
})
}3) 保存游戏状态
js
async function saveGame(slot, gameState) {
const db = await openGameDB()
return new Promise((resolve, reject) => {
const tx = db.transaction('saves', 'readwrite')
const store = tx.objectStore('saves')
const saveData = {
slot,
state: gameState,
timestamp: Date.now(),
}
const request = store.put(saveData)
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
}4) 加载游戏状态
js
async function loadGame(slot) {
const db = await openGameDB()
return new Promise((resolve, reject) => {
const tx = db.transaction('saves', 'readonly')
const store = tx.objectStore('saves')
const request = store.get(slot)
request.onsuccess = () => resolve(request.result?.state || null)
request.onerror = () => reject(request.error)
})
}5) 列出所有存档
js
async function listSaves() {
const db = await openGameDB()
return new Promise((resolve, reject) => {
const tx = db.transaction('saves', 'readonly')
const store = tx.objectStore('saves')
const request = store.getAll()
request.onsuccess = () => resolve(request.result)
request.onerror = () => reject(request.error)
})
}6) 删除存档
js
async function deleteSave(slot) {
const db = await openGameDB()
return new Promise((resolve, reject) => {
const tx = db.transaction('saves', 'readwrite')
const store = tx.objectStore('saves')
const request = store.delete(slot)
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
}7) 完整的 GameStorage 类
js
class GameStorage {
constructor(dbName = 'GameData', version = 1) {
this.dbName = dbName
this.version = version
this.db = null
}
async init() {
this.db = await this.openDB()
}
openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version)
request.onerror = () => reject(request.error)
request.onsuccess = () => resolve(request.result)
request.onupgradeneeded = (e) => {
const db = e.target.result
if (!db.objectStoreNames.contains('saves')) {
db.createObjectStore('saves', { keyPath: 'slot' })
}
if (!db.objectStoreNames.contains('settings')) {
db.createObjectStore('settings', { keyPath: 'key' })
}
if (!db.objectStoreNames.contains('assets')) {
db.createObjectStore('assets', { keyPath: 'url' })
}
}
})
}
async save(slot, data) {
const tx = this.db.transaction('saves', 'readwrite')
tx.objectStore('saves').put({ slot, data, timestamp: Date.now() })
return tx.complete
}
async load(slot) {
const tx = this.db.transaction('saves', 'readonly')
const result = await this.promisify(tx.objectStore('saves').get(slot))
return result?.data || null
}
async setSetting(key, value) {
const tx = this.db.transaction('settings', 'readwrite')
tx.objectStore('settings').put({ key, value })
}
async getSetting(key, defaultValue = null) {
const tx = this.db.transaction('settings', 'readonly')
const result = await this.promisify(tx.objectStore('settings').get(key))
return result?.value ?? defaultValue
}
promisify(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result)
request.onerror = () => reject(request.error)
})
}
}8) 存储二进制数据(纹理、音频)
IndexedDB 可以处理 Blob 和 ArrayBuffer:
js
async function cacheAsset(url, blob) {
const tx = db.transaction('assets', 'readwrite')
tx.objectStore('assets').put({ url, blob, cached: Date.now() })
}
async function getCachedAsset(url) {
const tx = db.transaction('assets', 'readonly')
const result = await promisify(tx.objectStore('assets').get(url))
return result?.blob || null
}9) 自动保存模式
js
class AutoSave {
constructor(storage, interval = 60000) {
this.storage = storage
this.interval = interval
this.timer = null
this.dirty = false
}
markDirty() {
this.dirty = true
}
start(getState) {
this.timer = setInterval(async () => {
if (this.dirty) {
await this.storage.save('autosave', getState())
this.dirty = false
console.log('已自动保存')
}
}, this.interval)
}
stop() {
clearInterval(this.timer)
}
}10) 错误处理与回退方案
js
async function safeLoad(slot, defaultState) {
try {
const saved = await loadGame(slot)
if (saved) {
// 根据需要验证或迁移旧存档
return migrateSave(saved)
}
} catch (err) {
console.warn('加载存档失败:', err)
}
return defaultState
}
function migrateSave(save) {
// 处理旧存档格式
if (!save.version) {
save.version = 1
save.settings = save.settings || {}
}
return save
}11) 防止存档被清理
IndexedDB 中的数据并不保证会永久保留。默认情况下,来源使用“尽力而为”存储;当磁盘空间不足时,浏览器可能会清理这些数据。在 Safari/WebKit 中,如果用户有一段时间未与你的网站互动,数据也可能被清理。对于游戏存档来说,这恰恰是你最不希望丢失的数据。
请求持久化存储,使浏览器不会在缺少用户明确操作的情况下清除你的数据:
js
async function makeStoragePersistent() {
if (navigator.storage && navigator.storage.persist) {
const persisted = await navigator.storage.persist()
console.log(persisted ? '存档已受到保护,不会被自动清理' : '存储空间紧张时,存档可能会被清理')
return persisted
}
return false
}浏览器会根据互动情况来决定是否批准请求,例如用户与你的网站互动频率,以及网站是否已安装为 PWA。因此,不要假定请求总会成功。在写入大型存档或缓存资源之前,你还可以检查可用空间:
js
async function checkStorage() {
if (navigator.storage && navigator.storage.estimate) {
const { usage, quota } = await navigator.storage.estimate()
console.log(`已使用 ${usage} 字节,总配额为 ${quota} 字节`)
}
}这两个 API 都需要安全上下文(HTTPS 或 localhost)。
相关内容
- 为离线游戏构建 PWA
- 使用 Service Worker 缓存游戏
- 发布可快速加载的网页游戏
- 流式加载资源 — 使用 IndexedDB 作为资源缓存
- 网页游戏数据分析 — 跟踪保存和加载模式,以了解玩家行为
外部资源
- MDN:IndexedDB API — 完整的 API 参考
- MDN:使用 IndexedDB — 分步指南
- idb 库 — 一个基于 Promise 的轻量级 IndexedDB 封装库