====== 第七部分:游标(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|第八部分:数据库版本升级]]。