资讯中心

代码展示组件设计:用 TaoToken 统一 Key 打通 Syntax Highlight 与 Copy 交互工程

📅 2026/9/26 17:32:30
代码展示组件设计:用 TaoToken 统一 Key 打通 Syntax Highlight 与 Copy 交互工程
1. 代码展示组件为什么需要统一 Key 通道做前端文档站或者技术博客的朋友大概率都遇到过这个场景页面上要展示一段 TypeScript 代码既要语法高亮好看又要能一键复制还得在暗色/亮色主题下都不刺眼。更麻烦的是如果这个页面背后还要调用大模型来生成示例代码或者做代码解释那 Key 的管理就成了一个绕不开的工程问题。我最近在重构一个内部文档站核心诉求有三个第一代码块用 Shiki 做语法高亮因为它的 TextMate 语法解析比正则方案准确得多第二封装一个 CodeBlock 组件把 Copy 交互做成三态状态机第三页面里嵌入的 AI 代码解释功能通过 TaoToken 统一 Key 来管理多模型调用避免每个组件各自维护一套 API Key。TaoToken 在这里扮演的角色是统一 API 通道。你可以把它理解成一个 Key 的集中管理处前端组件不需要知道具体调的是哪个模型只需要向同一个 API 端点发请求由 TaoToken 侧完成模型路由和 Key 的鉴权。这样代码展示组件在需要「解释这段代码」或者「生成示例」时调用链路是干净的。适合谁看正在做技术文档站、组件库文档、或者任何需要展示代码并附带 AI 能力的前端工程师。如果你只用过 Highlight.js 没碰过 Shiki或者 Copy 按钮还在用document.execCommand裸写这篇可以跟着走一遍。2. TaoToken 前置Key 申请与 API 通道配置在写组件之前先把 Key 的事情搞定。TaoToken 的官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后进入控制台创建 API Key。具体路径登录后找到 API Keys 管理页点「创建新 Key」复制生成的sk-开头的字符串。这个 Key 就是后续所有模型调用的凭证。TaoToken 的 API 端点是 https://taotoken.net/api 注意这个地址不带 UTM 参数是纯粹的接口地址。你的前端代码里请求模型时base URL 填这个路径按 OpenAI 兼容格式拼/v1/chat/completions即可。关于模型选择TaoToken 支持多种模型路由。在代码展示组件这个场景里我建议用轻量级模型做代码解释因为文档站的 AI 功能通常是辅助性的不需要顶级推理能力。你可以在控制台的模型列表里选一个响应快的把模型名称记下来后面配置里要用。Key 的安全管理有个基本原则前端代码里绝对不能硬编码 Key。正确做法是通过环境变量注入Next.js 项目里放在.env.local# .env.local TAOTOKEN_API_KEYsk-your-key-here TAOTOKEN_BASE_URLhttps://taotoken.net/api然后在服务端路由或者 Server Action 里读取process.env.TAOTOKEN_API_KEY。如果你用的是纯静态站点那就需要搭一个轻量后端做代理Key 只存在服务端。注意TaoToken 的 Key 权限可以在控制台里限制建议只开需要的模型权限不要用全权限 Key 跑前端请求。3. 可复制配置Shiki 高亮 CodeBlock 组件3.1 Shiki 初始化配置Shiki 的核心优势是直接复用 VS Code 的语法定义。安装npm install shiki服务端渲染的初始化代码// lib/shiki.ts import { createHighlighter, type Highlighter } from shiki; let highlighter: Highlighter | null null; export async function getHighlighter() { if (!highlighter) { highlighter await createHighlighter({ themes: [dark-plus, light-plus], langs: [typescript, javascript, tsx, jsx, bash, json, python], }); } return highlighter; } export async function highlightCode(code: string, lang: string, theme: string) { const hl await getHighlighter(); return hl.codeToHtml(code, { lang, theme }); }这里只加载了实际用到的语言和主题避免 bundle 膨胀。Shiki v1 之后支持按需加载createHighlighter是异步的适合在服务端组件里调用。3.2 Copy 交互的 Hook 封装Copy 按钮的交互逻辑抽成独立 Hook方便复用// hooks/useClipboard.ts import { useState, useCallback } from react; export function useClipboard({ timeout 2000 }: { timeout?: number } {}) { const [isCopied, setIsCopied] useState(false); const copy useCallback(async (text: string) { try { if (navigator.clipboard window.isSecureContext) { await navigator.clipboard.writeText(text); } else { const textarea document.createElement(textarea); textarea.value text; textarea.style.position fixed; textarea.style.opacity 0; document.body.appendChild(textarea); textarea.select(); const ok document.execCommand(copy); document.body.removeChild(textarea); if (!ok) throw new Error(execCommand failed); } setIsCopied(true); setTimeout(() setIsCopied(false), timeout); } catch (err) { console.error(Copy failed:, err); } }, [timeout]); return { isCopied, copy }; }关键点window.isSecureContext判断当前是否 HTTPS 或 localhost非安全上下文下 Clipboard API 会抛异常所以要有execCommand降级。isCopied为 true 期间按钮 disabled防止连点导致状态混乱。3.3 CodeBlock 组件完整封装// components/CodeBlock.tsx use client; import { useState, useEffect } from react; import { Check, Clipboard } from lucide-react; import { useClipboard } from /hooks/useClipboard; interface CodeBlockProps { code: string; language: string; highlightedHtml: string; filename?: string; showLineNumbers?: boolean; } export function CodeBlock({ code, language, highlightedHtml, filename, showLineNumbers true, }: CodeBlockProps) { const { isCopied, copy } useClipboard({ timeout: 2000 }); const [lines, setLines] useStatestring[]([]); useEffect(() { setLines(code.split(\n)); }, [code]); return ( div classNamegroup relative my-6 rounded-xl border border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-950 overflow-hidden div classNameflex items-center justify-between px-4 py-3 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 div classNameflex items-center gap-3 {filename ( span classNametext-sm font-medium text-slate-700 dark:text-slate-300 {filename} /span )} span classNametext-xs font-mono uppercase tracking-wider text-slate-500 {language} /span /div button onClick{() copy(code)} disabled{isCopied} aria-label{isCopied ? Copied : Copy code} classNameflex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium transition-all text-slate-500 hover:text-slate-700 hover:bg-slate-100 dark:text-slate-400 dark:hover:text-slate-200 dark:hover:bg-slate-800 disabled:text-emerald-600 dark:disabled:text-emerald-400 focus:outline-none focus:ring-2 focus:ring-blue-500/50 {isCopied ? ( Check classNameh-3.5 w-3.5 /spanCopied!/span/ ) : ( Clipboard classNameh-3.5 w-3.5 /span classNamehidden sm:inlineCopy/span/ )} /button /div div classNamerelative flex overflow-x-auto {showLineNumbers ( div classNameselect-none border-r border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-950 py-5 pr-4 pl-4 text-right min-w-[3rem] {lines.map((_, i) ( div key{i} classNametext-xs leading-6 text-slate-400 font-mono {i 1} /div ))} /div )} pre tabIndex{0} roleregion aria-label{Code snippet in ${language}} classNameflex-1 py-5 px-6 outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500/30 code classNametext-sm leading-6 font-mono dangerouslySetInnerHTML{{ __html: highlightedHtml }} / /pre /div /div ); }3.4 服务端集成与 AI 解释入口在 Next.js App Router 的页面里服务端完成 Shiki 渲染把 HTML 字符串传给客户端组件// app/docs/page.tsx import { highlightCode } from /lib/shiki; import { CodeBlock } from /components/CodeBlock; export default async function DocsPage() { const code const agent new Agent({ model: gpt-4.1 });; const html await highlightCode(code, typescript, dark-plus); return ( CodeBlock code{code} languagetypescript highlightedHtml{html} filenameagent.ts showLineNumbers / ); }如果要在代码块旁边加一个「AI 解释」按钮调用 TaoToken 的 API 时走服务端路由// app/api/explain/route.ts import { NextResponse } from next/server; export async function POST(req: Request) { const { code } await req.json(); const res await fetch(${process.env.TAOTOKEN_BASE_URL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.TAOTOKEN_API_KEY}, }, body: JSON.stringify({ model: gpt-4.1-mini, messages: [ { role: system, content: 用中文简要解释这段代码的功能。 }, { role: user, content: code }, ], }), }); const data await res.json(); return NextResponse.json({ explanation: data.choices[0].message.content }); }这样 Key 只存在服务端环境变量里前端组件通过/api/explain调用TaoToken 统一管理模型路由和鉴权。4. 验证请求与成功结果配置写完后跑一次完整验证。启动开发服务器npm run dev打开文档页你应该看到代码块渲染出 TypeScript 语法高亮关键字是蓝色、字符串是橙色、注释是绿色。点击 Copy 按钮按钮文案变成「Copied!」并显示对勾图标2 秒后恢复。验证复制是否真的成功打开浏览器控制台粘贴剪贴板内容应该和代码块里的文本完全一致包括缩进和换行。验证 TaoToken 通道在页面里触发一次 AI 解释请求观察 Network 面板。请求发往/api/explain服务端再转发到https://taotoken.net/api/v1/chat/completions返回 200 且choices[0].message.content有内容。如果返回 401说明 Key 没读到返回 404检查 base URL 拼接是否正确。一个实测下来比较稳的检查清单检查项预期结果常见偏差Shiki 高亮关键字/字符串/注释颜色区分语言未加载导致纯文本Copy 按钮点击后 2 秒内显示 Copied!非 HTTPS 下 Clipboard 报错行号对齐行号与代码行一一对应代码末尾空行导致行号多一TaoToken 请求200 有效响应体Key 未注入或模型名错误主题切换暗色/亮色下高亮均清晰硬编码色值导致亮色下看不清5. 本篇常见错排查Shiki 报错Language xxx not found原因是你用了createHighlighter但没在langs数组里注册该语言。Shiki 不会自动加载所有语言必须显式声明。解决在lib/shiki.ts的langs里加上对应语言标识比如vue、go。Copy 按钮在 HTTP 环境下失效navigator.clipboard在非安全上下文HTTP 且非 localhost下是undefined。代码里已经做了window.isSecureContext判断和execCommand降级但如果降级也失败检查textarea是否被正确添加到 DOM 并执行了select()。有些浏览器要求textarea可见才能复制可以把opacity设为0而不是display: none。行号与代码行错位常见原因是code.split(\n)时末尾多了一个空字符串。如果代码以换行结尾split会产生一个空元素导致行号多一行。解决code.replace(/\n$/, ).split(\n)。TaoToken 返回 401 Unauthorized检查.env.local里的TAOTOKEN_API_KEY是否以sk-开头以及服务端路由是否真的读到了这个变量。Next.js 里只有NEXT_PUBLIC_前缀的变量才会暴露给客户端服务端路由读process.env.TAOTOKEN_API_KEY没问题但如果你在客户端组件里直接读就会是undefined。高亮 HTML 被转义显示成文本用了dangerouslySetInnerHTML但 Shiki 返回的 HTML 里span被当成文本渲染了。检查是不是在传给组件之前又做了一次escape。Shiki 的codeToHtml返回的就是可直接插入的 HTML 字符串不要再转义。主题切换后高亮颜色不变如果你用的是固定theme: dark-plus切换data-theme属性不会影响已渲染的 HTML。解决方案有两种一是用 Shiki 的css-variables主题通过 CSS 变量控制颜色二是服务端根据当前主题分别渲染两套 HTML客户端切换时切换显示。6. 统一 Key 通道的后续接入代码展示组件跑通之后TaoToken 的 Key 通道可以复用到其他需要模型调用的地方。比如文档站的搜索框加一个「AI 问答」或者代码块旁边加「生成单元测试」按钮都走同一个/api/explain路由只是 prompt 不同。如果你打算长期在项目里做编码相关的 AI 功能可以看看 Coding Plan 的接入方式它针对代码场景做了优化。模型对话的调试入口在模型对话页可以快速验证 Key 和模型是否通。API Keys 的管理在控制台接入文档里有完整的参数说明。实际落地时建议把 TaoToken 的调用封装成一个统一的lib/ai.ts所有需要模型能力的地方都从这里走Key 只在一处配置模型切换也只改一个地方。这样代码展示组件就真正做到了「展示归展示AI 能力归通道」两边解耦维护成本低。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案