首页
/ 解剖 freeCodeCamp 课程挑战定义文件:以首个 HTML 挑战为样本的前置元数据、五段式正文与校验链路

解剖 freeCodeCamp 课程挑战定义文件:以首个 HTML 挑战为样本的前置元数据、五段式正文与校验链路

2026-09-06 15:27:47作者:裘晴惠Vivianne

freeCodeCamp 的课程(curriculum)以大量 Markdown 挑战文件为核心资产,curriculum/fixtures/combined.md 正是其中最典型的样本——它是「Say Hello to HTML Elements」(向 HTML Elements 说你好)这一 HTML 入门挑战的合并版夹具文件。本文以该文件为主体,逐字段解析其前置元数据(Front Matter)与五段式正文结构,并结合 curriculum 包的源码与测试,说明这类挑战文件如何被校验、如何区分英文源与翻译合并版,读完你可以完整读懂乃至亲手编写一个合法的课程挑战文件。

样本文件在 curriculum 包中的位置:测试夹具约定

curriculum/__fixtures__/ 目录是 curriculum 包测试用的夹具(fixture)根目录,其布局本身就携带了一条重要约定:顶层的 combined*.md 文件是翻译/合并后的挑战版本,english/ 子目录中存放对应的英文源文件。当前目录结构如下:

这条约定直接对应 get-challenges.ts 中的 hasEnglishSource 函数:它把英文源根目录解析为 <basePath>/english,再检查目标挑战文件是否存在,返回布尔值。对应的测试 get-challenges.test.jsbasePath = '../__fixtures__' 验证了两种情形:challenge.md 存在时返回 trueno/challenge.md 缺失时返回 false。也就是说,顶层 combined.md(中文版)与 english/challenge.md(英文版)构成了「非英文挑战文件 + 其英文源」的标准配对样本,供翻译管线相关逻辑做测试。

前置元数据(Front Matter)逐字段解析

combined.md 的 Front Matter 完整内容如下,这是每个挑战文件的身份声明区:

---
id: bd7123c8c441eddfaeb5bdef
originalTitle: Say Hello to HTML Elements
challengeType: 0
videoUrl: 'https://scrimba.com/p/pVMPUv/cE8Gpt2'
forumTopicId: 18276
title: 向HTML Elements说你好
---

各字段含义与约束如下:

字段 取值 说明
id bd7123c8c441eddfaeb5bdef 挑战的唯一标识(24 位十六进制)。中文版与英文版共用同一个 id,表示二者是同一挑战的不同语言版本
originalTitle Say Hello to HTML Elements 英文原标题。该字段仅出现在非英文(翻译/合并)版本中,英文源文件里没有它
challengeType 0 挑战类型编号,0 表示 HTML 挑战
videoUrl Scrimba 视频地址 挑战配套视频,可选字段,允许为空字符串
forumTopicId 18276 帮助论坛对应主题帖的编号,类型为数字
title 向HTML Elements说你好 面向学习者显示的标题,这里已是中文

challengeType: 0 这一取值不是随意约定的,它在共享包 challenge-types.ts 中被定义为 const html = 0;,并且由此派生出两条关键行为:

  • 视图类型viewTypes 表中 [html]: 'classic',即 HTML 挑战使用经典编辑器视图渲染;
  • 提交类型submitTypes 表中 [html]: 'tests',即完成提交前必须通过该挑战声明的全部测试。

此外,hasNoSolution(0) 返回 false——HTML 类型不在「无解答」列表中,从源码结构看这正对应了挑战文件中存在的 ## Solution 小节:这类挑战携带参考解答。

这些字段的硬性约束由 challenge-schema.js 中的 Joi 校验表把关,其中与本样本直接相关的条目包括:

  • id: Joi.objectId().required()——id 必须是合法的 ObjectId 格式,这与文件中 24 位十六进制取值的形态一致;
  • challengeType: Joi.number().min(0).max(33).required()——类型编号必须是 0 到 33 的整数;
  • title: Joi.string().required()——标题必填;
  • tests: Joi.array().items(...).required()——公开挑战的每条测试必须是 { id?, text(必填), testString } 结构;
  • videoUrl: Joi.string().allow('')forumTopicId: Joi.number()——分别对应上表中的可选与数字字段。

校验入口为同文件导出的 challengeSchemaValidator,它对单个挑战对象执行 schema.validate(challenge)

五段式正文:从 Description 到 Solution

Front Matter 之后是固定骨架的五个小节:## Description## Instructions## Tests## Challenge Seed## Solution,每节用带 id<section> 标签包裹。以下按原文件顺序逐节拆解。

Description:HTML 元素与开闭标签

Description 小节面向学习者讲解本章第一个概念,原文核心内容如下:

欢迎来到 freeCodeCamp 的 HTML 编码挑战。这些挑战将逐步引导您完成 Web 开发。首先,您将使用 HTML 构建一个简单的网页。您可以在嵌入到此网页中的代码编辑器(code editor)编辑代码。您是否在代码编辑器中看到了 <h1>Hello</h1>?这是一个 HTML 元素(element)。大多数 HTML 元素都有一个开始标记(opening tag)和结束标记(closing tag):开始标记形如 <h1>,结束标记形如 </h1>。两者唯一的区别是结束标记左括号后面的那个正斜杠。每个挑战都有可以随时单击「运行测试」按钮运行的测试;当您通过所有测试时,系统会提示您提交解决方案并进入下一个编码挑战。

