用 Web 技术做一套数据结构微课:从知识点到可发布视频

本文记录《数据结构微课》项目的技术设计与制作流程。项目成片已发布到:

项目代码计划开源在 GitHub:https://github.com/haodehaode378/video-pipeline。目前项目仍处于开发和整理阶段,后续会逐步补充完整的源码、制作脚本、示例工程和使用说明。

1. 为什么用前端技术做微课视频

数据结构的难点不在于定义本身,而在于学生很难在静态文字里看清“状态如何变化”。例如顺序表插入时元素为什么要移动,链表插入时指针为什么只改两条边,二叉树遍历为什么会得到不同序列,DFS 和 BFS 为什么访问顺序不同。

因此,这套微课没有采用传统 PPT 录屏,而是把每一节课拆成可编程的动画场景。核心思路是:

知识点脚本 -> HTML 场景 -> CSS/JS 动画 -> 浏览器预览 -> 视频渲染 -> 分段配音 -> 音视频合成

前端技术的优势在于可控。每一个节点、数组格、指针、栈顶标记、队头队尾标记都可以被精确定位和高亮;每一个时间点都能通过 URL 参数复现,便于截图检查和局部修正。

2. 内容结构:6 集覆盖核心脉络

这套微课按“先建立整体地图,再讲典型结构和操作”的顺序组织,共 6 集:

集数 主题 核心目标
01 数据结构到底学什么 建立逻辑结构、存储结构、数据运算的总览
02 顺序表 vs 链表 对比连续存储和指针存储的访问、插入、删除差异
03 栈和队列 理解 LIFO、FIFO、top、front、rear 和循环队列
04 树与二叉树遍历 理解根、左右子树、先序、中序、后序、层次遍历
05 图:多对多关系与遍历 理解顶点、边、邻接矩阵、邻接表、DFS、BFS
06 查找与排序 理解顺序查找、二分查找、散列查找和常见排序思想

每一集都遵循固定节奏:

标题引入 -> 概念解释 -> 动画演示 -> 关键代码/公式 -> 一句话总结

这样做的好处是学生每次打开视频都能预期学习路径:先知道这一集解决什么问题,再看到结构如何变化,最后用一小段代码或公式把图形过程落到实现层。

3. 工程结构:每集都是一个可运行网页

项目将每一集放在独立目录中:

videos/
01-overview/
02-linear-list/
03-stack-queue/
04-binary-tree/
05-graph/
06-search-sort/

每集基本包含:

index.html      # 控制台预览入口
submit.html # 干净提交/渲染入口
style.css # 视觉系统和动画样式
script.js # 时间轴、场景切换、状态更新
assets/ # 分段旁白、音频等素材
output/ # 导出视频
snapshots/ # 关键帧截图

这个目录设计有一个重要原则:课程内容、动画逻辑、配音素材和导出结果放在同一个 episode 边界内。这样第 3 集调整栈和队列时,不会影响第 4 集二叉树;第 5 集重新配音时,也不需要改全局工程配置。

index.html 不是普通网页,而是一个固定比例的视频舞台。根节点会声明视频尺寸、总时长和 composition id:

<main
id="root"
class="stage"
data-composition-id="03-stack-queue"
data-width="1920"
data-height="1080"
data-duration="289.12"
>
</main>

这里的 data-widthdata-heightdata-duration 不是装饰字段,而是后续预览、截图和渲染的统一协议:

  • 浏览器预览时,脚本按 1920x1080 等比缩放舞台,避免不同屏幕尺寸导致布局变形。
  • 截图时,自动化工具读取同一个入口,通过 ?t=120 定位到第 120 秒。
  • 渲染时,导出脚本可以按 composition id 和 duration 推断视频范围。

每个场景用 section.scene 表示,并通过 data-startdata-duration 标注时间范围:

<section class="scene scene-stack-push bg-yellow" data-start="78.90" data-duration="22.86">
<h2>进栈:先移动 top,再放入元素</h2>
...
</section>

这种写法相当于把一条视频拆成多个“时间片”。每个时间片内部只关心自己的教学对象,例如栈场景只处理 top 指针和栈元素,队列场景只处理 frontrear 和数组格。相比把所有动画写在一个长函数里,这种结构更容易定位问题:某个画面错了,先看它属于哪个 section.scene,再看对应的局部时间计算。

submit.html 则承担“干净渲染入口”的角色。开发阶段需要控制台、滑块、时间码,方便检查动画;正式提交或录制时,这些调试组件会干扰画面。因此项目保留两个入口:

入口 用途 特点
index.html 开发预览 显示控制台、时间码、调试滑块
submit.html 截图/渲染/提交 隐藏调试 UI,只保留视频画面

这样可以避免一个常见问题:为了临时录制把调试控件删掉,录完又要恢复。入口分离后,开发态和成片态各自稳定。

4. 时间轴设计:把视频变成可 seek 的状态机

微课动画最关键的不是“自动播放”,而是“任意时刻都能稳定复现画面”。项目里的 script.js 会暴露类似 window.__hfSeek(time) 的能力,用于把画面跳转到指定秒数。

这种设计解决了三个问题:

  1. 预览时可以拖动时间轴,快速检查某一秒的布局。
  2. 截图时可以打开 index.html?t=120,直接定位关键帧。
  3. 渲染时可以逐帧推进,保证导出视频和浏览器预览一致。

以第 3 集“栈和队列”为例,脚本中维护了旁白时间段、场景切换、元素高亮和控制台状态。栈的入栈、出栈,队列的 front/rear 移动,循环队列的取模回绕,都由当前时间计算出来,而不是依赖一次性播放动画。

核心实现可以概括成三步:

function updateScene(time) {
scenes.forEach((scene) => {
const start = Number(scene.dataset.start);
const duration = Number(scene.dataset.duration);
scene.classList.toggle("is-active", time >= start && time < start + duration);
});

const pushLocal = sceneLocal(".scene-stack-push", time);
setLit(".item-a", pushLocal >= 2, "is-in");
setLit(".item-b", pushLocal >= 6, "is-in");
setLit(".item-c", pushLocal >= 10, "is-in");
}

第一步是根据全局时间 time 找到当前场景。第二步是把全局时间转换成场景内部的局部时间,例如第 3 集入栈场景从 78.90 秒开始,那么全局 84.90 秒对应局部 6 秒。第三步是用局部时间切换 CSS class,让元素进入、离开、高亮或移动。

这里没有使用 setTimeout 串联动画,原因很直接:setTimeout 适合从头播放,但不适合跳到任意一帧。如果学生反馈“第 2 分 10 秒的 top 指针不对”,工程上应该能直接打开 index.html?t=130 复现,而不是从头播放等到那一刻。

项目实际采用的是“状态派生”思路:

当前秒数
-> 当前场景
-> 场景局部时间
-> DOM class 状态
-> CSS 负责最终视觉表现

这和很多前端 UI 的状态管理类似:不要记录“动画已经播放到哪一步”,而是每次根据当前时间重新计算“此刻应该是什么状态”。这样即使拖动进度条、刷新页面、自动截图或逐帧渲染,画面状态也能保持一致。

第 3 集里几个典型状态映射如下:

知识点 时间状态 画面状态
顺序栈入栈 pushLocal >= 2/6/10 A、B、C 依次添加 is-in
栈顶变化 pushLocal 落在不同区间 top 指针添加 at-aat-bat-c
出栈 popLocal >= 4 C 添加 is-out,B 变成 is-top
队列入队 queueLocal >= 2/7/12/17 rear 指针向后移动
循环队列 circleLocal >= 9 rear 从末尾回到 0 号位置

技术上看,这套动画不是视频剪辑,而是一个由时间驱动的确定性 UI 状态机。

5. 视觉风格:Geometric Bold

这套微课采用 Geometric Bold 风格:高饱和纯色、粗黑边框、大字号标题、强几何图形。

选择这个风格不是为了装饰,而是因为数据结构本身很适合被转译成几何对象:

数据结构对象 画面表达
数组 等宽矩形格
链表节点 节点块 + 粗箭头
竖向容器 + top 指针
队列 横向格子 + front/rear 指针
圆形节点 + 连接边
顶点网络 + 访问高亮
排序 柱状条 + 比较/交换颜色

视觉规则保持克制:

  • 不使用渐变背景。
  • 不使用模糊阴影。
  • 主体元素使用纯色块和粗黑边。
  • 代码只展示 2 到 4 行关键逻辑。
  • 每一屏都必须有图形、流程图或结构图,避免变成文字卡片。

例如讲顺序栈时,代码只保留最核心的两行:

S.data[++S.top] = x;
x = S.data[S.top--];

画面同步展示 top 指针上移、元素入格、元素出格、top 回退。学生看到的不是孤立语法,而是“代码变量”和“图形状态”的一一映射。

