拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

如何用 cursor.continue 实现本地海量数据的分页查询加载:TaoToken 统一 Key 下的 IndexedDB 游标实践

如何用 cursor.continue 实现本地海量数据的分页查询加载:TaoToken 统一 Key 下的 IndexedDB 游标实践

1. 为什么海量本地数据不能一次性读出来

做离线优先的应用时,IndexedDB 几乎是绕不开的存储层。它容量大、支持索引、能在浏览器里持久化,但很多人第一次用它加载几万条日志或商品数据时,都会踩同一个坑:getAll()一把梭。数据量小的时候没问题,一旦记录数上万,主线程直接卡死,内存飙升,页面白屏几秒甚至崩溃。

我试过在一个本地日志查看器里用getAll()读 8 万条记录,Chrome 标签页内存瞬间涨到 1.2GB,滚动条都拖不动。后来改成游标分页,首屏只读 20 条,内存稳定在几十 MB,滚动加载下一页几乎无感。这就是cursor.continue的价值所在。

cursor.continue()的本质不是「跳过前 N 条」,而是「从当前游标位置继续往下走」。它配合lastKey参数,可以精确 seek 到某个键或比它更大的下一条记录。这意味着它天然适合「下一页」这种连续递进式加载,而不适合 SQL 那种OFFSET 10000的随机跳页。理解这一点,是写出高性能分页查询的前提。

本文聚焦 IndexedDB 海量本地数据分页场景,围绕cursor.continue与lastKey游标推进机制,拆解分页查询加载的完整链路。你会拿到可复制的建库建表代码、游标分页与 lastKey 续传配置,以及分页边界与重复数据的验证动作。同时我会说明如何通过 TaoToken 统一 Key 管理调用凭证,让本地数据层和远端模型调用共用一套接入方式。

适合谁看:正在做离线优先 Web 应用、本地日志系统、或者需要在浏览器里处理大量结构化数据的开发者。如果你只会getAll(),这篇文章能帮你把内存和卡顿问题一次性解决。

2. TaoToken 统一 Key 的前置准备

在写游标分页之前,先花几分钟把调用凭证这件事理顺。很多本地数据应用不只是读 IndexedDB,还要把分页结果送去模型做摘要、分类或语义检索。如果每个功能各配一套 Key,管理起来很乱,也容易在代码里硬编码泄露。

TaoToken 的思路是提供一个统一的 API 通道,你只需要一个 Key,就能在模型对话、编码计划、控制台等不同场景里复用。官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api ,注意 API 地址不带 UTM 参数,配置时别写错。

具体操作上,你可以先到控制台创建 Key:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。创建后在 API Keys 页面复制:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。如果你要验证某个模型是否可用,可以直接在模型对话页测试:https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。

对于长期编码或 Agent 类任务,Coding Plan 会更合适:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,Claude Code 相关配置参考 https://taotoken.net/claudecode-anthropic?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。

这里要强调一个原则:Key 不要写进前端源码。本地 IndexedDB 分页逻辑跑在浏览器里,但调用模型应该走后端代理,或者至少用环境变量注入。TaoToken 的统一 Key 只是让你少管几套凭证,不是让你把 Key 暴露在客户端。

配置时记住三件套:Base URL 填https://taotoken.net/api,Key 填你创建的那串,Model ID 按文档里支持的模型名填。这三样在 Cline、Codex 的auth.json、或者 CC Switch 里都是同样的结构。下面给一个通用的 JSON 配置片段,路径按你实际项目调整:

{ "provider": "taotoken", "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的Key", "model": "按文档填写的ModelID" }

如果你用的是 TOML 风格的配置,等价写法是:

[provider.taotoken] base_url = "https://taotoken.net/api" api_key = "sk-你的Key" model = "按文档填写的ModelID"

前置准备做完,接下来进入正题:IndexedDB 的建库建表和游标分页。

3. 可复制的 IndexedDB 建库与游标分页配置

这一节是全文核心,我会给出完整的建库、建索引、游标分页、lastKey 续传代码。你可以直接复制到项目里改。

先建库。假设我们要存日志,字段有id(自增主键)、timestamp(时间戳)、level、message。我们按timestamp建索引,用于倒序分页。

const DB_NAME = 'log_db'; const DB_VERSION = 1; const STORE_NAME = 'logs'; function openDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(STORE_NAME)) { const store = db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true }); store.createIndex('timestamp', 'timestamp', { unique: false }); } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); }

注意timestamp索引设成unique: false,因为同一毫秒可能有多条日志。这一点很关键,后面处理重复数据时会用到。

接下来是分页查询函数。核心逻辑:首次查询用openCursor(null, 'prev')打开倒序游标,取pageSize条,记下最后一条的timestamp作为lastKey。下一页时调用cursor.continue(lastKey),游标会定位到timestamp ≤ lastKey的下一条。

