IndexedDB 入门
IndexedDB 是浏览器提供的大型客户端数据库系统,支持存储结构化数据、文件和二进制对象。与 localStorage 相比,IndexedDB 支持索引查询、事务操作和游标遍历,存储容量几乎没有上限(通常可达数 GB),适合需要存储大量数据或进行复杂查询的 Web 应用。
前置知识
阅读本节前,建议先了解:Storage API 详解
基础概念
IndexedDB 核心概念
| 概念 | 说明 | 类比 |
|---|---|---|
| 数据库(Database) | 数据存储的最高层级 | 数据库 |
| 对象仓库(Object Store) | 类似"表",存储结构化数据 | 表 |
| 索引(Index) | 加速数据查询 | 索引 |
| 事务(Transaction) | 保证数据操作原子性 | 事务 |
| 游标(Cursor) | 遍历查询结果集 | 游标 |
| 请求(Request) | 所有异步操作返回 IDBRequest | Promise |
IndexedDB vs localStorage
| 特性 | IndexedDB | localStorage |
|---|---|---|
| 存储容量 | 数百 MB 到数 GB | 约 5MB |
| 数据类型 | 对象、文件、Blob 等 | 仅字符串 |
| 查询能力 | 索引查询、游标遍历 | 只能按键名 |
| 事务支持 | 支持 | 不支持 |
| 异步操作 | 是(非阻塞) | 否(同步阻塞) |
| 适用场景 | 大量数据、复杂查询 | 简单键值存储 |
语法与 API
打开数据库
javascript
// 打开或创建数据库
const request = indexedDB.open('myDatabase', 1);
// 数据库版本号变化时触发(创建或升级)
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建对象仓库(类似创建表)
if (!db.objectStoreNames.contains('users')) {
const store = db.createObjectStore('users', {
keyPath: 'id', // 主键
autoIncrement: true // 自动递增
});
// 创建索引
store.createIndex('name', 'name', { unique: false });
store.createIndex('email', 'email', { unique: true });
store.createIndex('age', 'age', { unique: false });
}
// 创建第二个对象仓库
if (!db.objectStoreNames.contains('products')) {
const store = db.createObjectStore('products', {
keyPath: 'id',
autoIncrement: true
});
store.createIndex('name', 'name', { unique: false });
store.createIndex('price', 'price', { unique: false });
store.createIndex('category', 'category', { unique: false });
}
};
// 成功打开
request.onsuccess = (event) => {
const db = event.target.result;
console.log('数据库打开成功:', db.name, '版本:', db.version);
};
// 打开失败
request.onerror = (event) => {
console.error('数据库打开失败:', event.target.error);
};增删改查操作
javascript
// 获取数据库引用后执行操作
const db = null; // 假设已通过 open 获取
// ========== 新增数据(Create)==========
function addUser(user) {
const transaction = db.transaction('users', 'readwrite');
const store = transaction.objectStore('users');
const request = store.add(user);
request.onsuccess = () => {
console.log('用户添加成功, ID:', request.result);
};
request.onerror = (event) => {
console.error('添加失败:', event.target.error);
};
}
// ========== 读取数据(Read)==========
function getUser(id) {
const transaction = db.transaction('users', 'readonly');
const store = transaction.objectStore('users');
const request = store.get(id);
request.onsuccess = () => {
console.log('查询结果:', request.result);
};
}
// ========== 更新数据(Update)==========
function updateUser(user) {
const transaction = db.transaction('users', 'readwrite');
const store = transaction.objectStore('users');
const request = store.put(user); // put 可以新增或更新
request.onsuccess = () => {
console.log('用户更新成功');
};
}
// ========== 删除数据(Delete)==========
function deleteUser(id) {
const transaction = db.transaction('users', 'readwrite');
const store = transaction.objectStore('users');
const request = store.delete(id);
request.onsuccess = () => {
console.log('用户删除成功');
};
}
// ========== 清空数据 ==========
function clearUsers() {
const transaction = db.transaction('users', 'readwrite');
const store = transaction.objectStore('users');
const request = store.clear();
request.onsuccess = () => {
console.log('用户数据已清空');
};
}详细说明
事务模式
IndexedDB 的事务有三种模式:
| 模式 | 说明 | 操作 |
|---|---|---|
readonly | 只读(默认) | 读取数据 |
readwrite | 读写 | 增删改 |
versionchange | 版本变更 | 创建/删除对象仓库和索引 |
javascript
// 只读事务
const tx1 = db.transaction('users', 'readonly');
// 读写事务
const tx2 = db.transaction('users', 'readwrite');
// 跨多个对象仓库的事务
const tx3 = db.transaction(['users', 'products'], 'readwrite');
// 监听事务完成
tx3.oncomplete = () => {
console.log('事务完成');
};
tx3.onerror = (event) => {
console.error('事务失败:', event.target.error);
};
// 事务回滚
tx3.abort();使用索引查询
javascript
// 通过索引查找
function getUserByName(name) {
const transaction = db.transaction('users', 'readonly');
const store = transaction.objectStore('users');
const index = store.index('name');
const request = index.get(name);
request.onsuccess = () => {
console.log('找到用户:', request.result);
};
}
// 通过索引范围查询
function getUsersByAgeRange(min, max) {
const transaction = db.transaction('users', 'readonly');
const store = transaction.objectStore('users');
const index = store.index('age');
// IDBKeyRange 范围查询
const range = IDBKeyRange.bound(min, max);
const request = index.openCursor(range);
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
console.log(`年龄 ${cursor.key}:`, cursor.value);
cursor.continue();
}
};
}
// IDBKeyRange 方法
IDBKeyRange.only(25); // 等于 25
IDBKeyRange.lowerBound(18); // 大于等于 18
IDBKeyRange.upperBound(60); // 小于等于 60
IDBKeyRange.bound(18, 60); // 18 到 60
IDBKeyRange.bound(18, 60, true); // 18 到 60(不含边界)使用游标遍历
javascript
// 遍历所有数据
function getAllUsers() {
const transaction = db.transaction('users', 'readonly');
const store = transaction.objectStore('users');
const request = store.openCursor();
const users = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
users.push(cursor.value);
cursor.continue(); // 继续下一条
} else {
console.log('所有用户:', users);
}
};
}
// 游标高级用法
function getUsersFiltered() {
const transaction = db.transaction('users', 'readonly');
const store = transaction.objectStore('users');
const index = store.index('age');
const range = IDBKeyRange.lowerBound(20);
const request = index.openCursor(range, 'prev'); // prev: 降序
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
console.log(cursor.key, cursor.value);
// cursor.advance(5); // 跳过 5 条
// cursor.delete(); // 删除当前记录
// cursor.update(newValue); // 更新当前记录
cursor.continue();
}
};
}Promise 封装
javascript
// 将 IndexedDB 的回调风格封装为 Promise
class IDBHelper {
constructor(dbName, version) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
// 打开数据库
open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onupgradeneeded = (event) => {
const db = event.target.result;
this._onUpgrade(db);
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve(this.db);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}
// 子类覆盖此方法定义结构
_onUpgrade(db) {}
// 通用事务操作
_transaction(storeName, mode, callback) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(storeName, mode);
const store = transaction.objectStore(storeName);
const result = callback(store);
if (result && result.onsuccess !== undefined) {
result.onsuccess = () => resolve(result.result);
result.onerror = () => reject(result.error);
} else {
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
}
});
}
// 添加
add(storeName, data) {
return this._transaction(storeName, 'readwrite', store => store.add(data));
}
// 获取
get(storeName, key) {
return this._transaction(storeName, 'readonly', store => store.get(key));
}
// 获取所有
getAll(storeName) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 更新
put(storeName, data) {
return this._transaction(storeName, 'readwrite', store => store.put(data));
}
// 删除
delete(storeName, key) {
return this._transaction(storeName, 'readwrite', store => store.delete(key));
}
}
// 使用示例
const db = new IDBHelper('myApp', 1);
// 自定义数据库结构
db._onUpgrade = (database) => {
if (!database.objectStoreNames.contains('notes')) {
const store = database.createObjectStore('notes', { keyPath: 'id' });
store.createIndex('title', 'title', { unique: false });
store.createIndex('updatedAt', 'updatedAt', { unique: false });
}
};
// 初始化
db.open().then(() => {
console.log('数据库就绪');
// 添加数据
return db.add('notes', {
id: 1,
title: '学习笔记',
content: 'IndexedDB 学习内容',
updatedAt: Date.now()
});
}).then(() => {
// 获取所有数据
return db.getAll('notes');
}).then(notes => {
console.log('笔记列表:', notes);
});实战示例
完整的 IndexedDB 笔记应用
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>IndexedDB 笔记应用</title>
<style>
body { font-family: -apple-system, sans-serif; padding: 20px; background: #f5f7fa; }
.app { max-width: 700px; margin: 0 auto; }
h1 { text-align: center; color: #1a1a2e; }
.form {
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.form input, .form textarea {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
}
.form textarea { height: 80px; resize: vertical; }
.form button {
padding: 10px 24px;
background: #1a73e8;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
}
.note-item {
padding: 16px;
background: white;
border-radius: 10px;
margin-bottom: 10px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.note-title { font-weight: 600; color: #333; }
.note-content { color: #666; margin-top: 6px; font-size: 14px; }
.note-time { font-size: 12px; color: #aaa; margin-top: 8px; }
.note-actions {
margin-top: 8px;
display: flex;
gap: 8px;
}
.note-actions button {
padding: 4px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.btn-delete { background: #fee2e2; color: #dc2626; }
.stats { margin-top: 12px; font-size: 13px; color: #888; }
</style>
</head>
<body>
<div class="app">
<h1>IndexedDB 笔记应用</h1>
<div class="form">
<input type="text" id="noteTitle" placeholder="标题">
<textarea id="noteContent" placeholder="内容"></textarea>
<button onclick="addNote()">添加笔记</button>
</div>
<div id="noteList"></div>
<div class="stats" id="stats"></div>
</div>
<script>
const DB_NAME = 'NotesApp';
const DB_VERSION = 1;
const STORE_NAME = 'notes';
let db = null;
// 初始化数据库
function initDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = (event) => {
const database = event.target.result;
if (!database.objectStoreNames.contains(STORE_NAME)) {
const store = database.createObjectStore(STORE_NAME, {
keyPath: 'id',
autoIncrement: true
});
store.createIndex('title', 'title', { unique: false });
store.createIndex('createdAt', 'createdAt', { unique: false });
}
};
request.onsuccess = (event) => {
db = event.target.result;
resolve(db);
loadNotes();
};
request.onerror = (event) => reject(event.target.error);
});
}
// 添加笔记
function addNote() {
const title = document.getElementById('noteTitle').value.trim();
const content = document.getElementById('noteContent').value.trim();
if (!title) { alert('请输入标题'); return; }
const note = {
title,
content,
createdAt: Date.now()
};
const transaction = db.transaction(STORE_NAME, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
store.add(note);
transaction.oncomplete = () => {
document.getElementById('noteTitle').value = '';
document.getElementById('noteContent').value = '';
loadNotes();
};
}
// 加载所有笔记
function loadNotes() {
const transaction = db.transaction(STORE_NAME, 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.getAll();
request.onsuccess = () => {
const notes = request.result.sort((a, b) => b.createdAt - a.createdAt);
renderNotes(notes);
document.getElementById('stats').textContent =
`共 ${notes.length} 条笔记,存储在 IndexedDB 中`;
};
}
// 渲染笔记
function renderNotes(notes) {
const container = document.getElementById('noteList');
container.innerHTML = notes.map(note => `
<div class="note-item">
<div class="note-title">${note.title}</div>
<div class="note-content">${note.content}</div>
<div class="note-time">${new Date(note.createdAt).toLocaleString()}</div>
<div class="note-actions">
<button class="btn-delete" onclick="deleteNote(${note.id})">删除</button>
</div>
</div>
`).join('');
}
// 删除笔记
function deleteNote(id) {
const transaction = db.transaction(STORE_NAME, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
store.delete(id);
transaction.oncomplete = () => loadNotes();
}
// 初始化
initDB().catch(console.error);
</script>
</body>
</html>注意事项
- IndexedDB 是异步 API,不会阻塞主线程
- 数据库结构只能在
onupgradeneeded中修改 - 版本号只能升高,不能降低
- 事务会在所有请求完成后自动提交
- 大量数据操作应使用
getAll()而非逐条游标
最佳实践
- 封装 Promise 化的 IndexedDB 操作
- 合理设计索引以提升查询性能
- 批量操作使用事务减少开销
- 为每个应用使用独立的数据库和版本
- 处理
onblocked事件处理多标签页升级冲突
下一节
继续学习:Cookie 对比