6. 配音流程:分段生成,统一对齐

配音没有采用整篇一次性生成,而是按场景切成多个小段:

assets/narration/
segments.csv
001.txt
002.txt
...
audio-minimax/
aligned-minimax/
voice-03-stack-queue-minimax.wav

segments.csv 记录每段旁白的起止时间和文本文件。每个 txt 只对应一个明确的画面段落,例如“栈的概念”“push 动作”“pop 动作”“循环队列取模”。

这里的关键不是“把文本拆小”,而是让旁白也进入同一条时间轴。每一段旁白都有明确的开始秒数、结束秒数和对应文本:

start,end,file
78.90,101.76,004.txt
102.26,127.86,005.txt
128.36,153.06,006.txt

这样第 004 段就只服务于入栈动画,第 005 段只服务于出栈动画,第 006 段只服务于栈代码解释。画面时间和音频时间不再靠人工感觉对齐,而是由同一份分段表约束。

分段的好处很明显:

  • 某一句念得不自然,只需要重生成这一段。
  • 某个动画加长后,只需要调整对应段落。
  • 音频可以用静音补齐或轻微变速对齐到目标时间。
  • 最终再用 ffmpeg 将视频轨和旁白音轨合成。

项目中使用 PowerShell 脚本封装 MiniMax TTS、ffprobe 时长检测、音频对齐和 ffmpeg 合成。大致流程是:

读取 segments.csv
-> 逐段读取 001.txt、002.txt ...
-> 调用 TTS 生成原始语音
-> 用 ffprobe 获取实际音频时长
-> 对齐到目标 start/end 区间
-> 拼接为整集旁白 wav
-> 与无声 mp4 合成为带配音 mp4

音频对齐时有两个常见情况:

情况 处理方式
生成音频短于目标时长 在片段末尾补静音
生成音频略长于目标时长 轻微调整语速或回到文本压缩

这一步的技术价值在于“可局部返工”。如果第 3 集最后一句公式讲得太快,只需要改 011.txt 并重生成最后一段,不需要重新处理整集旁白。最终输出也会保留无声视频和配音视频两个版本,例如:

output/
episode-03-stack-queue.mp4
episode-03-stack-queue-minimax-voiceover.mp4

保留无声版本可以让后续换声音、换语速、换平台字幕时更方便,避免每次都从浏览器重新渲染画面。

7. 质量检查:截图比肉眼拖进度条可靠

数据结构微课的画面问题通常出现在关键帧:元素刚移动完、指针刚切换、代码刚高亮、字幕刚进入。单纯播放一遍很容易漏掉文字越界或状态错位。

因此项目保留了大量 snapshots/ 截图,用于检查:

  • 画布是否完整填满 16:9。
  • 中文是否正常显示。
  • 标题、字幕、代码是否越界。
  • 节点、箭头、数组格是否对齐。
  • 当前讲解的变量是否和图形状态一致。
  • submit.html 是否隐藏控制台和调试时间码。

截图检查的具体做法是先列出每集的关键时刻,再用浏览器自动打开对应时间点:

index.html?t=3
index.html?t=78.9
index.html?t=102.26
index.html?t=234.26

这些时间点不是随便选的,而是来自场景起止时间和动作变化时间。例如栈入栈要截 A 入栈、B 入栈、C 入栈三个状态;循环队列要截 rear 到 3、到 4、回绕到 0 的状态;图遍历要截 DFS 正在深入、BFS 队列扩展、访问完成这几个状态。

对数据结构课程来说,截图验收不只是看排版,还要看“教学状态是否正确”:

检查对象 技术检查 教学检查
顺序栈 top 指针位置是否和 CSS class 一致 是否体现后进先出
循环队列 front/rear 是否正确回绕 是否解释清楚假溢出
二叉树遍历 节点高亮顺序是否正确 序列是否匹配先序/中序/后序定义
图遍历 顶点访问状态是否稳定 DFS 和 BFS 的差异是否可见
排序动画 比较、交换、已排序颜色是否区分 学生能否看出算法过程

这种检查方式更接近前端 UI 自动化测试:不是“看起来差不多”,而是把每个关键状态固定下来,逐帧验收。尤其是 submit.html,必须单独检查,因为它是最终导出入口。开发入口没问题,不代表提交入口一定没问题;如果提交入口忘记隐藏控制台,最终视频就会带上时间码或滑块。

所以项目的验收标准可以写成一句话:同一秒钟,开发预览、截图结果、最终视频三者必须一致

