首页
/ 免费代码训练营「构建个人作品集网页」完整通关指南:从用户故事到自动化测试

免费代码训练营「构建个人作品集网页」完整通关指南:从用户故事到自动化测试

2026-09-07 18:04:38作者:平淮齐Percy

freeCodeCamp「响应式网页设计」认证经典项目——构建个人作品集网页(Build a Personal Portfolio Webpage),要求用纯 HTML 与 CSS 从零搭建一个含欢迎区、作品区、固定导航栏和个人主页链接的作品集页面,并逐条通过内置自动化验收测试。阅读本文,你将吃透该项目全部 11 条用户故事(User Stories)与对应的 JS 断言(Hints)校验逻辑,掌握 position: fixed 吸顶导航、100vh 满屏欢迎区、媒体查询与锚点菜单等关键实现手法,并拿到一份可直接通过全部测试的完整代码。

本文主体基于 freeCodeCamp 开源仓库中的课程文件 bd7158d8c242eddfaeb5bd13.md,并结合仓库内的课程结构配置与官方 Solution 做深度拆解。

一、项目定位:这是一道怎样的挑战

从仓库的课程结构可以确认它的地位与运行方式:

  • Block 级配置见 build-a-personal-portfolio-webpage-project.json:该 Block 的 challengeOrder 仅包含本挑战一个条目;usesMultifileEditor: true 表示在多功能编辑器中以「HTML + CSS 两个文件」的形式编辑;helpCategory: "HTML-CSS" 标注其归类。
  • 超级模块级配置见 responsive-web-design-22.jsonbuild-a-personal-portfolio-webpage-projectresponsive-web-design-22 超级模块(含 22 个 Block)的收官项目,排在 learn-css-transforms-by-building-a-penguin 等课程块之后。
  • 挑战文件头部的元数据 challengeType: 14saveSubmissionToDB: true,说明它属于会记录进认证进度的「项目型」提交。

原文档给出的 Objective 只有一句话:构建一个与官方演示站点功能相似的 App,不要复制该演示项目,并加上自己的个人风格("Give it your own personal style")。

同时原文档有一条重要 Note:务必在 HTML 中加入 <link rel="stylesheet" href="styles.css"> 来关联样式表,否则 CSS 全部不生效,测试无法通过。这是新手最常见的「零样式」事故根源。

二、需求规格:11 条 User Stories 逐条精读

原文档把验收标准描述为 11 条用户故事,本质上是「作品集必须能做什么」的行为契约:

  1. 作品集应有一个 welcome section(欢迎区),其 idwelcome-section
  2. 欢迎区应包含一个含文本的 h1 元素;
  3. 作品集应有一个 projects section(作品区),其 idprojects
  4. 作品区应包含至少一个 classproject-tile 的元素来容纳某个项目;
  5. 作品区应包含至少一个指向项目的链接
  6. 作品集应有一个 navbar(导航栏),其 idnavbar
  7. 导航栏应包含至少一个可点击、能在页面各区块间页内跳转的链接;
  8. 作品集应有一个 idprofile-link 的链接,点击后在新标签页打开你的 GitHub 或 freeCodeCamp 主页;
  9. 作品集应至少使用一个媒体查询(media query)
  10. 欢迎区的高度应等于视口高度
  11. 导航栏应始终保持在视口顶部(页面滚动也不离开)。

表面看这些都是基础能力,但真正决定成败的,是隐藏在 Hints 段落里那套浏览器端 JS 断言——下面逐条翻译这些「阅卷标准」。

三、测试机制:Hints 里的 JS 断言是如何验收的

原文档在 --hints-- 段给出了与每条 User Story 对应的 JS 断言。读懂它们,就等于拿到了精确到像素的评分标准。

3.1 关键区块必须真实存在

const el = document.getElementById('welcome-section');
assert.isNotNull(el);

projectsnavbar 两条断言结构完全相同。结论:welcome-sectionprojectsnavbar 三个 id 必须一字不差地存在,任何拼写偏差(大小写、连字符、换行)都会直接失败。

