首页
/ freeCodeCamp 课程实战解析:用 `<form>` 与 `action` 属性创建可向服务器提交数据的纯 HTML 表单

freeCodeCamp 课程实战解析:用 `<form>` 与 `action` 属性创建可向服务器提交数据的纯 HTML 表单

2026-09-06 19:07:44作者:温玫谨Lighthearted

本文基于 freeCodeCamp 开源仓库中 Responsive Web Design(响应式网页设计)认证的 Basic HTML and HTML5 课程块里的 CatPhotoApp 练习 Create a Form Element,系统讲解 HTML 表单元素的创建方法、action 属性的作用与取值方式,完整还原挑战的初始代码(seed)、题目要求、自动判题(hints)逻辑与标准答案,并结合仓库中的课程结构、挑战文件格式与 Schema 校验规则,帮助读者既通过关卡,也真正理解「纯 HTML 即可实现表单数据提交」这一核心机制。

挑战全景:它在 freeCodeCamp 课程体系中的位置

Create a Form Element 对应仓库文件 curriculum/challenges/english/blocks/basic-html-and-html5/bad87fee1348bd9aede08830.md,属于 Basic HTML and HTML5 课程块。该块的块级定义位于 curriculum/structure/blocks/basic-html-and-html5.json,从该 JSON 可以看到其 helpCategory"HTML-CSS",且块内按顺序列出了全部课程挑战;而本块又是响应式网页设计超块(superblock)的第一个块,见 curriculum/structure/superblocks/responsive-web-design.json

curriculum/structure/blocks/basic-html-and-html5.json 中记录的挑战顺序可以确认它的前后语境:

  • 前一个挑战是 Add Placeholder Text to a Text Field(已创建一个带占位文字的文本输入框);
  • 本挑战 Create a Form Element 的任务是把这个文本框用 <form> 包起来,并给出提交目标地址;
  • 紧随其后的下一个挑战是 Add a Submit Button to a Form,负责在表单内补充真正的提交按钮,让整条提交链路完整落地。

因此本挑战是 CatPhotoApp 系列表单部分承上启下的一环:上一关准备好了输入控件,这一关把控件「放进会提交数据的容器」,下一关再加「触发提交的按钮」。

挑战文件的 frontmatter 解读

挑战文件头部是一段 YAML frontmatter,字段分别代表:

字段 本挑战取值 含义
id bad87fee1348bd9aede08830 挑战全局唯一 ID,也用于结构 JSON 中与标题配对
title Create a Form Element 挑战标题
challengeType 0 挑战类型,0 代表 HTML 挑战(classic 编辑器形态)
forumTopicId 16817 论坛讨论主题 ID
dashedName create-a-form-element URL 友好的英文短名

关于 challengeType: 0 的含义,可在仓库常量定义 packages/shared/src/config/challenge-types.ts 中验证:const html = 0;,且在后续 viewTypes 映射中 [html]: 'classic',说明该类型挑战在经典 HTML 编辑器界面中完成。这一整套 frontmatter 字段还会被 curriculum/schema/challenge-schema.js 里的 Joi Schema 校验(例如要求 challengeType0–33 的数字、dashedName 符合 slug 规则),保证仓库中每个挑战文件结构合法。

核心知识:action 属性如何让表单真正「提交」

挑战的 # --description-- 部分点明了本关要传递的核心概念:

使用纯 HTML,就可以构建出真正把数据提交到服务器的网页表单。实现方式是在 form 元素上指定一个 action 属性。

action 属性决定了表单数据提交时发送到的目标地址。当用户填写表单并触发提交后,浏览器会把表单内带有 name 的各控件取值收集起来,编码后向 action 指定的 URL 发起请求。默认情况下请求方法为 GET,数据会以查询字符串(query string)的形式附加在地址上;也可以在 <form> 上通过 method 属性改为 POST 等其他方法。

挑战给出的示范代码清晰地展示了最小可提交表单的骨架:

<form action="url-where-you-want-to-submit-form-data">
  <input>
</form>

这也就是本关需要理解并动手实践的关键结构:<form> 是数据提交的容器,action 是数据送达的目的地

挑战任务解读:把输入框嵌套进表单并指定提交地址