8. 技术取舍

这套方案的核心取舍是:用工程复杂度换取教学画面的可控性。

传统 PPT 更快,但状态变化不容易精确复现;专业动画软件表现力更强,但批量修改和版本管理成本高。HTML/CSS/JS 处在中间位置:开发成本可接受,又能把每个教学对象变成可维护的 DOM 状态。

更具体地说,项目做了几组取舍:

方案 优点 问题 本项目选择
PPT 录屏 制作快、门槛低 精确动画和批量修改弱 不作为主方案
AE/PR 动画 视觉表现强 版本管理和程序化复用成本高 只适合后期包装
Canvas 绘制能力强 文本排版和 DOM 调试不如 HTML 直观 局部可用,不作为主结构
HTML/CSS/JS 可维护、可截图、可复现 初期工程搭建更复杂 作为主方案

另一个取舍是“CSS class 驱动”而不是“直接改 style”。例如元素进入、指针移动、代码高亮都尽量通过添加 is-inis-topis-lit 等类名完成。这样 JS 负责状态,CSS 负责表现,两者边界比较清楚:

JS:现在 C 应该是栈顶
CSS:栈顶应该用什么颜色、边框、位置、过渡效果

这会让调试更直观。打开浏览器开发者工具,看一个节点有没有 is-top,就能判断是逻辑没算对,还是样式没写对。

当前方案适合以下场景:

  • 需要大量结构化图解。
  • 需要反复修改局部动画。
  • 需要批量生产同一视觉系统下的视频。
  • 需要把知识点、代码、旁白、截图、成片统一纳入版本管理。

不适合的场景也很明确:

  • 只做口播课,不需要复杂动画。
  • 追求影视级特效。
  • 内容变化极少,不需要工程化复用。

9. 后续自动化方向

项目已经形成了清晰流水线,后续可以继续自动化:

输入主题和知识点
-> AI 生成脚本
-> AI 生成 HTML/CSS/JS
-> 浏览器自动截图检查
-> 渲染无声 MP4
-> 生成分段旁白
-> TTS 配音
-> 音视频合成
-> 前端面板展示进度和产物

这里最值得继续投入的是“自动检查”。因为 AI 可以生成脚本和代码,但最终教学质量仍然取决于画面是否清楚、节奏是否合理、代码是否真的对应动画。自动截图、关键帧验收、样式规则检查,比单纯追求一键生成更重要。

后续可以把自动化拆成三个层次:

第一层是文件级检查。脚本可以检查每集是否都有 index.htmlsubmit.htmlstyle.cssscript.jssegments.csvoutput/,以及根节点是否声明 data-composition-iddata-widthdata-heightdata-duration

第二层是画面级检查。用 Playwright 打开每个关键帧,自动截图并保存到 snapshots/qa-current/。如果画布不是 16:9、出现横向滚动条、标题越界、字幕为空、调试控件出现在提交入口,就直接报错。

第三层是教学一致性检查。每个 episode 可以维护一份关键帧清单:

[
{ "time": 78.9, "expect": ["scene-stack-push", "item-a"] },
{ "time": 102.26, "expect": ["scene-stack-pop", "pop-c"] },
{ "time": 260.46, "expect": ["scene-formulas", "f-empty"] }
]

自动化脚本根据清单检查 DOM class 是否符合预期。这样不仅能发现“页面有没有渲染”,还能发现“该亮的元素有没有亮”。对于数据结构微课,这比单纯截图更重要,因为教学错误往往不是页面空白,而是某个指针、某个访问顺序、某个公式和旁白不一致。

如果继续扩展,还可以把 AI 生成内容纳入这个流程:AI 先生成脚本和场景代码,再由自动检查给出失败点,例如“第 145 秒 DFS 高亮节点和旁白不一致”“第 210 秒公式卡片超出舞台”。这样 AI 不只是生成代码,还能进入一个可验证、可迭代的制作闭环。

10. 总结

这套数据结构微课的技术路线可以概括为一句话:

用前端工程的方法,把抽象的数据结构状态变化变成可定位、可复现、可检查、可合成的视频动画。

它不是把网页录成视频这么简单,而是把视频当成一条可编程时间轴:每个知识点对应一个场景,每个场景对应一组几何对象,每个对象的状态都能被脚本精确控制。对数据结构这种强调“过程理解”的课程来说,这种方式能把抽象概念转成学生真正看得见的变化。