3.2 欢迎区的语义与内容约束

assert.isAbove(
  document.querySelectorAll('#welcome-section h1').length,
  0,
  'Welcome section should contain an h1 element '
);

assert.isAbove(
  document.querySelectorAll('#welcome-section h1')?.[0]?.innerText?.length,
  0,
  'h1 element in welcome section should contain your name or camper name '
);

两条断言合起来的意思是:#welcome-section 内至少有一个 h1,且其中第一个 h1innerText 长度大于 0。建议直接写姓名或 camp 昵称;只放注释或空白不算「有文本」。

3.3 作品区结构与链接约束

assert.isAbove(document.querySelectorAll('#projects .project-tile').length, 0);
assert.isAbove(document.querySelectorAll('#projects a').length, 0);

作品区 #projects 内至少要有一个 class="project-tile" 的容器,并且至少存在一个 <a> 链接指向某个真实项目(可以指向外部托管页或仓库内页面)。

3.4 导航栏必须是「锚点菜单」

const links = [...document.querySelectorAll('#navbar a')].filter(
  nav => (nav?.getAttribute('href') || '').substring(0, 1) === '#'
);
assert.isAbove(links.length, 0, 'Navbar should contain an anchor link ');

测试会遍历 #navbar 内所有 <a>,过滤出 href# 开头的链接并断言其数量大于 0。也就是说,导航栏里只放外链不算数,必须要有 href="#projects"href="#welcome-section" 这类页内锚点链接

3.5 个人主页链接:标签与打开方式双重校验

const el = document.getElementById('profile-link');
assert.isNotNull(el);
assert.strictEqual(el.tagName, 'A');   // 必须是 <a>
assert.strictEqual(el.target, '_blank'); // 必须新标签页打开

#profile-link 必须挂在真正的 <a> 元素上(用 <div> 冒充无效),且必须显式书写 target="_blank"

3.6 至少一个媒体查询

const htmlSourceAttr = Array.from(document.querySelectorAll('source')).map(el => el.getAttribute('media'))
const cssCheck = new __helpers.CSSHelp(document).getCSSRules('media')
assert.isTrue(cssCheck.length > 0 || htmlSourceAttr.length > 0);

验收方式有两种:样式表中存在 @media 规则(通过 CSSHelp.getCSSRules('media') 探测),或文档中存在带 media 属性的 <source> 元素。常规做法是在 CSS 中书写 @media (max-width: ...);若 <link> 没写对导致 CSS 未被加载,这里会拿不到任何 @media 规则而失败——这也印证了原文档 Note 强调关联样式表的原因。

3.7 吸顶导航:唯一带「滚动后复检」的动态断言

const timeout = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));

const navbar = document.getElementById('navbar');
assert.approximately(
  navbar?.getBoundingClientRect().top, 0, 15,
  "Navbar's parent should be body and it should be at the top of the viewport "
);

window.scroll(0, 500);
await timeout(1);

assert.approximately(
  navbar?.getBoundingClientRect().top, 0, 15,
  'Navbar should be at the top of the viewport even after scrolling '
);
window.scroll(0, 0);

这条断言隐藏了三个关键信息:

  1. 两次采样:先检查初始状态,再把页面滚动到 y=500、等待 1ms 后复检。「初始在顶、一滚就跑」的实现(例如仅把导航放在文档顶部而不做固定定位)会在第二次断言失败;
  2. 判定基准getBoundingClientRect().top 是导航栏相对当前视口的坐标,滚动后仍须近似为 0(容差 15px)。只有 position: fixed(或 sticky 等效布局)能持续满足;
  3. 父级约束:断言消息提示 "Navbar's parent should be body",即 #navbar 最好作为 body 的直接子元素,避免被滚动容器包裹产生额外偏移。

四、从零实现:HTML 骨架与 CSS 关键布局

原文档的 --seed-contents-- 提供的种子是两个空文件



也就是说官方不给任何起始代码,HTML 与 CSS 都要你自己写。下面给出按测试要求整理、可直接运行的最小完整实现(讲解用,建议在此基础上加入你的真实内容与个人风格)。

