1. JavaScript IndexedDB 数据库入门实战
IndexedDB 是浏览器内置的非关系型数据库系统,它允许我们在客户端存储大量结构化数据。与 localStorage 相比,IndexedDB 提供了更强大的查询能力、事务支持和更大的存储空间(通常为浏览器可用磁盘空间的50%)。下面我将通过一个完整的联系人管理示例,带你掌握 IndexedDB 的核心用法。
1.1 初始化数据库连接
首先我们需要打开数据库连接。IndexedDB 采用异步API设计,所有操作都基于事件驱动:
javascript复制const dbName = 'ContactsDB';
const dbVersion = 1; // 版本号用于触发升级逻辑
const request = indexedDB.open(dbName, dbVersion);
request.onerror = (event) => {
console.error('数据库打开失败:', event.target.error);
};
request.onsuccess = (event) => {
const db = event.target.result;
console.log('数据库连接成功');
// 这里可以执行查询等操作
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 首次创建或版本升级时会进入此回调
if (!db.objectStoreNames.contains('contacts')) {
const store = db.createObjectStore('contacts', {
keyPath: 'id',
autoIncrement: true
});
// 创建索引以便按姓名搜索
store.createIndex('name_idx', 'name', { unique: false });
}
};
关键点说明:版本号变更会触发onupgradeneeded事件,这是修改数据库结构的唯一入口。在生产环境中,应该实现渐进式升级策略处理不同客户端版本。
1.2 数据操作基础方法
1.2.1 添加联系人
javascript复制function addContact(db, contact) {
return new Promise((resolve, reject) => {
const tx = db.transaction('contacts', 'readwrite');
const store = tx.objectStore('contacts');
const request = store.add(contact);
request.onsuccess = () => resolve(request.result);
request.onerror = (event) => reject(event.target.error);
});
}
// 使用示例
const newContact = {
name: '张三',
phone: '13800138000',
email: 'zhangsan@example.com'
};
addContact(db, newContact)
.then(id => console.log('添加成功,ID:', id))
.catch(err => console.error('添加失败:', err));
1.2.2 查询联系人
javascript复制function getContact(db, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction('contacts', 'readonly');
const store = tx.objectStore('contacts');
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = (event) => reject(event.target.error);
});
}
// 按姓名模糊查询
function searchContacts(db, name) {
return new Promise((resolve, reject) => {
const tx = db.transaction('contacts', 'readonly');
const store = tx.objectStore('contacts');
const index = store.index('name_idx');
const request = index.openCursor();
const results = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
if (cursor.value.name.includes(name)) {
results.push(cursor.value);
}
cursor.continue();
} else {
resolve(results);
}
};
request.onerror = (event) => reject(event.target.error);
});
}
1.3 事务与错误处理
IndexedDB 的事务模型是其核心特性之一。每个操作都必须在事务中执行,事务有三种模式:
- readonly:只读,可以并发执行
- readwrite:读写,会锁定对象存储
- versionchange:数据库结构变更
javascript复制// 批量操作示例
function batchUpdateContacts(db, updates) {
return new Promise((resolve, reject) => {
const tx = db.transaction('contacts', 'readwrite');
const store = tx.objectStore('contacts');
let completed = 0;
const total = updates.length;
updates.forEach(update => {
const request = store.put(update);
request.onsuccess = () => {
if (++completed === total) {
resolve();
}
};
request.onerror = (event) => {
reject(event.target.error);
};
});
// 事务自动提交,无需手动调用
});
}
// 错误处理最佳实践
tx.onerror = (event) => {
// 事务中任何一个操作失败都会触发
console.error('事务失败:', event.target.error);
// 可以检查错误类型做不同处理
if (event.target.error.name === 'QuotaExceededError') {
alert('存储空间不足,请清理后重试');
}
};
1.4 性能优化技巧
- 批量操作:将多个操作放在单个事务中执行
- 游标分页:处理大型数据集时使用游标分批获取
- 索引策略:为常用查询字段创建合适索引
- 连接池:复用数据库连接避免重复打开
javascript复制// 分页查询实现
function getContactsByPage(db, page, pageSize) {
return new Promise((resolve, reject) => {
const tx = db.transaction('contacts', 'readonly');
const store = tx.objectStore('contacts');
const request = store.openCursor();
const results = [];
let advanced = false;
let counter = 0;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
resolve(results);
return;
}
if (!advanced && page > 1) {
cursor.advance((page - 1) * pageSize);
advanced = true;
return;
}
if (counter++ < pageSize) {
results.push(cursor.value);
cursor.continue();
} else {
resolve(results);
}
};
request.onerror = (event) => reject(event.target.error);
});
}
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高级特性与实战技巧
2.1 复合索引与复杂查询
对于需要多条件查询的场景,可以创建复合索引:
javascript复制// 在onupgradeneeded中添加
store.createIndex('name_phone_idx', ['name', 'phone'], { unique: false });
// 复合查询
function searchByNameAndPhone(db, name, phone) {
return new Promise((resolve, reject) => {
const tx = db.transaction('contacts', 'readonly');
const store = tx.objectStore('contacts');
const index = store.index('name_phone_idx');
// 精确匹配
const request = index.get([name, phone]);
request.onsuccess = () => resolve(request.result);
request.onerror = (event) => reject(event.target.error);
});
}
2.2 数据迁移策略
当数据库结构需要变更时,需要在onupgradeneeded中处理:
javascript复制request.onupgradeneeded = (event) => {
const db = event.target.result;
const oldVersion = event.oldVersion;
if (oldVersion < 1) {
// 初始版本创建
const store = db.createObjectStore('contacts', {
keyPath: 'id',
autoIncrement: true
});
store.createIndex('name_idx', 'name', { unique: false });
}
if (oldVersion < 2) {
// 版本2升级:添加email字段和索引
const tx = event.target.transaction;
const store = tx.objectStore('contacts');
store.createIndex('email_idx', 'email', { unique: true });
}
if (oldVersion < 3) {
// 版本3升级:添加创建时间字段
const tx = event.target.transaction;
const store = tx.objectStore('contacts');
store.getAll().onsuccess = (e) => {
const contacts = e.target.result;
contacts.forEach(contact => {
contact.createdAt = new Date().toISOString();
store.put(contact);
});
};
}
};
2.3 与Web Worker集成
将耗时的数据库操作放到Web Worker中执行:
javascript复制// worker.js
self.onmessage = function(e) {
const { type, payload } = e.data;
if (type === 'query') {
const request = indexedDB.open('ContactsDB');
request.onsuccess = (event) => {
const db = event.target.result;
const tx = db.transaction('contacts', 'readonly');
const store = tx.objectStore('contacts');
const query = store.getAll();
query.onsuccess = (e) => {
self.postMessage({ type: 'result', data: e.target.result });
};
};
}
};
// 主线程
const worker = new Worker('worker.js');
worker.postMessage({ type: 'query' });
worker.onmessage = (e) => {
if (e.data.type === 'result') {
console.log('收到worker数据:', e.data.data);
}
};
3. 常见问题与解决方案
3.1 存储空间限制问题
不同浏览器的IndexedDB存储限制不同,通常在50MB到80%磁盘空间之间。处理配额超限的方案:
javascript复制function checkQuota(db) {
return new Promise((resolve) => {
const testData = new ArrayBuffer(1024 * 1024); // 1MB测试数据
const tx = db.transaction('contacts', 'readwrite');
const store = tx.objectStore('contacts');
const testItem = { id: 'quota_test', data: testData };
const request = store.add(testItem);
request.onsuccess = () => {
store.delete('quota_test');
resolve(true);
};
request.onerror = (event) => {
if (event.target.error.name === 'QuotaExceededError') {
resolve(false);
}
};
});
}
3.2 浏览器兼容性处理
虽然现代浏览器都支持IndexedDB,但实现细节有差异。推荐使用以下兼容方案:
javascript复制function getIDB() {
if (!window.indexedDB) {
console.warn('当前浏览器不支持IndexedDB');
return null;
}
// 处理不同浏览器的前缀问题
const vendors = ['', 'moz', 'webkit', 'ms'];
for (const vendor of vendors) {
try {
const idb = window[`${vendor}IndexedDB`] || window.indexedDB;
if (idb) return idb;
} catch (e) {
console.warn(`${vendor}IndexedDB测试失败`, e);
}
}
return null;
}
// 使用兼容版本打开数据库
const idb = getIDB();
if (idb) {
const request = idb.open('ContactsDB', 1);
// ...其他操作
}
3.3 数据同步策略
实现客户端与服务端数据同步的常见模式:
javascript复制async function syncWithServer(db, apiClient) {
// 1. 获取本地修改记录
const localChanges = await getUnsyncedChanges(db);
// 2. 推送更改到服务器
try {
const serverResponse = await apiClient.sync(localChanges);
// 3. 应用服务器返回的更新
await applyServerUpdates(db, serverResponse.updates);
// 4. 标记已同步记录
await markAsSynced(db, localChanges);
return { success: true };
} catch (error) {
console.error('同步失败:', error);
return { success: false, error };
}
}
// 获取未同步记录
function getUnsyncedChanges(db) {
return new Promise((resolve) => {
const tx = db.transaction('contacts', 'readonly');
const store = tx.objectStore('contacts');
const index = store.index('sync_status_idx');
const request = index.getAll('unsynced');
request.onsuccess = (e) => resolve(e.target.result || []);
});
}
4. 实战案例:联系人管理系统
下面我们整合前面介绍的技术,实现一个完整的联系人CRUD示例:
4.1 完整HTML结构
html复制<!DOCTYPE html>
<html>
<head>
<title>IndexedDB联系人管理</title>
<style>
.contact-form { margin: 20px; padding: 15px; border: 1px solid #ddd; }
.contact-list { margin-top: 20px; }
.contact-card {
padding: 10px;
margin: 5px 0;
border: 1px solid #eee;
display: flex;
justify-content: space-between;
}
</style>
</head>
<body>
<div class="contact-form">
<h2>添加联系人</h2>
<form id="contactForm">
<input type="text" name="name" placeholder="姓名" required>
<input type="tel" name="phone" placeholder="电话">
<input type="email" name="email" placeholder="邮箱">
<button type="submit">保存</button>
</form>
</div>
<div class="search-box">
<input type="text" id="searchInput" placeholder="搜索联系人...">
<button id="searchBtn">搜索</button>
</div>
<div class="contact-list" id="contactList">
<!-- 联系人列表将在这里动态生成 -->
</div>
<script src="app.js"></script>
</body>
</html>
4.2 JavaScript 主逻辑
javascript复制// app.js
class ContactManager {
constructor() {
this.db = null;
this.initDB().then(() => this.renderContacts());
// 绑定事件
document.getElementById('contactForm').addEventListener('submit', this.handleSubmit.bind(this));
document.getElementById('searchBtn').addEventListener('click', this.handleSearch.bind(this));
}
async initDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('ContactManagerDB', 3);
request.onerror = (event) => {
console.error('数据库初始化失败:', event.target.error);
reject(event.target.error);
};
request.onsuccess = (event) => {
this.db = event.target.result;
console.log('数据库连接成功');
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('contacts')) {
const store = db.createObjectStore('contacts', {
keyPath: 'id',
autoIncrement: true
});
store.createIndex('name_idx', 'name', { unique: false });
store.createIndex('phone_idx', 'phone', { unique: false });
store.createIndex('email_idx', 'email', { unique: true });
}
};
});
}
async addContact(contact) {
const tx = this.db.transaction('contacts', 'readwrite');
const store = tx.objectStore('contacts');
return new Promise((resolve, reject) => {
const request = store.add(contact);
request.onsuccess = () => resolve(request.result);
request.onerror = (event) => reject(event.target.error);
});
}
async getAllContacts() {
const tx = this.db.transaction('contacts', 'readonly');
const store = tx.objectStore('contacts');
return new Promise((resolve) => {
const request = store.getAll();
request.onsuccess = (event) => {
resolve(event.target.result || []);
};
});
}
async searchContacts(keyword) {
const tx = this.db.transaction('contacts', 'readonly');
const store = tx.objectStore('contacts');
return new Promise((resolve) => {
const request = store.openCursor();
const results = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const contact = cursor.value;
if (contact.name.includes(keyword) ||
contact.phone.includes(keyword) ||
contact.email.includes(keyword)) {
results.push(contact);
}
cursor.continue();
} else {
resolve(results);
}
};
});
}
async deleteContact(id) {
const tx = this.db.transaction('contacts', 'readwrite');
const store = tx.objectStore('contacts');
return new Promise((resolve, reject) => {
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = (event) => reject(event.target.error);
});
}
async renderContacts(contacts) {
if (!contacts) {
contacts = await this.getAllContacts();
}
const listEl = document.getElementById('contactList');
listEl.innerHTML = '';
if (contacts.length === 0) {
listEl.innerHTML = '<p>暂无联系人</p>';
return;
}
contacts.forEach(contact => {
const card = document.createElement('div');
card.className = 'contact-card';
card.innerHTML = `
<div>
<h3>${contact.name}</h3>
<p>电话: ${contact.phone || '未填写'}</p>
<p>邮箱: ${contact.email || '未填写'}</p>
</div>
<button data-id="${contact.id}" class="delete-btn">删除</button>
`;
listEl.appendChild(card);
});
// 绑定删除按钮事件
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
const id = Number(e.target.getAttribute('data-id'));
await this.deleteContact(id);
this.renderContacts();
});
});
}
handleSubmit(event) {
event.preventDefault();
const form = event.target;
const contact = {
name: form.name.value,
phone: form.phone.value,
email: form.email.value
};
this.addContact(contact)
.then(() => {
form.reset();
this.renderContacts();
})
.catch(err => {
console.error('添加联系人失败:', err);
alert('添加失败,请查看控制台');
});
}
handleSearch() {
const keyword = document.getElementById('searchInput').value.trim();
if (keyword) {
this.searchContacts(keyword)
.then(results => this.renderContacts(results));
} else {
this.renderContacts();
}
}
}
// 初始化应用
document.addEventListener('DOMContentLoaded', () => {
new ContactManager();
});
4.3 性能优化实践
- 批量操作:当需要导入大量联系人时,使用批量插入
javascript复制async function importContacts(contacts) {
const tx = this.db.transaction('contacts', 'readwrite');
const store = tx.objectStore('contacts');
return new Promise((resolve, reject) => {
let completed = 0;
const total = contacts.length;
contacts.forEach(contact => {
const request = store.add(contact);
request.onsuccess = () => {
if (++completed === total) {
resolve();
}
};
request.onerror = (event) => {
// 部分失败不影响整体导入
if (++completed === total) {
resolve();
}
};
});
});
}
- 数据分页加载:对于大型联系人列表实现懒加载
javascript复制async function getContactsPaginated(page = 1, pageSize = 20) {
const tx = this.db.transaction('contacts', 'readonly');
const store = tx.objectStore('contacts');
return new Promise((resolve) => {
const request = store.openCursor();
const results = [];
let counter = 0;
let skipped = false;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
resolve(results);
return;
}
if (!skipped && page > 1) {
cursor.advance((page - 1) * pageSize);
skipped = true;
return;
}
if (counter++ < pageSize) {
results.push(cursor.value);
cursor.continue();
} else {
resolve(results);
}
};
});
}
- 使用Web Worker处理复杂查询:将耗时的搜索操作放到后台线程
javascript复制// worker.js
self.onmessage = function(e) {
const { type, payload } = e.data;
if (type === 'search') {
const request = indexedDB.open('ContactManagerDB');
request.onsuccess = (event) => {
const db = event.target.result;
const tx = db.transaction('contacts', 'readonly');
const store = tx.objectStore('contacts');
const keyword = payload.keyword.toLowerCase();
const request = store.openCursor();
const results = [];
request.onsuccess = (e) => {
const cursor = e.target.result;
if (cursor) {
const contact = cursor.value;
if (contact.name.toLowerCase().includes(keyword) ||
(contact.phone && contact.phone.includes(keyword)) ||
(contact.email && contact.email.toLowerCase().includes(keyword))) {
results.push(contact);
}
cursor.continue();
} else {
self.postMessage({ type: 'search_results', results });
}
};
};
}
};
// 在主线程中使用
class ContactManager {
constructor() {
this.searchWorker = new Worker('worker.js');
this.searchWorker.onmessage = (e) => {
if (e.data.type === 'search_results') {
this.renderContacts(e.data.results);
}
};
}
handleSearch() {
const keyword = document.getElementById('searchInput').value.trim();
if (keyword) {
this.searchWorker.postMessage({
type: 'search',
payload: { keyword }
});
} else {
this.renderContacts();
}
}
}
5. 调试与问题排查
5.1 开发者工具使用技巧
现代浏览器开发者工具提供了完善的IndexedDB调试支持:
-
Chrome DevTools:
- Application → IndexedDB 查看所有数据库和对象存储
- 可以直接编辑、删除数据
- 查看数据库结构和索引
-
Firefox Developer Tools:
- Storage → IndexedDB 管理数据库
- 可以导出/导入数据
-
调试技巧:
javascript复制// 在控制台快速访问数据库 const req = indexedDB.open('ContactManagerDB'); req.onsuccess = (e) => { window.tempDB = e.target.result; // 现在可以通过tempDB直接操作 };
5.2 常见错误处理
-
版本冲突错误:
javascript复制request.onblocked = (event) => { // 当有旧版本连接未关闭时触发 console.warn('数据库被旧版本连接阻塞,请关闭其他页面'); }; -
事务失效问题:
javascript复制// 错误示例:事务外使用异步操作 function badExample(db) { const tx = db.transaction('contacts', 'readwrite'); const store = tx.objectStore('contacts'); setTimeout(() => { // 此时事务已自动关闭 store.add({name: '错误示例'}); // 会抛出事务失效错误 }, 1000); } // 正确做法:所有操作在事务生命周期内完成 function goodExample(db) { return new Promise((resolve) => { const tx = db.transaction('contacts', 'readwrite'); const store = tx.objectStore('contacts'); store.add({name: '正确示例'}).onsuccess = resolve; }); } -
游标性能优化:
javascript复制// 低效做法:一次性获取全部数据 store.getAll().onsuccess = (e) => { const allData = e.target.result; // 大数据集会占用大量内存 }; // 高效做法:使用游标分批处理 function processLargeData(store, processItem) { return new Promise((resolve) => { const request = store.openCursor(); let processed = 0; request.onsuccess = (e) => { const cursor = e.target.result; if (cursor) { processItem(cursor.value); processed++; if (processed % 100 === 0) { // 每处理100条休息一下避免阻塞UI setTimeout(() => cursor.continue(), 0); } else { cursor.continue(); } } else { resolve(processed); } }; }); }
5.3 数据备份与恢复
实现数据导出导入功能:
javascript复制class ContactManager {
// ...其他代码
async exportData() {
const contacts = await this.getAllContacts();
const blob = new Blob([JSON.stringify(contacts)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `contacts_backup_${new Date().toISOString()}.json`;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 100);
}
async importData(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async (e) => {
try {
const contacts = JSON.parse(e.target.result);
await this.importContacts(contacts);
resolve();
} catch (err) {
reject(err);
}
};
reader.onerror = (e) => reject(reader.error);
reader.readAsText(file);
});
}
}
