====== 第七部分:游标(Cursor)与范围查询 ======
===== 7.1 游标概述 =====
游标(Cursor)是 IndexedDB 中遍历数据的机制。与 getAll() 一次性加载所有数据不同,游标可以逐条处理记录,适合大数据集的遍历和复杂查询场景。
===== 7.1.1 游标 vs getAll =====
^ 特性 ^ 游标 ^ getAll ^
| 内存占用 | 低(逐条加载) | 高(全部加载) |
| 适用数据量 | 大数据集 | 小数据集 |
| 能否中途停止 | 能 | 不能 |
| 能否边遍历边修改 | 能 | 不能 |
| 使用复杂度 | 较高 | 简单 |
| 性能 | 适合大量数据 | 适合少量数据 |
===== 7.2 基本游标操作 =====
===== 7.2.1 打开游标 =====
// 基本语法
const request = store.openCursor();
const request = store.openCursor(query);
const request = store.openCursor(query, direction);
// 参数说明
// query: 主键值或 IDBKeyRange
// direction: 'next', 'nextunique', 'prev', 'prevunique'
===== 7.2.2 正向遍历 =====
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.openCursor();
request.onsuccess = function(event) {
const cursor = event.target.result;
if (cursor) {
console.log('主键:', cursor.key);
console.log('数据:', cursor.value);
// 继续下一条
cursor.continue();
} else {
console.log('遍历完成');
}
};
===== 7.2.3 反向遍历 =====
// 从最后一条开始遍历
const request = store.openCursor(null, 'prev');
request.onsuccess = function(event) {
const cursor = event.target.result;
if (cursor) {
console.log('倒序 - 主键:', cursor.key);
cursor.continue();
}
};
===== 7.3 范围查询 =====
===== 7.3.1 IDBKeyRange 详解 =====
// 等于某个值
IDBKeyRange.only(value);
// 大于等于 lower
IDBKeyRange.lowerBound(lower, open);
// open = true: 大于(不包含 lower)
// open = false: 大于等于(包含 lower)
// 小于等于 upper
IDBKeyRange.upperBound(upper, open);
// 在 lower 和 upper 之间
IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen);
// 示例
IDBKeyRange.only(25); // = 25
IDBKeyRange.lowerBound(20); // >= 20
IDBKeyRange.lowerBound(20, true); // > 20
IDBKeyRange.upperBound(30); // <= 30
IDBKeyRange.upperBound(30, true); // < 30
IDBKeyRange.bound(20, 30); // 20 <= x <= 30
IDBKeyRange.bound(20, 30, true, true); // 20 < x < 30
IDBKeyRange.bound(20, 30, false, true); // 20 <= x < 30
===== 7.3.2 范围查询示例 =====
// 查询年龄在 20-30 之间的用户
const ageIndex = store.index('ageIndex');
const range = IDBKeyRange.bound(20, 30);
const request = ageIndex.openCursor(range);
// 查询名字以 "张" 开头的用户
const nameIndex = store.index('nameIndex');
const nameRange = IDBKeyRange.bound('张', '张\uffff');
const request2 = nameIndex.openCursor(nameRange);
// 查询创建时间在最近 7 天的记录
const dateIndex = store.index('createdAtIndex');
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 3600 * 1000);
const dateRange = IDBKeyRange.lowerBound(sevenDaysAgo);
const request3 = dateIndex.openCursor(dateRange);
===== 7.4 索引游标 =====
===== 7.4.1 通过索引遍历 =====
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const ageIndex = store.index('ageIndex');
// 按年龄排序遍历
const request = ageIndex.openCursor();
request.onsuccess = function(event) {
const cursor = event.target.result;
if (cursor) {
// cursor.key 是索引值(年龄)
// cursor.primaryKey 是主键
// cursor.value 是完整对象
console.log(`年龄: ${cursor.key}, 用户ID: ${cursor.primaryKey}`);
cursor.continue();
}
};
===== 7.4.2 键游标 =====
键游标只遍历键,不加载完整对象,性能更好:
// 只获取键
const request = store.openKeyCursor();
request.onsuccess = function(event) {
const cursor = event.target.result;
if (cursor) {
console.log('键:', cursor.key);
// cursor.value 为 undefined
cursor.continue();
}
};
// 索引键游标
const index = store.index('ageIndex');
const request2 = index.openKeyCursor();
request2.onsuccess = function(event) {
const cursor = event.target.result;
if (cursor) {
console.log('年龄:', cursor.key);
console.log('主键:', cursor.primaryKey);
cursor.continue();
}
};
===== 7.5 高级游标操作 =====
===== 7.5.1 更新游标当前记录 =====
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.openCursor();
request.onsuccess = function(event) {
const cursor = event.target.result;
if (cursor) {
const user = cursor.value;
// 修改数据
if (user.status === 'inactive') {
user.lastLogin = new Date();
user.status = 'active';
// 更新当前记录
const updateRequest = cursor.update(user);
updateRequest.onsuccess = () => {
console.log('更新成功:', user.id);
};
}
cursor.continue();
}
};
===== 7.5.2 删除游标当前记录 =====
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.openCursor();
request.onsuccess = function(event) {
const cursor = event.target.result;
if (cursor) {
const user = cursor.value;
// 删除符合条件的记录
if (user.createdAt < Date.now() - 365 * 24 * 3600 * 1000) {
const deleteRequest = cursor.delete();
deleteRequest.onsuccess = () => {
console.log('删除成功:', user.id);
};
}
cursor.continue();
}
};
===== 7.5.3 提前终止遍历 =====
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
let count = 0;
const maxResults = 10;
const request = store.openCursor();
request.onsuccess = function(event) {
const cursor = event.target.result;
if (cursor && count < maxResults) {
console.log(cursor.value);
count++;
cursor.continue();
} else {
console.log(`已获取 ${count} 条记录,遍历结束`);
// 不调用 cursor.continue(),遍历自动停止
}
};
===== 7.6 分页实现 =====
===== 7.6.1 偏移量分页 =====
function getPage(storeName, page = 1, pageSize = 20, options = {}) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
let index = store;
if (options.sortBy) {
try {
index = store.index(`${options.sortBy}Index`);
} catch (e) {
console.warn(`索引 ${options.sortBy}Index 不存在,使用主键排序`);
}
}
const direction = options.sortOrder === 'desc' ? 'prev' : 'next';
const skip = (page - 1) * pageSize;
const results = [];
let skipped = 0;
const request = index.openCursor(null, direction);
request.onsuccess = function(event) {
const cursor = event.target.result;
if (!cursor) {
resolve(results);
return;
}
if (skipped < skip) {
skipped++;
cursor.continue();
return;
}
if (results.length < pageSize) {
results.push(cursor.value);
cursor.continue();
} else {
resolve(results);
}
};
request.onerror = () => reject(request.error);
});
}
// 使用
const page1 = await getPage('users', 1, 20, { sortBy: 'createdAt', sortOrder: 'desc' });
const page2 = await getPage('users', 2, 20, { sortBy: 'createdAt', sortOrder: 'desc' });
===== 7.6.2 游标分页 =====
更高效的分页方式,使用上次的位置继续:
async function* paginate(storeName, pageSize = 20) {
const transaction = db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
let hasMore = true;
let lastKey = null;
while (hasMore) {
const results = [];
let count = 0;
await new Promise((resolve, reject) => {
let request;
if (lastKey) {
request = store.openCursor(IDBKeyRange.lowerBound(lastKey, true));
} else {
request = store.openCursor();
}
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor && count < pageSize) {
results.push(cursor.value);
lastKey = cursor.key;
count++;
cursor.continue();
} else {
hasMore = !!cursor;
resolve();
}
};
request.onerror = () => reject(request.error);
});
yield results;
}
}
// 使用
const pager = paginate('users', 50);
for await (const page of pager) {
console.log('获取到', page.length, '条记录');
// 处理数据...
}
===== 7.7 本章小结 =====
本章介绍了游标的使用方式:
* 游标适合大数据集的逐条遍历
* IDBKeyRange 提供强大的范围查询能力
* 索引游标可按非主键属性排序遍历
* 可以在遍历过程中更新或删除记录
* 分页查询可结合游标实现高效翻页
继续阅读 [[part08-upgrade|第八部分:数据库版本升级]]。