- 前端
- UI组件
【免费下载链接】code-surfer
Rad code slides <🏄/>
@code-surfer/standalone是 Code Surfer 项目中负责"渲染代码幻灯片"的核心 React 组件包,它在内部被code-surfer主包(MDX Deck 集成层)所调用,同时也可以被开发者直接独立使用。本文以 packs/standalone/readme.md 为主体,结合 packs/standalone/src 的完整源码、packs/step-parser 的步骤解析器以及 sites/book 的 Storybook 示例,系统讲解该组件的定位、API 数据结构、渲染与动画实现原理,并给出可复制的本地开发与测试工作流。
一、包定位:内部组件,但可以独立使用
@code-surfer/standalone的 readme.md 开头就明确了它的定位:
For internal use by code-surfer, but you can use it if you want.
也就是说,这是一个"为 Code Surfer 主包服务"的底层渲染组件,但包本身是公开发布的(package.json 中"private": false、"license": "MIT"、"main": "dist/index.js"、"module": "dist/standalone.esm.js"),你可以把它当作一个独立的 React 组件直接引入到自己的幻灯片或演示应用中。
最重要的使用警告:不要依赖语义化版本
文档明确强调了一个关键约束:
it doesn't follow semantic versioning, so pin the version just in case.
@code-surfer/standalone不遵循语义化版本(SemVer)规范。这意味着在小版本号之内,其 API 与行为都可能发生变化,因此在 package.json 中务必将版本号锁定为精确版本(不要使用^3.1.1或~3.1.1这类浮动区间),例如:
{ "dependencies": { "@code-surfer/standalone": "3.1.1" } }当前仓库中 packs/standalone/package.json 的版本为3.1.1,仓库内的 sites/book/package.json 也正是以精确版本"3.1.1"引用它的,这正是"锁定版本"的官方实践范例。
依赖与运行环境
从 packs/standalone/package.json 可以看到它的技术栈与依赖:
- peerDependencies:
react: ^16.8.0(需要支持 Hooks 的 React 版本) - 核心依赖:
@code-surfer/step-parser(步骤解析)、@code-surfer/themes(主题系统)、prismjs(语法高亮)、diff(行级差异)、rebound、shell-quote、array.prototype.flat - engines:
node >= 8、npm >= 5 - 构建工具:
tsdx(TypeScript 包开发工具链),提供tsdx watch与tsdx build
二、本地开发工作流:构建、Storybook 与测试
readme 的 "Contributing" 一节给出了三条可直接执行的命令,它们都基于 Yarn Workspaces 的 monorepo 结构(见根目录 package.json 中的"workspaces"配置)。
1. 安装依赖并开启 watch 构建
$ yarn $ yarn workspace @code-surfer/standalone start第一条命令在仓库根目录安装所有 workspace 的依赖;第二条命令进入@code-surfer/standalone包执行start脚本。从 packs/standalone/package.json 的 scripts 可见,start实际执行的是tsdx watch,它会监听src/下的 TypeScript 源码变更并持续输出dist/构建产物,适合在改代码的同时配合 Storybook 实时预览。
2. 运行 Storybook(book 站点)
$ yarn workspace book startbook是仓库中的 Storybook 站点(sites/book),其start脚本为start-storybook -c ./src(见 sites/book/package.json)。它聚合了 sites/book/src/index.js 通过require.context(".", true, /\.story\.js$/, "sync")自动发现的所有*.story.js文件,包括basic.story.js、big.story.js、focus.story.js、themed.story.js、title.story.js、parsed-steps.story.js等。readme 明确指出 "No docs, but you can check the code insites/book/",也就是说 Storybook 中的示例代码就是这份包最权威的"文档"。
3. 监听测试
$ yarn workspace @code-surfer/standalone test:watchtest:watch脚本为tsdx test --env=jsdom -u --watch(见 packs/standalone/package.json),使用 jsdom 环境运行 Jest 测试并持续监听。当前包内已有 animation.test.ts(动画插值/链式/stagger 数值断言)与 tuple.test.ts(Tuple 数据结构行为测试)两类测试,配合snapshots/animation.test.ts.snap 快照使用。
根目录 package.json 还提供了聚合命令:
yarn build:standalone构建本包、yarn test:standalone运行本包测试、yarn test并行运行全部四个包的格式检查与测试。
三、组件 API:CodeSurfer 的 Props 与数据结构
@code-surfer/standalone对外导出的核心是CodeSurfer组件(以及UnknownError错误组件),导出语句位于 packs/standalone/src/index.tsx:
export { CodeSurferWrapper as CodeSurfer, UnknownError };从 packs/standalone/src/index.tsx 可以看到完整的 Props 类型定义:
type CodeSurferProps = { steps?: InputStep[]; // 原始输入步骤(含 code/focus/title/subtitle/lang/showNumbers) parsedSteps?: ParsedSteps; // 预解析好的步骤(跳过解析,直接渲染) progress: number; // 浮点进度,取值区间 [0, steps.length - 1] theme?: CodeSurferTheme; // 主题对象,来自 @code-surfer/themes nonblocking?: boolean; // 是否先用占位符渲染再异步切换 };progress:驱动一切的"进度旋钮"
progress是组件唯一必传的 prop,它是一个浮点数,范围是[0, steps.length - 1](见 packs/standalone/src/code-surfer.tsx 的注释)。例如有 3 个步骤时:
progress = 0:完整显示第 0 步progress = 1:完整显示第 1 步progress = 1.5:处于第 1 步到第 2 步的动画过渡中(t = progress % 1,见 frame.tsx)
在 sites/book/src/utils.js 的StoryWithSlider中可以看到典型用法:滑块值经过use-spring弹性动画库平滑后传入progress,从而驱动组件内部的缩放、滚动与淡入淡出动画。
InputStep 与 Step:输入与内部表示
types.d.ts 声明了两种数据结构:
InputStep(用户输入):
export interface InputStep { code: string; // 代码文本 focus?: string; // focus 字符串,如 "1:2,3[8:10]" title?: string; // 标题 subtitle?: string; // 副标题 lang?: string; // 代码语言(Prism 语法名) showNumbers?: boolean; // 是否显示行号 }Step(解析后的内部表示):
export interface Step { lines: LineKey[]; // 该步骤涉及的行(按行 key 引用) longestLineIndex: LineIndex; // 最长行的索引(用于测量宽度) focus: Record<LineIndex, true | StepIndex[]>; // 每行的聚焦状态;数组表示按列聚焦 focusCenter: number; // 聚焦区域中心(行单位),用于滚动与缩放动画 focusCount: number; // 聚焦区域行数,用于缩放比例计算 title?: string; subtitle?: string; dimensions?: { paddingTop: number; paddingBottom: number }; // 运行时测量 }focus的值要么是true(整行聚焦),要么是一个列索引数组(仅聚焦该行的指定列,这是列级高亮的基础,例如 focus 字符串3[8:10])。
steps 与 parsedSteps:两种输入模式
steps模式:传入原始InputStep[],组件内部调用parseSteps解析。见 index.tsx:
const { steps, tokens, types, maxLineCount, showNumbers } = React.useMemo(() => { if (parsedSteps) return parsedSteps; return parseSteps(inputSteps!); }, [inputSteps, parsedSteps]);parsedSteps模式:传入已经通过@code-surfer/step-parser的parseSteps解析好的结构化数据(steps、tokens、types、maxLineCount、showNumbers)。这种模式完全跳过运行时解析,适合"预计算 + 离线渲染"的场景。sites/book/src/parsed-steps.story.js 的 "50 Steps Parsed" 示例正是用这种模式承载 50 步、300 行代码量级的大数据(见 sites/book/src/parsed-steps.js 中{"maxLineCount":300,...}的预解析 JSON)。
两种模式可任选其一:传了parsedSteps就以它为准,否则回退到解析steps。
nonblocking 与 theme:渲染策略与主题注入
CodeSurferWrapper(见 index.tsx)处理了两个细节:
nonblocking:为true时,首次渲染先输出一个Styled.Placeholder占位符(继承主题的pre背景色,见 packs/themes/src/styles.tsx),随后在useEffect中立即切换到真实渲染。这可以避免在首屏主线程忙碌时阻塞交互。theme:组件被StylesProvider包裹(来自@code-surfer/themes),主题通过 Theme UI 的ThemeProvider层层合并(外层 deck 主题 + 传入主题,见 styles.tsx)。
另外,当steps为空时组件会直接抛出Error("No steps")(index.tsx),调用方需要保证至少提供一个步骤。
四、渲染管线:从解析到像素
CodeSurfer组件的渲染分为两条路径(packs/standalone/src/code-surfer.tsx):
路径一:未测量尺寸时的"假步骤"预渲染
组件先通过useDimensions测量容器的行高、宽度等真实尺寸。在测量完成之前,它会渲染一组getFakeSteps生成的假步骤(code-surfer.tsx):把所有行的行 key 替换为代码中最短行的 key,并用最长行占位,目的是以最小的布局代价把容器撑起来、拿到真实尺寸。每一个假步骤都对应渲染一个Frame,从而让隐藏容器内部的 DOM 与最终结构一致,保证测量结果准确。
路径二:测量完成后的正式渲染
拿到dimensions与带dimensions信息的步骤后,渲染单个Frame(frame.tsx)。Frame内部的渲染层级为:
CodeSurferContainer (.cs-container) ├── CodeSurferContent (.cs-content / <pre>) │ └── .cs-scaled-content (<code>) │ ├── 上半部占位(containerHeight/2,用于滚动对齐) │ ├── LineList(逐行渲染) │ └── 下半部占位 ├── Title (.cs-title) —— 顶部居中标题 └── Subtitle (.cs-subtitle) —— 底部副标题核心机制(frame.tsx):
- 用
Math.floor(progress)取出当前步与下一步组成Tuple(tuple.ts 中定义的二元组数据结构,专门用于表达"前后两帧"的成对状态); - 通过
scaleToFocus与scrollToFocus两个动画函数分别计算缩放比例与滚动偏移,并以transform: scale(...)和ref.current.scrollTop = scrollTop(useLayoutEffect同步写入)应用到代码区; - 代码区上下各放
containerHeight / 2高的占位 div,配合transformOrigin: center verticalOrigin实现"以聚焦行为中心缩放"的效果; Title/Subtitle使用fadeOutIn/halfFadeIn/halfFadeOut在前后文本之间过渡,标题与副标题的切换阈值是t < 0.5。
行级渲染:LineList 的三种状态机
lines.tsx 是逐行渲染的核心。对每一行,它根据前后两步的状态将行划分为三类:
- 静态行(isStatic):前后步都存在且聚焦状态不变,直接渲染,聚焦行全透明、未聚焦行使用
unfocusedStyle.opacity(主题默认0.3,见 styles.tsx); - 进入/退出行(enterLine / exitLine):新增的行从右侧滑入并撑开高度,删除的行先向左滑出再收缩高度(
distx = 250,见 animation.ts); - 聚焦切换行(fadeInFocus / fadeOutFocus):聚焦状态变化的行做整体透明度渐变。
行内 token 的渲染由tokens[lineKey]与types[lineKey]驱动,每个 token 的 class 为"token-" + type(见 lines.tsx)。当某行的focus是列数组时(列级聚焦),areTokensAnimated为 true,则逐 token 计算各自透明度,实现"同一行内部分列高亮、其余列淡出"的效果。
行号显示由showNumbers控制,行号用空格左填充对齐(pad = maxLineCount.toString().replace(/./g, " ")),class 为token-line-number(lines.tsx)。
五、动画引擎:tween、chain 与 stagger
所有过渡动画都由 animation.ts 提供,它是组件的"动画数学库"。
关键概念
tween(from, to, t, ease):线性插值,ease默认linear(animation.ts);chain(steps, start):把一段动画拆成多个关键阶段。动画时间轴被划分为EXIT = 0.25、SCROLL = 0.75、ENTER = 1三段:前 25% 处理"旧内容退出",中间 50% 处理"滚动/缩放/高度变化",最后 25% 处理"新内容进入"(animation.ts)。chain逐段计算插值进度并在首个未完成阶段返回(animation.ts);stagger(animation, index, length, interval = 0.3):让多条线/多个 token 的动画错峰开始,interval控制总错峰跨度,从而产生"依次浮现"的节奏感(animation.ts)。
缩放与滚动计算
scaleToFocus:通过getZoom计算缩放比——纵向取"可用高度(容器高度减去两倍 max(paddingTop,paddingBottom))÷ 聚焦行数 × 行高",横向取0.9 × 容器宽度 ÷ 内容宽度,最终取Math.min(yZoom, 1, xZoom),即只缩小、不放大,保证聚焦内容完整可见(animation.ts);scrollToFocus:在t ∈ [0.25, 0.75]阶段用easeInOutCubic从上一聚焦中心滚到下一聚焦中心,中心位置为focusCenter × lineHeight(animation.ts)。
focusCenter/focusCount由 packs/step-parser/src/focus-parser.ts 计算:focusCenter = (min + max + 1) / 2,focusCount = max - min + 1。
缓动函数库
easing.ts 内置了 linear、quad/cubic/quart/quint 的 In/Out/InOut 共 10 种缓动函数。你可以从测试 animation.test.ts 看到这些数值被逐点断言:测试对t = 0, 0.05, ..., 1采样 21 个点,把tween/chain/stagger/fadeInFocus/exitLine/enterLine的输出表格化后与snapshots/animation.test.ts.snap 中的快照比对,确保动画曲线在版本迭代中不被意外改动。
六、主题系统:@code-surfer/themes 如何为组件供血
组件本身不关心颜色,所有样式都通过@code-surfer/themes注入。packs/themes/src/styles.tsx 定义了StylesProvider与Styled五个子组件:
| Styled 成员 | 对应 DOM | 说明 |
|---|---|---|
Styled.Pre | <pre> | 代码容器,会把主题tokens中的 token 类型映射为.token-xxx选择器 |
Styled.Code | <code> | 代码内容,应用styles.CodeSurfer.code |
Styled.Title | <h4> | 绝对定位的顶部标题 |
Styled.Subtitle | <p> | 绝对定位的底部副标题 |
Styled.Placeholder | <div> | nonblocking模式下的占位块 |
主题对象的结构(CodeSurferTheme,见 packs/themes/src/utils.ts)包含styles.CodeSurfer下的title、subtitle、code、pre、tokens(token 类型到样式的映射表)、unfocused(仅允许改未聚焦行的 opacity)等字段。makeTheme(utils.ts)可以从一个 Prism 风格主题自动生成 Code Surfer 主题。
useUnfocusedStyle()返回主题中unfocused.opacity(缺省0.3),这正是LineList中未聚焦行透明度的来源——也就是说,未聚焦代码的"淡出程度"完全由主题控制。
七、实战示例:从 Storybook 抄作业
basic.story.js 提供了最直观的最小示例,它展示了两种模式的完整用法:
import { CodeSurfer } from "@code-surfer/standalone"; const steps = [ { code: `var x1 = 1 debugger`, focus: "1", lang: "js" }, { code: `var x0 = 3 var x1 = 1 var x0 = 3`, lang: "js" } ]; // 通过 steps 传入原始输入 <CodeSurfer progress={progress} steps={steps} /> // 通过 parsedSteps 传入预解析结果(跳过运行时解析,性能更好) <CodeSurfer progress={progress} parsedSteps={parsedSteps} /> // 开启行号 <CodeSurfer progress={progress} steps={stepsWithNumbers} />parsedSteps的结构可以直接参考同一文件中的手写样例:steps(每步的lines/focus/focusCenter/focusCount/longestLineIndex)、tokens(每行的 token 文本数组)、types(每行的 token 类型数组)、maxLineCount。
focus 字符串语法(step-parser 支持)
focus字符串由 packs/step-parser/src/focus-parser.ts 解析,支持三种形式:
- 单行/范围:
"2"、"5:10"(5 到 10 行) - 多段:
"1,3:5,7"(第 1、3、4、5、7 行) - 行内列选择:
"2[5]"(第 2 行第 5 列)、"2[5:8]"(第 2 行第 5~8 列)、"1,2[1,3:5,7],3"(组合)
解析时行号和列号都从 1 开始计数(内部转为 0-based 索引),且不允许使用 0;列级聚焦的 token 会被splitIntoColumns拆成单字符数组,从而支持逐 token 的列级高亮动画(见 packs/step-parser/src/step-parser.ts)。
语言支持与语法高亮
组件依赖 Prism 做词法分析(step-parser/src/tokenizer.ts)。default-syntaxes.ts 默认导入了 26 种常用语言组件(js、jsx、ts、python、go、css、json、yaml、graphql、diff、git、wasm 等)。如果你要渲染的语言不在默认列表内,需要手动导入对应的 Prism 组件,否则 errors.tsx 会渲染grammarNotFound错误提示,并给出修复建议:
import "prismjs/components/prism-smalltalk"; // 示例:手动导入未内置语言错误处理
errors.tsx 提供了三类用户可见错误:
- UnknownError:通用错误("Oops, there's a problem"),由组件导出;
- grammarNotFound:语法高亮器缺失,提示从
prismjs/components/prism-{lang}导入; - invalidFocusNumber / invalidLineOrColumnNumber:focus 字符串非法(非自然数、或使用了 0 作为行/列号),错误头会指出是第几步出错(如 "3rd step")。
八、源码阅读地图:建议的探索顺序
如果你想深入这份包的实现,按下面的顺序阅读效率最高:
- packs/standalone/src/index.tsx —— 入口与 Props 聚合,看
steps/parsedSteps如何二选一; - packs/standalone/src/code-surfer.tsx —— 尺寸测量与假步骤预渲染策略;
- packs/standalone/src/frame.tsx —— 帧容器、缩放/滚动应用、标题副标题过渡;
- packs/standalone/src/lines.tsx —— 行状态机与 token 级动画;
- packs/standalone/src/animation.ts 与 easing.ts —— 动画数学;
- packs/standalone/src/tuple.ts 与 tuple.test.ts —— "前后两帧"成对数据结构;
- packs/step-parser/src/step-parser.ts 与 focus-parser.ts —— 步骤解析与 focus 语法;
- sites/book/src 下的各
*.story.js—— 功能与性能的活文档。
九、小结与注意事项
@code-surfer/standalone是 Code Surfer 的独立渲染内核,可单独使用,但不遵循语义化版本,必须锁定精确版本;- 它的核心输入是
progress(浮点进度)+steps/parsedSteps二选一,输出是包含缩放、滚动、淡入淡出、行增删、列级高亮等动画的代码幻灯片; - 本地开发流程为
yarn→yarn workspace @code-surfer/standalone start(watch 构建)→yarn workspace book start(Storybook 预览)→yarn workspace @code-surfer/standalone test:watch(测试监听); - 组件依赖 React 16.8+ Hooks、Prism 语法高亮与 Theme UI 主题系统;未内置的语言需手动导入 Prism 组件;
- 官方没有专门文档,sites/book 的 Storybook 示例是理解其 API 与效果的最佳参照物,配合本包源码可完整还原其渲染与动画原理。
- 前端
- UI组件
【免费下载链接】code-surfer
Rad code slides <🏄/>
相关推荐
@code-surfer/step-parser 源码解析:Code Surfer 代码幻灯片的步骤解析引擎
@code surfer/step parser 源码解析:Code Surfer 代码幻灯片的步骤解析引擎 @code surfer/step parser
前端UI组件Home Assistant性能调优实战指南:从瓶颈分析到系统优化
Home Assistant性能调优实战指南:从瓶颈分析到系统优化 在智能家居系统的日常运行中,Home Assistant的性能优化是确保系统稳定、响应迅速的
智能家居物联网CLIProxyAPI实战指南:构建企业级AI代理网关的5个关键步骤
CLIProxyAPI实战指南:构建企业级AI代理网关的5个关键步骤 CLIProxyAPI是一个强大的AI代理服务器,为开发者提供OpenAI、Gemini、
后端API网关大模型AI 应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考