4.1 index.html:语义化骨架

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="styles.css">
    <title>Personal Portfolio</title>
</head>
<body>
    <nav id="navbar">
      <a href="#welcome-section">Home</a>
      <a href="#projects">Projects</a>
      <a href="#contact">Contact</a>
    </nav>

    <main>
      <section id="welcome-section">
        <h1>Hi, I'm Camper</h1>
        <p>A front-end learner from freeCodeCamp</p>
      </section>

      <section id="projects">
        <h2>These are some of my projects</h2>
        <div class="project-tile">
          <a href="https://your-demo-link.example/tribute" target="_blank">Tribute Page</a>
        </div>
        <div class="project-tile">
          <a href="https://your-demo-link.example/survey" target="_blank">Survey Form</a>
        </div>
      </section>

      <section id="contact">
        <h2>Let's work together...</h2>
        <a id="profile-link"
           href="https://www.freecodecamp.org/your-username"
           target="_blank" rel="noopener noreferrer">freeCodeCamp Profile</a>
      </section>
    </main>
</body>
</html>

对照需求自查:

需求编号 落实位置
1、2 <section id="welcome-section"> 内含非空 <h1>
3、4、5 <section id="projects"> 内有 .project-tile 与指向项目的 <a>
6、7 #navbarbody 直接子元素,内含 href="#..." 锚点链接
8 <a id="profile-link" target="_blank"> 指向 freeCodeCamp/GitHub 主页

4.2 styles.css:吸顶、满屏与媒体查询

/* 全局重置 */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* ---------- 导航栏:固定于视口顶部 ---------- */
#navbar {
  position: fixed;      /* 相对视口固定,是 3.7 断言的前提 */
  top: 0;
  left: 0;
  width: 100%;
  display: flex;
  justify-content: flex-end;
  background-color: #be3144;
  z-index: 10;
}
#navbar a {
  color: #fff;
  padding: 1rem 1.5rem;
  text-decoration: none;
}
#navbar a:hover {
  background-color: #45567d;
}

/* ---------- 欢迎区:高度 = 视口高度 ---------- */
#welcome-section {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100vh;         /* 需求 10:满屏 */
  background: linear-gradient(45deg, #3a3d40 0%, #181719 100%);
  color: #f0f0f0;
}

/* ---------- 作品区 ---------- */
#projects {
  text-align: center;
  padding: 6rem 1rem;
  background-color: #45567d;
  color: #f0f0f0;
}
.project-tile {
  display: inline-block;
  margin: 1rem;
  background: #303841;
}
.project-tile a {
  display: block;
  padding: 1.5rem;
  color: #f0f0f0;
  text-decoration: none;
}
.project-tile a:hover {
  background: #be3144;
}

/* ---------- 联系区 ---------- */
#contact {
  text-align: center;
  padding: 6rem 1rem;
  background-color: #303841;
  color: #f0f0f0;
}

/* ---------- 媒体查询(需求 9):小屏适配 ---------- */
@media (max-width: 720px) {
  #navbar {
    justify-content: center;
  }
  .project-tile {
    display: block;
    width: 90%;
    margin: 1rem auto;
  }
}

三处与断言的强对应关系:

  • #navbar { position: fixed; top: 0; } —— 满足 3.7 的动态滚动断言(getBoundingClientRect().top ≈ 0);
  • #welcome-section { height: 100vh; } —— 满足需求 10(100vh = 视口高度的 100%);
  • 文件末尾存在 @media —— 满足 3.6 的 CSSHelp.getCSSRules('media') 探测。

4.3 原文档官方 Solution 中的设计手法

原文档在 --solutions-- 给出了一套官方参考实现(HTML + CSS 双文件),其导航与满屏布局的核心片段如下:

/* 官方 Solution:导航栏固定 */
nav {
  position: fixed;
  width: 100%;
  text-align: right;
  font-size: 24pt;
  top: 0%;
  right: 5px;
  background-color: #000000;
  color: #ffffff;
}

