文章目录
-
- 每日一句正能量
- 前言
- 一、代码块的视觉设计体系
- [二、语法高亮引擎选型:Shiki 的工程优势](#二、语法高亮引擎选型:Shiki 的工程优势)
- [三、Copy 按钮的交互闭环设计](#三、Copy 按钮的交互闭环设计)
- 四、响应式与键盘可访问性
- [五、暗色/亮色主题的 Token 映射](#五、暗色/亮色主题的 Token 映射)
-
- [Token 映射策略](#Token 映射策略)
- [六、完整的 `<CodeBlock>` 组件封装](#六、完整的
<CodeBlock>组件封装) - 结语

每日一句正能量
回不去的何止是时间,还有曾经的自己。
时间不可逆是物理规律,而自我的变迁更隐秘深刻。承认这种不可回溯,不是伤感,而是对成长轨迹的尊重------每一个昨天的"我",都完成了ta当时的使命。
前言
在技术类产品的官网中,代码块不仅是内容的载体,更是品牌专业度的视觉宣言。一个精心设计的代码展示组件,需要在语法准确性、复制易用性、主题一致性和性能开销之间找到精妙的平衡点。本文将以 Codex 官网的代码块实现为蓝本,拆解从语法高亮引擎选型到复制交互闭环的完整工程方案。
一、代码块的视觉设计体系
Codex 官网中的代码块遵循了现代开发者工具的设计语言,其视觉特征可拆解为五个维度:
圆角与容器 :采用 rounded-xl(12px)的圆角处理,与页面中卡片组件的圆角体系保持一致,避免视觉碎片的割裂感。容器边框使用 1px solid 的细线分割,暗色主题下为 slate-800,亮色主题下为 slate-200,确保代码块从页面背景中"浮起"而非"陷入"。
内边距逻辑 :遵循"代码优先"的留白原则------水平方向 px-6(24px)保证行首不贴边,垂直方向 py-5(20px)为行号与代码主体留出呼吸空间。顶部额外预留 pt-12 的空间,用于放置语言标签和 Copy 按钮,避免操作控件与代码文本重叠。
行号系统 :左侧固定宽度的行号栏(w-12)使用 text-slate-500 的弱化色值,与主代码形成明确的层级对比。行号栏与代码区之间以 border-r 分隔,且行号文本右对齐(text-right pr-4),符合程序员阅读源码的肌肉记忆。
滚动条样式 :通过自定义 CSS 覆盖浏览器默认滚动条,暗色主题下滚动条轨道为透明,滑块为 slate-700,Hover 时提亮至 slate-600。移动端保持原生滚动条以避免触控手势冲突。
语言标签 :在代码块左上角放置语言标识(如 bash、python、tsx),使用 text-xs font-mono uppercase tracking-wider 的微型标签样式,颜色与行号一致,既不喧宾夺主,又能在扫描时快速定位语言类型。
二、语法高亮引擎选型:Shiki 的工程优势
在 Prism.js、Highlight.js 与 Shiki 的选型三角中,Codex 官网选择 Shiki 作为语法高亮引擎,其决策逻辑基于以下技术维度:
TextMate Grammar 的精确性:Shiki 直接复用 VS Code 的语法解析引擎,支持超过 170 种语言的精准高亮。与 Prism 基于正则的解析相比,TextMate Grammar 能够正确处理嵌套模板字符串、JSX 属性展开、TypeScript 泛型约束等复杂语法结构,避免"半个文件高亮错乱"的尴尬。
主题一致性:Shiki 可以直接加载 VS Code 主题 JSON 文件,这意味着 Codex 的代码块高亮颜色与官方推荐的编辑器主题(如 Dark+、Light+)保持 100% 一致。对于面向开发者的产品而言,这种"官网即 IDE"的视觉连续性是一种隐性的品牌信任构建。
SSR 原生支持 :Shiki 支持在 Node.js 服务端完成高亮渲染,输出静态 HTML 字符串。这意味着首屏代码块在到达浏览器时已经是带 <span class="token..."> 的纯 HTML,无需客户端 JavaScript 二次解析,彻底消除了语法高亮对 LCP(Largest Contentful Paint)的影响。
树摇优化(Tree-shaking):Shiki v1.0+ 支持按需加载语言和主题,构建时仅打包实际使用的语法定义,避免全量引入导致的 Bundle 膨胀。
三、Copy 按钮的交互闭环设计
复制功能是代码块中使用频率最高的交互,其体验细节直接决定用户满意度。
状态机设计
代码块的复制交互遵循一个三态状态机:
- Idle(闲置态) :Copy 按钮显示剪贴板图标(
Clipboard),透明度在桌面端为 0(opacity-0),仅在代码块 Hover 时显现(group-hover:opacity-100),保持界面简洁。 - Success(成功态) :点击后图标切换为对勾(
Check),文案变为「Copied!」,颜色切换为品牌绿(text-emerald-400),持续 2 秒后自动恢复 Idle 态。 - Error(错误态) :当 Clipboard API 不可用时(如非安全上下文 HTTP 环境),降级使用
document.execCommand('copy');若两者均失败,显示「Failed to copy」提示,并提供手动选中的引导。
实现细节
tsx
// hooks/useClipboard.ts
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 {
// 降级方案:execCommand
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
const success = document.execCommand('copy');
document.body.removeChild(textarea);
if (!success) throw new Error('execCommand failed');
}
setIsCopied(true);
setTimeout(() => setIsCopied(false), timeout);
} catch (err) {
console.error('Copy failed:', err);
// 可扩展为 Toast 提示
}
}, [timeout]);
return { isCopied, copy };
}
关键工程决策:
- 安全上下文检测 :
window.isSecureContext用于判断当前是否处于 HTTPS 或 localhost,避免在 HTTP 生产环境中调用 Clipboard API 抛出异常。 - 临时 DOM 降级 :
execCommand方案需要创建一个不可见的<textarea>并执行选中和复制命令,完成后立即清理 DOM,避免内存泄漏。 - 防抖保护 :在
isCopied为true的 2 秒内,禁用按钮点击,防止用户快速连击导致的动画状态混乱。
四、响应式与键盘可访问性
代码块在移动端的体验往往被忽视,但 Codex 作为开发者工具,其用户可能在平板或手机端查阅文档。
横向滚动处理 :移动端代码块容器设置 overflow-x-auto,而非简单的 overflow-hidden 或强制换行。配合 -webkit-overflow-scrolling: touch 确保 iOS 上的惯性滚动流畅。代码文本保持 whitespace-pre,尊重原始缩进和换行。
键盘可访问性 :代码块容器添加 tabindex="0" 和 role="region",使键盘用户可以通过 Tab 键聚焦到代码块,随后使用方向键进行横向/纵向滚动。配合 aria-label={Code snippet in ${language}},为屏幕阅读器用户提供上下文。
tsx
<pre
ref={preRef}
tabIndex={0}
role="region"
aria-label={`Code snippet in ${language}`}
className="overflow-x-auto focus:outline-none focus:ring-2 focus:ring-blue-500/50 rounded-xl"
>
<code>{highlightedCode}</code>
</pre>
触控设备优化 :通过 @media (hover: none) 检测触控设备,此时 Copy 按钮始终可见(opacity-100),且热区扩展至 44×44px,符合 WCAG 2.5.5 的触控目标最小尺寸要求。
五、暗色/亮色主题的 Token 映射
Shiki 的主题系统通过 Token 类型映射到具体的色值。Codex 官网需要确保在暗色与亮色模式下,代码高亮不仅"能看清",更要"有层次"。
Token 映射策略
以 TypeScript 代码为例,核心 Token 的映射逻辑如下:
| Token 类型 | 暗色主题 (Dark+) | 亮色主题 (Light+) | 语义 |
|---|---|---|---|
keyword |
#569CD6 (蓝) |
#0000FF (蓝) |
保留字 (const, return) |
string |
#CE9178 (橙) |
#A31515 (红) |
字符串字面量 |
function |
#DCDCAA (黄) |
#795E26 (棕) |
函数名与调用 |
number |
#B5CEA8 (浅绿) |
#098658 (绿) |
数字常量 |
comment |
#6A9955 (绿) |
#008000 (绿) |
注释 |
type |
#4EC9B0 (青) |
#267F99 (青) |
类型注解 |
variable |
#9CDCFE (浅蓝) |
#001080 (深蓝) |
变量标识符 |
主题切换的实现 :通过 CSS 变量或 data-theme 属性驱动 Shiki 的 css-variables 渲染器,避免在主题切换时重新执行高亮计算:
tsx
// 使用 Shiki 的 css-variables 渲染器
const highlighted = await shiki.codeToHtml(code, {
lang: language,
theme: 'css-variables', // 使用 CSS 变量而非硬编码色值
});
css
/* 暗色主题 */
[data-theme='dark'] {
--shiki-token-keyword: #569CD6;
--shiki-token-string: #CE9178;
--shiki-token-function: #DCDCAA;
/* ... */
}
/* 亮色主题 */
[data-theme='light'] {
--shiki-token-keyword: #0000FF;
--shiki-token-string: #A31515;
--shiki-token-function: #795E26;
/* ... */
}
这种方式的优势在于:主题切换时仅需切换父级的 data-theme 属性,Shiki 生成的 HTML 结构无需重建,实现毫秒级的主题切换。
六、完整的 <CodeBlock> 组件封装
tsx
// components/CodeBlock.tsx
'use client';
import { useState, useEffect, useCallback } from 'react';
import { Check, Clipboard } from 'lucide-react';
import { useClipboard } from '@/hooks/useClipboard';
interface CodeBlockProps {
code: string;
language: string;
highlightedHtml: string; // 由服务端 Shiki 渲染传入
showLineNumbers?: boolean;
filename?: string;
}
export function CodeBlock({
code,
language,
highlightedHtml,
showLineNumbers = true,
filename,
}: CodeBlockProps) {
const { isCopied, copy } = useClipboard({ timeout: 2000 });
const [lines, setLines] = useState<string[]>([]);
useEffect(() => {
setLines(code.split('\n'));
}, [code]);
return (
<div className="group relative my-6 rounded-xl border border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-950 overflow-hidden">
{/* 顶部栏:文件名 + 语言标签 + Copy 按钮 */}
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
<div className="flex items-center gap-3">
{filename && (
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">
{filename}
</span>
)}
<span className="text-xs font-mono uppercase tracking-wider text-slate-500">
{language}
</span>
</div>
<button
onClick={() => copy(code)}
disabled={isCopied}
className="flex 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"
aria-label={isCopied ? 'Copied to clipboard' : 'Copy code to clipboard'}
>
{isCopied ? (
<>
<Check className="h-3.5 w-3.5" />
<span>Copied!</span>
</>
) : (
<>
<Clipboard className="h-3.5 w-3.5" />
<span className="hidden sm:inline">Copy</span>
</>
)}
</button>
</div>
{/* 代码区域 */}
<div className="relative flex overflow-x-auto">
{/* 行号 */}
{showLineNumbers && (
<div className="select-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}
className="text-xs leading-6 text-slate-400 font-mono"
>
{i + 1}
</div>
))}
</div>
)}
{/* 高亮代码 */}
<pre
tabIndex={0}
role="region"
aria-label={`Code snippet in ${language}`}
className="flex-1 py-5 px-6 outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500/30"
>
<code
className="text-sm leading-6 font-mono"
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
/>
</pre>
</div>
</div>
);
}
服务端集成(Next.js App Router):
tsx
// app/docs/page.tsx
import * as shiki from 'shiki';
import { CodeBlock } from '@/components/CodeBlock';
export default async function DocsPage() {
const code = `const codex = new CodexAgent({
model: 'gpt-4.1',
instructions: 'Build a React component',
});`;
const highlighted = await shiki.codeToHtml(code, {
lang: 'typescript',
theme: 'dark-plus',
});
return (
<CodeBlock
code={code}
language="typescript"
highlightedHtml={highlighted}
filename="agent.ts"
showLineNumbers
/>
);
}
结语
代码展示组件的设计,是技术产品官网中"魔鬼藏在细节里"的典型场景。从 Shiki 的 TextMate 精确解析到 Copy 按钮的三态状态机,从移动端的横向滚动到主题切换的 Token 映射,每一个决策都在服务于同一个目标:让开发者在阅读代码时感受到"这不是一个网页,这是一个为我准备的开发环境"。Codex 官网通过这套工程方案,将代码块从静态内容提升为交互式的开发者体验触点------而这,正是技术品牌专业度的最佳注脚。
转载自:https://blog.csdn.net/sghtgjfhv/article/details/164045015
欢迎 👍点赞✍评论⭐收藏,欢迎指正