1. 原生 JS 分页横向图片浏览为什么要接统一 Key 通道
分页浏览横向图片(类轮播)这个需求,前端圈里几乎人人都写过:一个overflow-x: hidden的容器,里面塞一排img,点「上一页 / 下一页」时用innerHTML换一批图。它不依赖任何框架,纯原生 JS 就能跑,特别适合做图片墙、商品缩略图、相册预览这类轻量场景。
但真正把它放到业务里,问题就来了。图片列表往往不是写死在 HTML 里的,而是从后端接口拉回来的;而接口调用又绕不开鉴权——Key 放前端怕泄露,放后端要维护一套代理,多个环境(本地、测试、预发)还要各配一份。我试过最省事的做法,是把模型/接口调用统一收敛到一个 Key 通道上,前端只认一个settings.json骨架,切换环境只改一个字段。
这篇就聚焦两件事:一是原生 JS 分页横向图片的核心逻辑怎么写得稳、可扩展;二是怎么通过 TaoToken 的统一 Key/API 通道,把接口调用接进来,并给出一份可以直接复制的settings.json配置骨架,最后用几个验证动作确认连通性。适合正在写图片浏览组件、又不想在鉴权上反复折腾的前端同学。
TaoToken 在这里扮演的角色,是一个统一的 API 入口:你拿到一个 Key,就能通过https://taotoken.net/api调用模型对话、代码补全等能力。官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册和文档都在上面。对图片浏览场景来说,它最实用的地方是:当你想给图片加「AI 生成描述」「智能打标签」这类能力时,不用再单独接一套鉴权,直接复用同一个 Key 通道即可。
2. TaoToken 前置准备:Key、通道与 settings.json 定位
在动手写配置之前,先把几个概念对齐,不然后面容易懵。
TaoToken 的 API 基址是https://taotoken.net/api,注意这个地址不带任何查询参数,是纯粹的接口前缀。你所有的请求都拼在它后面,比如模型对话通常是/v1/chat/completions这类路径。Key 则在控制台里生成,形如sk-开头的一串字符。控制台入口在 https://taotoken.net/console ,API Keys 管理页在 https://taotoken.net/api-keys 。
settings.json这个文件,本质上是前端项目的「环境配置中心」。原生 JS 项目没有构建工具时,你可以把它放在config/目录下,用fetch读进来;有构建工具时,它可以是public/settings.json,运行时加载。它的作用是:把 API 基址、Key、超时、分页参数这些易变的东西集中管理,代码里只引用字段名,不写死值。
这里有个关键点:Key 不要硬编码进 JS 源码。哪怕是小项目,也建议把 Key 放在settings.json里,并且这个文件在部署时由服务端注入或通过环境变量替换。前端代码只负责读取,不负责存储明文。如果你只是本地自测,那直接写进settings.json没问题,但上线前一定要换掉。
配置骨架大致长这样,字段含义我后面会逐个解释:
{ "api": { "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的Key", "timeout": 15000, "model": "claude-3-5-sonnet" }, "gallery": { "pageSize": 4, "containerId": "div1", "imageDir": "images/", "imageExt": ".jpg" }, "debug": { "logLevel": "info", "mock": false } }api段管接口,gallery段管图片浏览,debug段管调试开关。这样分层的好处是:换模型只改model,换图片目录只改imageDir,互不影响。
3. 可复制配置:settings.json 骨架与加载逻辑
先给一份完整可用的settings.json,你可以直接复制到项目里改字段值。
{ "api": { "baseUrl": "https://taotoken.net/api", "apiKey": "sk-替换成你的Key", "timeout": 15000, "model": "claude-3-5-sonnet", "maxRetries": 2 }, "gallery": { "pageSize": 4, "containerId": "div1", "imageDir": "images/", "imageExt": ".jpg", "totalImages": 7, "loop": false }, "debug": { "logLevel": "info", "mock": false } }字段说明用表格对照更清楚:
| 字段 | 含义 | 建议值 |
|---|---|---|
api.baseUrl | 接口前缀 | 固定https://taotoken.net/api |
api.apiKey | 鉴权 Key | 控制台生成,勿提交仓库 |
api.timeout | 请求超时毫秒 | 10000–20000 |
api.model | 默认模型名 | 按文档选 |
api.maxRetries | 失败重试次数 | 1–3 |
gallery.pageSize | 每页图片数 | 4 或 6 |
gallery.containerId | 横向容器 id | 与 HTML 一致 |
gallery.imageDir | 图片目录 | 相对路径 |
gallery.totalImages | 图片总数 | 与后端一致 |
gallery.loop | 是否循环翻页 | 布尔 |
debug.mock | 是否用假数据 | 本地调试 true |
加载逻辑用一个独立的config.js处理,避免和业务代码混在一起:
// config.js let SETTINGS = null; async function loadSettings() { if (SETTINGS) return SETTINGS; const res = await fetch('config/settings.json'); if (!res.ok) throw new Error('settings.json 加载失败: ' + res.status); SETTINGS = await res.json(); return SETTINGS; } function getApiConfig() { if (!SETTINGS) throw new Error('请先调用 loadSettings()'); return SETTINGS.api; }注意fetch读本地 JSON 时,如果你直接双击打开index.html(file://协议),浏览器会因为跨域策略拒绝读取。解决办法是用一个静态服务器,比如npx serve .或python -m http.server,这也是后面验证动作的前提。
4. 分页横向图片核心逻辑:从写死到配置驱动
原始写法里,pre()和next()把图片路径拼死成images/1.jpg到images/4.jpg,页数、每页数量都是硬编码。这种写法能跑,但一改需求就崩。我们把它改成配置驱动。
核心思路是:用「当前页」和「每页数量」算出这一页的图片索引区间,再统一渲染。这样不管每页 4 张还是 6 张,逻辑都一样。
// gallery.js let currentPage = 1; let totalPage = 1; let cfg = null; function initGallery(settings) { cfg = settings.gallery; totalPage = Math.ceil(cfg.totalImages / cfg.pageSize); renderPage(1); } function renderPage(page) { if (page < 1 || page > totalPage) return; currentPage = page; const start = (page - 1) * cfg.pageSize + 1; const end = Math.min(page * cfg.pageSize, cfg.totalImages); let html = ''; for (let i = start; i <= end; i++) { html += `<img src="${cfg.imageDir}${i}${cfg.imageExt}" alt="图片${i}">`; } document.getElementById(cfg.containerId).innerHTML = html; updatePager(); } function pre() { if (currentPage === 1) { if (cfg.loop) renderPage(totalPage); else console.log('已经是第一页了'); return; } renderPage(currentPage - 1); } function next() { if (currentPage === totalPage) { if (cfg.loop) renderPage(1); else console.log('已经是最后一页了'); return; } renderPage(currentPage + 1); } function updatePager() { const el = document.getElementById('pager'); if (el) el.textContent = `${currentPage} / ${totalPage}`; }HTML 部分保持极简,容器和按钮分开:
<div class="div1" id="div1"></div> <a onclick="pre()">上一页</a> <a onclick="next()">下一页</a> <span id="pager"></span>CSS 里overflow-x: hidden配合white-space: nowrap让图片横向排列,超出部分隐藏。如果你想要平滑滚动效果,可以把hidden换成auto并加scroll-behavior: smooth,但那样会出现滚动条,看设计取舍。
.div1 { width: 410px; height: 100px; overflow-x: hidden; white-space: nowrap; } .div1 img { display: inline-block; height: 100px; margin-right: 4px; }到这里,分页浏览本身已经能跑了。接下来才是重点:怎么把 TaoToken 的接口调用接进来,让图片浏览具备「AI 能力」。
5. 接入 TaoToken:统一 Key 通道与请求封装
图片浏览场景接 TaoToken,最常见的用法是给图片生成描述或标签。比如用户翻到某一页,前端把这一页的图片信息发给模型,返回一段文字描述,展示在图片下方。
请求封装要处理三件事:拼 URL、带鉴权头、处理超时和重试。下面是一个通用的request函数:
// api.js async function request(path, payload) { const api = getApiConfig(); const url = api.baseUrl + path; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), api.timeout); let lastErr = null; for (let i = 0; i <= api.maxRetries; i++) { try { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + api.apiKey }, body: JSON.stringify(payload), signal: controller.signal }); clearTimeout(timer); if (!res.ok) throw new Error('HTTP ' + res.status); return await res.json(); } catch (e) { lastErr = e; if (i < api.maxRetries) await new Promise(r => setTimeout(r, 500 * (i + 1))); } } throw lastErr; }调用模型对话接口时,路径通常是/v1/chat/completions,payload 结构如下:
async function describeImages(imageUrls) { const api = getApiConfig(); const payload = { model: api.model, messages: [ { role: 'user', content: '请用一句话描述这些图片的主题:' + imageUrls.join('、') } ] }; const data = await request('/v1/chat/completions', payload); return data.choices?.[0]?.message?.content || ''; }把这段接到翻页逻辑里,每次renderPage之后触发一次描述请求:
async function renderPageWithAI(page) { renderPage(page); const start = (page - 1) * cfg.pageSize + 1; const end = Math.min(page * cfg.pageSize, cfg.totalImages); const urls = []; for (let i = start; i <= end; i++) { urls.push(`${cfg.imageDir}${i}${cfg.imageExt}`); } try { const desc = await describeImages(urls); document.getElementById('desc').textContent = desc; } catch (e) { console.warn('描述生成失败:', e.message); } }这里有个坑要注意:AbortController的timer在重试循环里只设了一次,如果第一次请求就超时,后续重试会立刻被 abort。更严谨的写法是把controller和timer放进循环内部,每次重试重新创建。我在实际项目里踩过这个坑,表现为「重试永远失败」,排查了半天才发现是 signal 被复用了。
6. 验证动作:确认接口连通与分页正确
配置写完,别急着上业务,先做三个验证动作。
动作一:验证 settings.json 能加载。启动静态服务器后,在浏览器控制台执行:
fetch('config/settings.json').then(r => r.json()).then(console.log)能看到完整对象,说明路径和格式没问题。如果报 404,检查文件位置;如果报 JSON 解析错误,用 JSONLint 校验一下。
动作二:验证 Key 通道连通。在控制台直接发一个最小请求:
fetch('https://taotoken.net/api/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer sk-你的Key' }, body: JSON.stringify({ model: 'claude-3-5-sonnet', messages: [{ role: 'user', content: 'ping' }] }) }).then(r => r.json()).then(console.log).catch(console.error)返回里带choices字段就说明通道通了。如果返回 401,检查 Key 是否复制完整、有没有多余空格;返回 404,检查路径拼写;返回超时,检查网络和baseUrl是否写成了带斜杠的版本。
动作三:验证分页边界。把totalImages设成 7、pageSize设成 4,然后手动调用renderPage(1)、renderPage(2),观察第二页是否只渲染 3 张图(7 减 4 等于 3)。再点「下一页」到最后一页,确认不会越界。这一步能提前发现Math.min和Math.ceil的边界问题。
三个动作都通过后,再打开页面点按钮,图片应该正常切换,描述文字也会异步出现。如果描述一直不出现,看控制台有没有描述生成失败的警告,多半是 Key 或模型名的问题。
7. 本篇常见错排查
错误一:settings.json加载失败,控制台报 CORS。这是file://协议导致的,不是配置问题。用npx serve .起一个本地服务,或者用 VS Code 的 Live Server 插件,访问http://localhost:xxxx即可。
错误二:翻页后图片不显示,但控制台无报错。检查imageDir和imageExt拼接后的路径是否和实际文件一致。常见的是imageDir写了images但实际目录是img,或者扩展名大小写不匹配(.JPG和.jpg在部分服务器上不等价)。
错误三:请求返回 401。Key 无效或过期。去 https://taotoken.net/api-keys 重新生成一个,注意复制时不要带上首尾空格。如果 Key 是从环境变量注入的,检查注入逻辑有没有把换行符带进去。
错误四:请求一直 pending 直到超时。大概率是baseUrl写错了,比如写成了https://taotoken.net/api/(末尾多斜杠)导致路径拼成//v1/...。统一去掉末尾斜杠,用baseUrl + path拼接。
错误五:重试逻辑导致重复请求。如果maxRetries设得太大,加上超时时间,用户点一次按钮可能等很久。建议maxRetries不超过 2,超时不超过 15 秒,并且给按钮加一个 loading 状态,防止用户连点。
错误六:图片描述返回乱码或截断。检查Content-Type是否设成了application/json,以及响应解析是否用了res.json()。如果模型返回的是流式数据,需要改用res.body.getReader()逐块读取,普通json()会失败。
8. 下一步:把 Key 通道用到更多场景
分页横向图片浏览只是一个入口。当你把settings.json骨架和request封装搭好之后,同一套 Key 通道可以复用到很多地方:给图片批量生成 alt 文本、做图片内容审核、根据图片生成 SEO 描述,甚至把图片列表喂给模型做智能分组。
如果你主要在写代码,想把这个通道用在长期编码和 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/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。把上面describeImages里的 prompt 粘进去,看看返回格式,再决定前端怎么解析。
接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,里面有完整的接口路径和参数说明。Key 管理和生成在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。建议先把这三个页面过一遍,再回头调settings.json里的model字段,能少走不少弯路。
最后提醒一句:settings.json里的 Key 字段,上线前一定要通过服务端注入替换,别把明文提交到 Git 仓库。本地调试用debug.mock: true走假数据,也能避免频繁消耗额度。