/* 官方 Solution:媒体查询 */
@media (max-width: 500px) {
  nav {
    display: none;    /* 小屏隐藏导航的响应式策略 */
  }
}
/* 官方 Solution:各 section 满屏并垂直居中 */
#welcome-section {
  display: table-cell;
  vertical-align: middle;
  width: 100vw;
  height: 100vh;
}

官方实现的可提炼手法有三点:

  1. position: fixed + top: 0% 实现吸顶,再辅以 right 调整位置;
  2. 借助 100vh/100vw 让区块占满一屏,用 display: table-cell; vertical-align: middle 或 flex 做内容垂直居中;
  3. <nav id="navbar"> 放在 body 的首层直接子元素位置,满足 3.7 断言对父容器的约束。

此外,原文档还附赠了一个最简可通过样例(第二份 Solution),直观展示「过测」的下限:

<head><style>@media (max-width: 500px){nav{display: none;}}</style></head><body><nav id="navbar"><a href="#projects">text</a> | </nav><main><section id="welcome-section"><h1>text</h1></section><hr><section id="projects"><h1>Projects</h1><h2 class="project-tile"><a id="profile-link" target="_blank" href="https://freecodecamp.org">text</a></h2></section><hr></body></html>

有趣的是,这份最简实现把 #profile-link 放进了 #projects 内部、把 @media 直接内联进 <style> 标签——说明测试只检查存在性、位置与关键行为,并不限制结构归属或要求内联/外联写法。但认证的意义不止于「过测」:请务必在满足断言的基础上,写出有完整配色、真实项目链接与个人品牌的成品页面。

五、常见失败点与排查对照表

结合前文断言,把最容易翻车的点整理成「症状 → 根因 → 对应断言」对照表:

症状 根因 对应断言
welcome-section/projects/navbar 报 null id 拼写不一致(大小写、连字符、空格) getElementById(...)
h1 断言不通过 #welcome-section 内无 h1,或 h1 内容为空 querySelectorAll('#welcome-section h1')innerText.length
导航栏有链接却报错 href 不以 # 开头(放了外链) 过滤后 links.length > 0
滚动后导航栏掉出视口 未用 position: fixed;或 #navbar 被包在滚动容器内 getBoundingClientRect().top 二次断言
media query 断言失败 CSS 未通过 <link> 正确加载,或确实没写 @media CSSHelp(...).getCSSRules('media')
profile-link 断言失败 挂在非 <a> 元素上;或漏写 target="_blank" el.tagNameel.target
页面整体无样式 忘记 <link rel="stylesheet" href="styles.css"> 原文档 Note 强调项

排查技巧:直接在浏览器 DevTools Console 中执行与断言等价的命令,例如 document.getElementById('welcome-section')document.querySelectorAll('#projects .project-tile').length;验证吸顶则手动 window.scrollTo(0, 500) 后读取 document.getElementById('navbar').getBoundingClientRect().top

六、在仓库内继续深挖与自行验证

本挑战完整保存在仓库课程目录中,可继续查阅:

本地练习建议:新建一个独立文件夹,放入上面 4.1/4.2 的 index.htmlstyles.css,直接双击打开即可预览;若浏览器阻止了某些加载方式,可用任意静态服务器(如 python3 -m http.server)起一个本地服务访问。请始终保留原文档 Note 强调的 <link rel="stylesheet" href="styles.css">,这是外部样式表在多功能编辑器中生效、并被测试读取到的前提。

结语

「构建个人作品集网页」是整个响应式网页设计课程体系中的收官项目,也是对语义化 HTML、id/class 选择器、固定定位、视口单位与媒体查询的一次综合验收。把它当作一份「可执行的契约」:User Stories 定义需求,Hints 断言定义可验证的验收标准,官方 Solution 与最简样例则分别代表「参考质量」与「过测下限」。在这两者之间留出空间,注入你自己的项目、配色与故事——你得到的将不止是一张认证,而是一个可以长期维护、真正上线的个人主页。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391