# --instructions-- 给出的任务分两步:

  1. 把已经存在的 input 元素嵌套到一个 form 元素内部;
  2. 为这个 form 元素的 action 属性赋值为字符串 https://www.freecatphotoapp.com/submit-cat-photo

这里的 https://www.freecatphotoapp.com/submit-cat-photo 是 freeCodeCamp 为 CatPhotoApp 示例虚构的一个表单处理地址,用于演示「把猫咪照片链接提交到某处」的完整表单流程。

初始代码(seed)

# --seed-- 中的 ## --seed-contents-- 提供了本关开始时编辑器内的完整 HTML:

<h2>CatPhotoApp</h2>
<main>
  <p>Click here to view more <a href="#">cat photos</a>.</p>

  <a href="#"><img src="https://cdn.freecodecamp.org/curriculum/cat-photo-app/relaxing-cat.jpg" alt="A cute orange cat lying on its back."></a>

  <p>Things cats love:</p>
  <ul>
    <li>catnip</li>
    <li>laser pointers</li>
    <li>lasagna</li>
  </ul>
  <p>Top 3 things cats hate:</p>
  <ol>
    <li>flea treatment</li>
    <li>thunder</li>
    <li>other cats</li>
  </ol>
  <input type="text" placeholder="cat photo URL">
</main>

可以看到,初始代码中最后的 <input type="text" placeholder="cat photo URL"> 是裸放在 <main> 里的,尚未被任何表单包裹,这正是本关需要修改的位置。注意 <input> 是一个典型的空元素(void element),它自身没有闭合标签,因此不存在「把闭合标签放进表单」的疑问——本关需要的只是在它外面包上一对 <form>...</form> 标签。

标准答案逐步拆解

# --solutions-- 部分给出的官方参考答案如下:

<h2>CatPhotoApp</h2>
<main>
  <p>Click here to view more <a href="#">cat photos</a>.</p>

  <a href="#"><img src="https://cdn.freecodecamp.org/curriculum/cat-photo-app/relaxing-cat.jpg" alt="A cute orange cat lying on its back."></a>

  <p>Things cats love:</p>
  <ul>
    <li>catnip</li>
    <li>laser pointers</li>
    <li>lasagna</li>
  </ul>
  <p>Top 3 things cats hate:</p>
  <ol>
    <li>flea treatment</li>
    <li>thunder</li>
    <li>other cats</li>
  </ol>
  <form action="https://www.freecatphotoapp.com/submit-cat-photo">
    <input type="text" placeholder="cat photo URL">
  </form>
</main>

与初始代码相比,改动集中在最后一处:在 <input> 外层套上 <form>,同时写入 action 属性。需要特别强调的是,input 原有的 type="text"placeholder="cat photo URL" 两个属性必须原样保留,因为自动判题会校验这两个属性是否仍然存在且值正确。

写作时的关键提醒:

  • <form>action 属性写成 https://www.freecatphotoapp.com/submit-cat-photo,不要省略协议头 https://
  • 把开标签放在 <input> 之前、闭标签 </form> 放在 <input> 之后;
  • 保持其余 HTML 内容(标题、段落、列表、图片链接等)不被删改。

参考实现中这一关只改动了 <main> 内部的尾部区域,这也正是挑战文件 # --solutions-- 区块中保存的最终形态,任何能通过下方 hints 的等价写法都是可接受的。

自动判题:hints 究竟在检查什么

freeCodeCamp 的课程文件用 # --hints-- 区块存放自动化测试断言。这些断言以 JavaScript 形式编写,实际运行时会在学习者代码渲染出的 DOM 与源码文本上执行校验。本关的 hints 共四条,逐条解读如下。

Hint 1:input 必须被嵌套在 form 内部

const inputElem = document.querySelector('form input');
assert.strictEqual(inputElem.getAttribute('type'),'text');
assert.strictEqual(inputElem.getAttribute('placeholder'),'cat photo URL');

document.querySelector('form input') 会查找「form 内部的后代 input」。只有把 <input> 放进 <form>,这条选择器才能命中元素;随后再校验该输入框的 type 仍为 textplaceholder 仍为 cat photo URL,确保上一关创建的文本框属性没有被破坏。

Hint 2:action 属性必须指向正确的提交地址

const action = document.querySelector('form').getAttribute('action');
assert.match(action,/^https:\/\/(www\.)?freecatphotoapp\.com\/submit-cat-photo$/i);

