ARTICLE DETAIL

资讯详情

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

openCursor 游标方向教程:用 TaoToken 让 Codex 走通 nextunique 示例

openCursor 游标方向教程:用 TaoToken 让 Codex 走通 nextunique 示例 openCursor() 的第二个参数 nextunique 到底有没有跳过重复记录你把《JavaScript高级程序设计》里的游标方向示例抄进浏览器Console 只给出一个 IDBRequest肉眼很难判断 prev 和 prevunique 的差别。这篇就从这段不确定的输出说起在 TaoToken 上打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建一把 Key把 Codex 的 Base URL 指到 https://taotoken.net/api让 Codex 帮你生成一份带逐条日志的 IndexedDB 测试页。你在本地浏览器跑完把 Console 输出贴回对话就能逐条核对游标方向参数有没有真的生效。很多教程只告诉你“nextunique 会跳过重复”却没有提醒你在对象存储的主键游标上主键本身就是唯一的nextunique 和 next 跑出来的条数可能一模一样。真正能看出差异的地方是把同样的方向参数放到username 索引上。下面按原文的节奏走一遍从 openCursor 的方向参数到索引游标再到 getKey、indexNames 和 deleteIndex每一步都配上可运行的验证代码。1. 从 nextunique 的 Console 输出说起游标方向为什么“看起来没生效”1.1 原文示例里最容易被忽略的两个参数位openCursor() 的第一个参数是 IDBKeyRange 实例第二个参数是表示方向的字符串。默认方向是 next游标从对象存储的第一条记录开始每次调用 continue() 或 advance() 都往最后一条记录前进。如果传入 null 作为第一个参数表示键范围覆盖所有值不额外做范围过滤。当对象存储里存在重复记录时可以把第二个参数改成 nextunique让游标跳过重复项。也可以传入 prev 或 prevunique 创建反向游标从最后一条记录往第一条记录移动。反向游标每次调用 continue() 或 advance() 都会在对象存储中反向移动。问题在于很多人抄完示例只看到request.onsuccess触发了一次或者只打印了event.target.result没有在回调里持续调用cursor.continue()于是 next 和 nextunique 的输出看起来差不多。更隐蔽的坑是你如果只在对象存储上测主键是自增 id每条记录的主键都不同nextunique 根本没有重复主键可跳过。要验证“跳过重复”这个行为必须把游标建在允许重复的索引键上。1.2 为什么要把 Console 输出贴回 Codex而不是让 Codex 直接跑Codex 不能直接打开你本地的浏览器也不能替你在页面上执行 IndexedDB 操作。它能做的是根据你的描述生成测试页、解释每一行日志、对照 next 和 nextunique 的输出差异。你需要在本地浏览器里运行代码把 Console 输出复制回对话让 Codex 帮你核对结果。这个分工很重要。TaoToken 负责让 Codex 的模型请求正常返回不负责执行你的浏览器代码。你把 Codex 的 Base URL 填成https://taotoken.net/api模型 ID 按模型广场当时的列表来选配置完成后Codex 就能正常生成和解释这段 IndexedDB 验证代码。2. 把 Codex 的 config.toml 接到 TaoToken拿 Key、填 Base URL、选模型2.1 打开 TaoToken 创建 Key并确认模型 ID准备材料只有三样一个可用的 Codex CLI、一把 TaoToken API Key、一个当前可用的模型 ID。打开 TaoToken 注册登录进入控制台创建 API Key把拿到的 Key 先记下来后面配置里统一用YOUR_API_KEY占位。模型 ID 不要凭记忆写也不要把网上看到的旧 ID 直接抄进来以 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 里的模型广场当时列表为准。Codex 的配置文件通常放在~/.codex/config.tomlWindows 下是%USERPROFILE%\.codex\config.toml。如果目录不存在就手动建一个。注意这里写的是 TOML不是 JSON也不是 Claude Code 那套ANTHROPIC_*环境变量。把 Codex 的配置项套成 Claude Code 的格式是最常见的错误之一。2.2 ~/.codex/config.toml 里的 model_provider 与 base_url打开配置文件写入下面这段。model_provider指向你自定义的 provider 名称base_url填https://taotoken.net/api末尾不要加/v1。env_key是 Codex 去读取 API Key 的环境变量名你可以自己起名这里用TAOTOKEN_API_KEY。model_provider taotoken model YOUR_MODEL_ID [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY保存后设置环境变量。macOS 或 Linuxexport TAOTOKEN_API_KEYYOUR_API_KEY codexWindows PowerShell$env:TAOTOKEN_API_KEYYOUR_API_KEY codex这里有一个容易混淆的点官网落地页是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end用来注册、创建 Key、看模型广场和用量填进 Codex 的 Base URL 是https://taotoken.net/api不要把 UTM 参数加到 API 地址上也不要写成https://taotoken.net/api/v1。2.3 启动后先问一个 IndexedDB 小问题配置保存后运行codex先问一个和本篇直接相关的问题“在 IndexedDB 里store.openCursor(null, nextunique) 和 index.openCursor(null, nextunique) 的输出有什么差别”如果 Codex 能正常返回解释说明 TaoToken 通道和模型 ID 都填对了。如果报 401优先检查TAOTOKEN_API_KEY环境变量是否真的在当前终端生效以及 Key 是否复制完整。如果报 404 或提示找不到接口检查base_url是不是多写了/v1或结尾多了斜杠。如果提示模型不存在回到 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 的模型广场重新确认模型 ID。排障顺序不要反过来先确认通道通再让 Codex 生成代码。3. 让 Codex 生成一份 users 测试页next 与 nextunique 的日志对照3.1 给 Codex 的提示词和单文件 HTML 骨架在 Codex 里发一条尽量具体的提示词让它生成一个单文件 HTML要求包含对象存储游标和索引游标两组测试“请生成一个单文件 HTML使用 IndexedDB 创建 users 对象存储主键 id 自增并在 username 上创建非唯一索引。插入 5 条记录其中 alice 和 bob 各出现两次。分别用 store.openCursor(null, direction) 和 index.openCursor(null, direction) 遍历direction 依次为 next、nextunique、prev、prevunique。每次 onsuccess 打印当前游标的方向、索引键、主键和整条记录。最后把所有 Console 输出整理成表格。”Codex 返回代码后不要直接全信。重点看三个地方索引的 unique 是否为 false、回调里有没有cursor.continue()、索引游标里是否同时打印了cursor.key和cursor.primaryKey。下面是一份可以直接复制到本地 HTML 文件的版本你可以用它对照 Codex 生成的结果。!doctype html html langzh-CN head meta charsetutf-8 titleIndexedDB openCursor 方向验证/title /head body script const DB_NAME cursor-direction-demo; const DB_VERSION 1; const STORE users; function openDB() { return new Promise((resolve, reject) { const req indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded (event) { const db event.target.result; if (!db.objectStoreNames.contains(STORE)) { const store db.createObjectStore(STORE, { keyPath: id, autoIncrement: true }); store.createIndex(username, username, { unique: false }); } }; req.onsuccess () resolve(req.result); req.onerror () reject(req.error); }); } function seed(db) { return new Promise((resolve, reject) { const tx db.transaction(STORE, readwrite); const store tx.objectStore(STORE); store.clear(); [ { username: alice, age: 20 }, { username: bob, age: 21 }, { username: alice, age: 22 }, { username: carol, age: 23 }, { username: bob, age: 24 } ].forEach((row) store.add(row)); tx.oncomplete () resolve(); tx.onerror () reject(tx.error); }); } function walkStore(db, direction) { return new Promise((resolve, reject) { const tx db.transaction(STORE, readonly); const store tx.objectStore(STORE); const req store.openCursor(null, direction); const log []; req.onsuccess (event) { const cursor event.target.result; if (!cursor) { console.log([对象存储 ${direction}] 结束共 ${log.length} 条, log); resolve(log); return; } const row { 主键: cursor.key, username: cursor.value.username, age: cursor.value.age }; log.push(row); console.log([对象存储 ${direction}], row); cursor.continue(); }; req.onerror () reject(req.error); }); } function walkIndex(db, direction) { return new Promise((resolve, reject) { const tx db.transaction(STORE, readonly); const store tx.objectStore(STORE); const index store.index(username); const req index.openCursor(null, direction); const log []; req.onsuccess (event) { const cursor event.target.result; if (!cursor) { console.log([索引 username ${direction}] 结束共 ${log.length} 条, log); resolve(log); return; } const row { 索引键: cursor.key, 主键: cursor.primaryKey, username: cursor.value.username, age: cursor.value.age }; log.push(row); console.log([索引 username ${direction}], row); cursor.continue(); }; req.onerror () reject(req.error); }); } (async () { const db await openDB(); await seed(db); for (const direction of [next, nextunique, prev, prevunique]) { await walkStore(db, direction); } for (const direction of [next, nextunique, prev, prevunique]) { await walkIndex(db, direction); } })().catch((err) console.error(err)); /script /body /html3.2 本地跑完把日志贴回 Codex逐行核对把上面代码保存为cursor.html用浏览器打开按 F12 打开 DevTools 的 Console 面板刷新页面。你会看到两组日志对象存储游标和索引游标。对象存储的 next 输出 5 条nextunique 大概率也是 5 条因为主键 id 是自增的没有重复主键可以跳。prev 和 prevunique 同样是 5 条只是顺序反过来。索引游标就不一样了。索引 username 上存在重复值next 输出 5 条nextunique 只输出 3 条alice、bob、carol 各取第一次出现的那条记录。prev 输出 5 条反向记录prevunique 也输出 3 条但取的是每个 username 最后出现的那条记录顺序是 carol、bob、alice。把这两组日志复制回 Codex让它逐行解释哪一条被跳过、为什么被跳过比你自己盯着IDBRequest猜要快得多。这里也解释了一个常见疑问为什么原文在对象存储上写openCursor(null, nextunique)你照抄后却看不出跳过效果。不是参数没生效而是对象存储的主键本身唯一nextunique 没有重复主键可跳。把同样的方向参数放到允许重复的索引上差异才会出现在日志里。3.3 对象存储游标与索引游标的 key/value 差异在对象存储游标里cursor.key是主键cursor.value是整条记录对象。在索引游标里cursor.key是索引键cursor.primaryKey是主键cursor.value仍然是整条记录。很多示例只打印cursor.value所以你看不到索引键的变化也就无法判断 nextunique 到底跳过了哪个 username。把打印语句拆开是验证游标方向最省事的办法。对象存储游标打印cursor.key、cursor.value.username索引游标打印cursor.key、cursor.primaryKey、cursor.value.username。两边同时跑 next 和 nextunique对照条数和顺序结论就出来了。4. 索引上的游标createIndex、index.openCursor 与 openKeyCursor 怎么配合方向参数4.1 createIndex 的三个参数与 unique 的坑原文用store.createIndex(username, username, { unique: true })创建索引并说明第一个参数是索引名称第二个参数是索引属性的路径第三个参数是包含unique的 options 对象。如果你只是学索引创建unique: true没问题但如果你想验证 nextunique 跳过重复索引键就千万不能把 username 索引设成唯一否则插入重复 username 时会直接抛出 ConstraintError游标连打开的机会都没有。所以本篇测试页里用的是{ unique: false }故意让 username 可以重复这样才能在索引游标上看到 nextunique 和 next 的条数差异。等你验证完再按真实业务决定是否需要唯一索引。真实项目里用户 ID 做主键、用户名做唯一索引是很常见的组合但测试游标方向时不要把这个顺序搞反。4.2 index.openCursor 的 result.key 是索引键在对象存储上调用index(username)可以得到一个 IDBIndex 实例在它上面调用openCursor()参数与对象存储上的openCursor()完全一样。最大的不同是event.result.key保存的是索引键而不是主键。也就是说索引游标遍历时cursor.key是 usernamecursor.primaryKey才是原来的自增 id。这一点直接影响你对 nextunique 的判断。对象存储游标看主键去重索引游标看索引键去重。你在 username 索引上跑index.openCursor(null, nextunique)看到 alice 只出现一次、bob 只出现一次就说明方向参数真的在索引键上生效了。4.3 openKeyCursor 只返回主键不返回整条记录IDBIndex 还提供了openKeyCursor()参数与openCursor()一样。它的特点是event.result.key是索引键event.result.value是主键而不是整条记录。也就是说它只告诉你“这个索引键对应哪条主键”不把整条记录读出来。当你只需要主键列表或者只需要确认游标方向时openKeyCursor 比 openCursor 更轻。可以在测试页里追加一个walkKeyIndex函数用index.openKeyCursor(null, direction)遍历打印cursor.key和cursor.value。你会看到 index.openCursor 的cursor.value是完整记录而 index.openKeyCursor 的cursor.value是主键。两者在 nextunique 下跳过的重复项一致只是返回内容不同。5. get、getKey、indexNames 与 deleteIndex补全验证清单5.1 index.get 与 index.getKey 的返回值差异除了游标索引还支持用get()和getKey()按索引键取单条记录。index.get(007)会创建一个新请求成功时event.target.result是整条记录index.getKey(007)同样创建新请求但event.target.result是主键而不是整条记录。原文里提到event.target.result.value在主键查询场景下才是用户 ID这一点在签名或只取主键时很有用。把这两个方法加进验证清单先确认索引里存在某个 username再用 get 取整条记录用 getKey 取主键对比控制台输出。如果 getKey 返回的是主键而不是记录对象说明你用的确实是 getKey而不是把 get 的返回误当成主键。5.2 用 indexNames 查看对象存储上已有哪些索引对象存储自身有一个indexNames属性保存着与之相关索引的名称。可以写一个循环把每个索引的名称、keyPath 和 unique 都打印出来。原文里那段for (let indexName in indexNames)的思路是对的只是真实项目里建议用Array.from(store.indexNames)或forEach避免把原型链上的属性也遍历进来。const tx db.transaction(users, readonly); const store tx.objectStore(users); Array.from(store.indexNames).forEach((name) { const index store.index(name); console.log({ name: index.name, keyPath: index.keyPath, unique: index.unique }); });IDBIndex 的name、keyPath、objectStore、unique四个属性刚好能帮你确认索引是否按预期创建。尤其unique如果你发现测试页里 username 索引是 true那就解释了你为什么插不进重复数据而不是游标方向没生效。5.3 deleteIndex 之后游标还能不能跑在对象存储上调用deleteIndex(username)并传入索引名称可以删除索引。删除索引不会影响对象存储里的数据这个操作也没有回调。你可以先跑一遍索引游标确认 nextunique 输出了 3 条然后删掉 username 索引再用store.index(username)就会直接抛错而不是返回空游标。这时只能重新建索引原来基于该索引的游标请求也不会继续。所以验证顺序建议是先建索引并插入重复数据跑完 next、nextunique、prev、prevunique 四组日志把输出贴回 Codex 确认再考虑是否删除索引或调整 unique 设置。不要一边验证一边删索引否则你会分不清是参数没生效还是索引已经被移除了。6. 这次 Codex 调用记上了吗回控制台看用量与下一步配置保存后先在 TaoToken 模型对话 里用同一把 Key 发一条测试消息确认模型 ID 和 Base URL 没填错。如果你打算让 Codex 长期帮你解释 IndexedDB 输出、对照游标日志可以打开 Coding Plan 看套餐是否够用Key 在 控制台 API Keys 创建模型广场也在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 里。接下来可以试一个更小的练习把 prevunique 在 username 索引上的输出单独贴给 Codex让它标出每条记录属于哪个 username、取的是该 username 的第几次出现。再把index.openKeyCursor(null, nextunique)的输出贴进去对比cursor.value为什么从整条记录变成了主键。两轮下来openCursor 的第二个参数就不再是靠猜的了。
返回列表