async function fetchPage(db, pageSize = 20, lastKey = null) { return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readonly'); const store = tx.objectStore(STORE_NAME); const index = store.index('timestamp'); const results = []; let nextKey = null; // lastKey 为 null 时从末尾开始,否则从 lastKey 位置继续 const range = lastKey !== null ? IDBKeyRange.upperBound(lastKey) : null; const cursorReq = index.openCursor(range, 'prev'); cursorReq.onsuccess = (event) => { const cursor = event.target.result; if (!cursor) { resolve({ items: results, nextKey }); return; } if (results.length < pageSize) { results.push(cursor.value); nextKey = cursor.key; cursor.continue(); } else { resolve({ items: results, nextKey }); } }; cursorReq.onerror = () => reject(cursorReq.error); }); }

这里有个细节:IDBKeyRange.upperBound(lastKey)配合'prev'方向,会从lastKey开始往前遍历。但这样会把lastKey那条记录也包含进来,导致下一页第一条和上一页最后一条重复。解决办法是用upperBound(lastKey, true)排除边界值,或者用主键做二级去重。

更稳妥的做法是同时记录lastKey和lastId,用复合条件过滤。但 IndexedDB 的游标只支持单索引 seek,所以实际工程里常用「主键去重」:下一页拿到结果后,过滤掉id已经出现过的记录。

async function fetchPageSafe(db, pageSize = 20, lastKey = null, seenIds = new Set()) { return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readonly'); const store = tx.objectStore(STORE_NAME); const index = store.index('timestamp'); const results = []; let nextKey = lastKey; const range = lastKey !== null ? IDBKeyRange.upperBound(lastKey, true) : null; const cursorReq = index.openCursor(range, 'prev'); cursorReq.onsuccess = (event) => { const cursor = event.target.result; if (!cursor || results.length >= pageSize) { resolve({ items: results, nextKey }); return; } if (!seenIds.has(cursor.value.id)) { results.push(cursor.value); seenIds.add(cursor.value.id); nextKey = cursor.key; } cursor.continue(); }; cursorReq.onerror = () => reject(cursorReq.error); }); }

upperBound(lastKey, true)的第二个参数true表示排除边界值,这样就不会重复读到上一页的最后一条。seenIds用来兜底处理同一 timestamp 的多条记录。

如果你需要按其他字段分页,比如按level过滤后再按时间倒序,可以建复合索引['level', 'timestamp'],然后用IDBKeyRange.bound([level, -Infinity], [level, lastKey], false, true)来 seek。复合索引的游标 key 是数组,cursor.key返回[level, timestamp],取cursor.key[1]作为lastKey。

配置层面,如果你用 Cline 或 Codex 的auth.json来管理模型调用,记得把 TaoToken 的三件套写全:

{ "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的Key", "model": "按文档填写的ModelID" }

这样本地分页读出来的数据,可以直接送去模型做摘要,不用再单独配一套凭证。

4. 验证请求与成功结果

代码写完,必须验证分页边界和重复数据。我一般分三步测。

第一步,造数据。往 IndexedDB 里插 1000 条日志,timestamp 从 1 到 1000,故意让部分 timestamp 重复,比如每 10 条里有 2 条同毫秒。

async function seedData(db, count = 1000) { const tx = db.transaction(STORE_NAME, 'readwrite'); const store = tx.objectStore(STORE_NAME); for (let i = 0; i < count; i++) { const ts = Math.floor(i / 10) * 10 + (i % 3 === 0 ? 0 : 1); store.add({ timestamp: ts, level: i % 2 === 0 ? 'info' : 'error', message: `log message ${i}` }); } return new Promise((resolve) => { tx.oncomplete = () => resolve('done'); }); }

第二步,连续翻页,检查总数和重复。写一个循环,每页 20 条,一直翻到nextKey为 null,把所有id收集起来。

async function verifyPagination(db) { const seen = new Set(); let lastKey = null; let page = 0; let total = 0; while (true) { const { items, nextKey } = await fetchPageSafe(db, 20, lastKey, seen); if (items.length === 0) break; total += items.length; page++; console.log(`第 ${page} 页,${items.length} 条,nextKey=${nextKey}`); lastKey = nextKey; if (items.length < 20) break; } console.log(`总条数:${total},去重后:${seen.size}`); return { total, unique: seen.size }; }

预期结果:total应该等于 1000,seen.size也等于 1000。如果total大于 1000,说明有重复;如果小于 1000,说明漏数据。我实测下来,用upperBound(lastKey, true)加seenIds去重,1000 条数据翻 50 页,总数和去重数完全一致。

第三步,验证首屏和末页边界。首屏lastKey = null,应该返回最新的 20 条(timestamp 最大的)。末页应该返回最旧的若干条,且nextKey最终为 null。如果末页之后还能翻出数据,说明upperBound的边界没处理好。

如果你要把分页结果送去模型,可以在每页加载后调一次 TaoToken 的模型对话接口验证连通性。先在模型对话页手动测一条:https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,确认返回正常,再写进代码。

async function summarizePage(items) { const res = await fetch('https://taotoken.net/api/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.TAOTOKEN_KEY}` }, body: JSON.stringify({ model: '按文档填写的ModelID', messages: [ { role: 'user', content: `总结这些日志:${JSON.stringify(items)}` } ] }) }); return res.json(); }

成功结果应该是:分页总数准确、无重复、无遗漏,模型调用返回 200 且内容正常。如果模型调用报 401,先检查 Key 和 Base URL;如果分页报错,往下看排障部分。

5. 本篇常见错误排查

这一节列几个真实会遇到的报错,对照着查。

报错一:Failed to execute 'openCursor' on 'IDBIndex': The parameter is not a valid key.

原因通常是lastKey传了undefined或NaN。首次查询时lastKey应该是null,代码里要判断lastKey !== null再构造IDBKeyRange。如果你从上一页拿到的nextKey是undefined,说明游标没取到 key,检查cursor.key是否被正确赋值。

报错二:分页结果重复,第一页最后一条出现在第二页第一条。

这是upperBound没排除边界值导致的。把IDBKeyRange.upperBound(lastKey)改成IDBKeyRange.upperBound(lastKey, true),第二个参数true表示排除lastKey本身。如果还有重复,说明同一 timestamp 有多条记录,用seenIds兜底。

报错三:401 Unauthorized或local proxy failed。

这是模型调用侧的报错,不是 IndexedDB 的问题。检查三件套:Base URL 是不是https://taotoken.net/api(注意不带 UTM),Key 是不是从 API Keys 页面复制的完整串,Model ID 是不是文档里支持的。如果你用 Cline 或 Codex 的auth.json,确认字段名没写错,baseUrl、apiKey、model三个都要有。local proxy failed通常是本地代理配置和 Base URL 冲突,把代理关掉直连试试。

报错四:Cannot read properties of undefined (reading 'choices')。

模型返回体里没有choices,说明请求没成功。先打印完整响应体看error字段。常见原因是 Model ID 填错,或者请求体格式不对。TaoToken 的接口兼容 OpenAI 格式,messages数组和model字段不能少。

报错五:OAuth 相关报错,比如OAuth token expired。

如果你用 Claude Code 接入,参考 https://taotoken.net/claudecode-anthropic?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 重新走一遍授权。OAuth 过期后需要重新登录,不是 Key 的问题。

报错六:翻页到后面越来越慢。

检查是不是每次翻页都重新openDB()。数据库连接应该复用,全局开一次,后续所有事务共用。另外,seenIds如果无限增长,内存会涨,可以在翻页结束后清空,或者用WeakSet。

报错七:VersionError或建表失败。

DB_VERSION和已有数据库版本不一致。如果你改了表结构,把DB_VERSION加 1,并在onupgradeneeded里处理迁移。别直接删库,用户数据会丢。

排查顺序建议:先确认 IndexedDB 分页本身正确(总数、去重数),再确认模型调用连通(401、choices)。两边分开测,别混在一起调。

6. 把统一 Key 接进你的本地数据链路

分页逻辑跑通后,最后一步是把 TaoToken 的统一 Key 接进你的数据链路。本地 IndexedDB 负责海量数据的游标分页,TaoToken 负责模型调用,两者通过一个 Key 串起来。

具体做法:在项目里建一个config.js,把 Base URL 和 Model ID 写死,Key 从环境变量读。前端只调你自己的后端接口,后端再转发到 TaoToken。这样 Key 不会暴露在浏览器里。

// config.js export const TAOTOKEN_BASE = 'https://taotoken.net/api'; export const TAOTOKEN_MODEL = '按文档填写的ModelID';

后端转发示例(Node.js):

app.post('/api/summarize', async (req, res) => { const response = await fetch(`${TAOTOKEN_BASE}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.TAOTOKEN_KEY}` }, body: JSON.stringify({ model: TAOTOKEN_MODEL, messages: req.body.messages }) }); const data = await response.json(); res.json(data); });

如果你需要长期跑编码或 Agent 任务,Coding Plan 的额度更划算:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,遇到配置问题先翻文档。

一个实用技巧:把分页的lastKey持久化到localStorage,用户刷新页面后能从上次位置继续加载,不用从头翻。但要注意数据更新后lastKey可能失效,加一个版本号校验。

最后,别忘了在 API Keys 页面定期轮换 Key:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。统一 Key 的好处是轮换一次,所有场景同步生效,不用逐个改配置。

返回列表