这里先取出页面中第一个 form 元素的 action 属性值,再用正则匹配。正则 ^https:\/\/(www\.)?freecatphotoapp\.com\/submit-cat-photo$ 配合末尾的 i 标志,允许 www. 出现或不出现、主机名大小写不敏感,但要求整个值以 https:// 开头、以 /submit-cat-photo 结尾——也就是说地址必须完整指向提交端点。

Hint 3:form 元素必须具有成对的开闭标签

assert.match(code,/<\/form>/g);
assert.match(code,/<form [^<]*>/g);
assert.strictEqual(code.match(/<\/form>/g).length,code.match(/<form [^<]*>/g).length);

这条 hint 直接对学习者提交的源代码文本(code)做正则统计:要求至少出现一个 </form> 闭合标签,至少出现一个带空格与属性内容的 <form ...> 开标签,并且两者的出现次数严格相等。换言之,多写或少写一对标签都无法通过——这是对标签「成对、良好闭合」的最直接检查。

测试框架层面的佐证

挑战内嵌的断言风格(assert.strictEqualassert.match)与 freeCodeCamp 测试框架一致;而「hints 是 JS 断言 + 运行在浏览器 DOM/源码上」这一事实,从本挑战 hints 内同时查询 document(DOM)与 code(源码文本)即可推断得出。读者在实际练习界面点击「Run the Tests」时,以上四条会逐一在背后执行,全部通过即表示过关。

挑战文件格式:Markdown 分节是如何组织的

以本挑战为代表的 freeCodeCamp 课程 Markdown 文件遵循一套固定分节规范,读者若要阅读或编写同类课程内容,可对照本文件结构理解:

  • # --description--:概念讲解与示例代码,是教学正文;
  • # --instructions--:给出本关具体的操作要求;
  • # --hints--:一组可执行的 JS 测试断言(自动判题逻辑);
  • # --seed--## --seed-contents--:学习者在编辑器中的初始代码;
  • # --solutions--:官方参考答案,通常每个答案都保证能通过全部 hints。

这一格式并非随意约定,仓库中的解析与校验链路都在围绕它工作:

学习闭环:从本关到完整表单提交

理解本关后,可以预演一下完整的数据提交链路为何需要后续课程补全:

  1. 本关让 <input> 进入 <form>,并给 <form> 绑定 action 提交地址;
  2. 紧随其后的 Add a Submit Button to a Form 挑战(对应文件 curriculum/challenges/english/blocks/basic-html-and-html5/bad87fee1348bd9aedd08830.md)会在 </form> 内部追加一个 type="submit" 的按钮,作为真正触发提交的控件。该文件 # --solutions-- 中呈现的最终代码正是在本关答案基础上增加了 <button type="submit">Submit</button>
  3. 后续还有 Use HTML5 to Require a Field 等挑战,进一步引入 required 属性等表单校验能力。

由此可见,action 属性 + 表单控件 + 提交按钮,构成了「纯 HTML 表单提交」的最小完整闭环:输入控件负责收集数据,form 容器与 action 地址负责定义送往哪里,submit 按钮负责发起动作。完成本关,意味着你已经掌握了三者中的中间一环,并且理解了仅凭 HTML 语义就能把用户数据发往服务端的核心原理。

小结与练习建议

  • 语法要点<form action="提交地址"> 包裹若干表单控件;本关中 action 的精确取值为 https://www.freecatphotoapp.com/submit-cat-photo
  • 兼容要点:包入 form 后不得破坏 <input> 已有的 typeplaceholder 属性;开闭标签必须成对且数量一致。
  • 验证方式:本关所有测试以 JS 断言驱动(DOM 查询 + 源码正则),分别覆盖嵌套关系、属性取值与标签闭合,全部通过即为过关。
  • 进阶方向:继续完成同块内 Add a Submit Button to a Form 等后续挑战,即可亲手串联出完整的表单提交演示。

想深入研究的读者,可直接在仓库中打开本挑战源文件 bad87fee1348bd9aede08830.md 查看原始 frontmatter 与全部 hints,或对照 basic-html-and-html5.jsonresponsive-web-design.json 观察其在课程树中的位置,从而把「一个关卡」放进「一条完整的学习与知识链路」中去理解。

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