注意原文中 <h1><code> 等尖括号内容是以 HTML 实体(&lt; / &gt;)转义后写在 <code> 标签里的——挑战正文允许内联 HTML,而尖括号必须转义才能安全通过解析。对比英文源 english/challenge.md,其 Description 使用的是 &#60;h1&#62; 这类十进制实体转义;两个文件转义风格不同但语义等价,从源码结构看这是翻译/合并环节产物与英文原件在文本细节上的正常差异。

Instructions:明确可执行的唯一指令

要通过此挑战的测试,请将 h1 元素的文本更改为 "Hello World"。

Instructions 小节给出学习者需要做的唯一修改:把 <h1> 里的文本从 Hello 改成 Hello World。它与 Tests 小节的断言是一一对应的——这是该课程「指令-测试」配对的典型写法。

Tests:断言即文档

Tests 小节以 YAML 代码块声明测试数组,combined.md 原文为:

tests:
  - text: 你的<code>h1</code>元素应该有“Hello World”文本。
    testString: assert.isTrue((/hello(\s)+world/gi).test($('h1').text()));

这条 testString 值得逐段拆解:

  • $('h1'):测试运行时提供 jQuery 风格的 $ 选择器,选中渲染结果里的 h1 元素;.text() 取其纯文本内容;
  • /hello(\s)+world/gi:正则表达式要求文本中 helloworld 之间有一个或多个空白字符(\s+),g 为全局匹配、i 为忽略大小写,因此 Hello WorldHELLO world 都能通过,而 HelloWorld(无空格)会失败;
  • assert.isTrue(...):测试断言 API,断言正则匹配结果为真,断言失败时展示 text 字段作为提示文案。

text 字段是写给学习者看的中文提示,testString 是机器执行的断言。二者构成 { text, testString } 结构,正好匹配前文 Joi 校验中对公开挑战测试的定义。

Challenge Seed:练习的起点代码

<h1>Hello</h1>

Challenge Seed 小节被 <div id='html-seed'> 包裹,其代码块是编辑器中预置的起始代码:一个写着 Helloh1 元素。学习者需要在此种子代码基础上完成 Instructions 要求的修改。

Solution:参考解答

<h1>Hello World</h1>

Solution 小节给出通过测试的参考解答——仅将文本补全为 Hello World。由于 challengeType: 0 属于「有解答」类型(见上文 hasNoSolution 分析),这个小节在样本中是齐备的。

英文版与合并版的差异:翻译管线留下的痕迹

combined.md 与英文源 english/challenge.md 并排对照,可以得到一份清晰的「合并版改造清单」:

维度 英文源 english/challenge.md 合并版 combined.md
id / videoUrl bd7123c8c441eddfaeb5bdef / 同一 Scrimba 地址 完全相同(同一挑战的身份与配套视频不随语言变化)
originalTitle 有,记录英文原标题
title Say Hello to HTML Elements 向HTML Elements说你好
forumTopicId 12345 18276
Description / Instructions / Tests text / Seed / Solution 英文 中文(代码本身保持英文,如 <h1>Hello</h1> 不变)
尖括号转义 &#60;h1&#62;(十进制实体) &lt;h1&gt;(命名实体)

testString 与代码种子在两版中保持字节级一致——测试逻辑与代码不随语言翻译,只有 text 提示与叙述性正文被替换,这正是「合并版」文件的核心特征:英文代码骨架 + 翻译后的说明文字 + 记录来处的 originalTitle

姊妹夹具 combined-html-comments.md 还展示了合并版可能携带的另一类内容:其 Challenge Seed 中出现了 <!-- (Chinese) Add your code below this line (Chinese) --> 这类中文专属代码注释标记,说明合并环节会把语言特定的种子代码注释一并合入。

从夹具到课程:构建与校验链路

理解了单个文件之后,再看 curriculum 包如何消费这类文件:

  1. 语言校验与构建入口get-challenges.tsgetChallengesForLang(lang, filters) 先校验语言是否在 availableLangs.curriculum 中(非法语言直接抛出 "lang is not an accepted language" 错误),再调用 buildCurriculum 构建整份课程。get-challenges.test.jsgetChallengesForLang('notlang') 验证了该抛错分支。
  2. 英文源存在性检查hasEnglishSource(同文件)解析 <basePath>/english 根目录并做文件系统访问检查,是翻译流程判断「是否已有英文原文可对齐」的依据,夹具中的 english/ 子目录即为此服务的测试数据。
  3. 翻译词典build-curriculum 提供的 createCommentMap 用于从词典目录生成「英文注释 → 各语言译文」的映射;其测试 build-curriculum.test.js 正是加载 __fixtures__/dictionaries(完整词典)与 __fixtures__/incomplete-dicts(残缺词典)验证两种行为——词典齐全时返回各语言译文,词条缺失时回退为未翻译的原文('To be translated two' 回退为自身)。这解释了夹具中为什么同时备有两套词典数据。
  4. 结构校验:任何挑战文件在构建产物中都要通过 challenge-schema.js 的 Joi 校验(idchallengeType 0–33、titletests 数组等硬性要求),challenge-schema.test.mjs 及其快照文件对校验器行为做了回归测试。

小结

combined.md 虽然只是一个小型夹具,却完整呈现了 freeCodeCamp 课程挑战定义文件的三要素:id 为跨语言锚点的前置元数据、指令与断言一一配对的五段式正文、以及「英文源 + 合并版 + 翻译词典」三层夹具约定的翻译管线数据面。掌握这套格式后,你可以对照 challenge-schema.js 的 Joi 约束自行书写挑战文件,并用 get-challenges.test.jsbuild-curriculum.test.js 等测试了解每个环节被验证的具体行为。

登录后查看全文
热门项目推荐
相关项目推荐