第四次作业交上去的那一刻,我长出了一口气。不是因为终于写完了,而是这次我终于感觉自己做的不是"练习",而是一个"东西"。前三次作业,我基本是跟着教程一步步敲,敲完就交,心里其实没底;第四次不一样,任务书发下来,我在终端里敲下 mkdir fourth-homework 的时候,脑子里已经有了一个完整的页面结构图——我知道自己要先做什么、后做什么、什么能做什么暂时做不了。
如果你也正在经历"每次作业都像在糊弄"的阶段,或者刚学完 HTML/CSS/JavaScript 但不知道怎么写一个像样的完整页面,这篇文章应该能帮到你。我完整复盘一下第四次作业从读题、选方案、写代码到调试交作业的全过程,包括踩过的坑和老师批注里反馈的要点,所有代码和设计思路都是可以直接参考复现的。
1. 第四次作业到底在考什么:从任务书里读出的三个信号
1.1 任务书关键要求与隐含考点
先交代一下作业背景。我当时参加的是一个前端开发方向的训练营,前三次作业分别是:用 HTML 完成个人简历页面、用 CSS 完成一个静态摄影作品集、用浮动和定位还原一个电商局部模块。到第四次作业,任务书很简短,就一段话:
制作一个"智能家居产品展示页",要求移动端优先、响应式布局;页面必须包含导航栏、产品展示区、功能介绍区、表单预约区、页脚;必须使用原生 JavaScript 完成至少三种交互;不使用任何框架或 UI 库;提交源码和一份简短的实现说明。
注意看,这段任务书里其实藏了三个信号。
第一,"移动端优先"不是让你把页面做窄,而是要求你从设计的第一秒就按小屏的体验来考虑。我见过很多同学在电脑上把页面调得好好的,一缩小就乱套。那次作业规定先写移动端样式再写桌面端样式,这背后是一种完全不同的开发习惯,不是"顺便适配一下"。
第二,"不使用任何框架或 UI 库"——这条非常重要。当时班里有人想直接用 Tailwind 或 Bootstrap,结果被打回重做。禁止框架的意义在于:训练营要检验的是你能否用原生代码实现常见的布局和交互。这个约束反而让我放心了,因为我当时的水平是框架也没学明白,老老实实写原生反而没有额外负担。
第三,**"至少三种交互"**意味着你需要自己想清楚:用户会在哪些场景下操作这个页面?不同的交互应该放在什么位置?不是凑三个按钮就算完事,而是交互要和内容天然相关。
1.2 前三次作业的对比与这次质变
我把前三次作业翻出来重新看了一遍,发现一个很明显的问题:第一二三次我都在"还原设计稿",做出来的页面就在那里摆着,没有任何反馈。第四次作业让我第一次意识到,写页面和做产品之间隔着一层"交互设计"。
前三次的差距具体在哪?我自己的对比是:
- 第一次作业:只写了 HTML,没有任何样式,纯看标签语义。我当时甚至分不清
section和article的区别。 - 第二次作业:CSS 布局,用浮动把图片排列整齐。我没有考虑任何屏幕尺寸,固定写死了宽度。
- 第三次作业:模拟一个电商局部模块,大量使用
div嵌套,类名随手起,写完之后自己看都费劲。 - 第四次作业:第一次要求"完整页面 + 交互 + 响应式",这意味着我要把前三次积累的东西全部串联起来使用,还额外增加了 JavaScript 和用户体验的维度。
这种质变,不是难度增加 20%,而是难度翻倍。因为你不再只是"画皮",而是要操心页面被用户操作时的状态变化。
1.3 我给自己定的验收标准
读题之后,我没有直接写代码,而是先列了一个验收清单。列清单这件事,是我这次作业做得最对的决定之一。因为如果没有清单,我很容易写着写着就开始纠结字体粗细,然后忘了整体结构。
我的自定验收标准如下:
- HTML 语义化,不允许出现一坨没意义的
div嵌套; - 移动端和桌面端都能正常阅读和操作,不出现横向滚动条;
- 至少三种交互,且交互之间有区分度(菜单、懒加载图片、表单校验);
- 页面在手机实际打开时体感流畅,不卡顿;
- 代码可读性好,类名有规律,CSS 变量统一管理颜色和间距;
- 用 Lighthouse 跑一遍,移动端性能分不低于 90。
这个清单让我整个开发过程都有方向感。每完成一项,我就在本子上打个勾。最后验收时,被打回的几个同学几乎都是因为"没有自测"或"自测不全面",所以这一条真的很值得参考。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术方案选型:为什么我用原生三件套而不是框架
2.1 原生 JavaScript 够不够用:训练营作业的正确姿势
说实话,我当时也纠结过要不要顺手引一个 Vue 的 CDN 进来。后来我想明白一件事:作业的目的是检验我当前的真实水平,而不是展示我会调用多少现成工具。如果用了框架,交互看起来确实炫酷,但一旦某个环节出问题,我可能根本不知道它内部发生了什么。
原生 JavaScript 的优势在这个项目里非常明显:
- 项目规模小,需要操作 DOM 的点也就那么几个(菜单开关、图片进入视口、表单提交),完全不需要引入响应式框架的复杂度;
- 原生事件绑定和 DOM API 本身就是基本功,作业阶段用原生能帮你把
querySelector、addEventListener、classList这些 API 用熟,后面学框架时才知道框架帮你省了什么; - 减少依赖,意味着减少莫名其妙的报错。我在调试那次作业时,就见过同学因为版本问题折腾半天,最后发现根本和作业内容无关。
所以我给所有卡在选择阶段的朋友一个建议:如果你的页面状态不多、组件不复杂、DOM 更新逻辑很明确,那就用原生三件套。 框架是工具,不是目的;学习阶段用原生把原理搞清楚,比表面上的"效率"重要得多。
2.2 布局方案:Flex 和 Grid 的分工思路
再聊布局。第四次作业的页面结构是:顶部导航 + 产品 Hero 区 + 功能介绍区(三张卡片)+ 预约表单区 + 页脚。这种结构非常适合 Grid 和 Flex 配合使用。
我当时的分配原则是这么定的:
- 页面主体框架(大骨架)用 Grid。比如 Hero 区左右分栏、功能介绍区三列、产品展示区的两列画廊,这些属于"页面级"的排布,Grid 的
grid-template-columns写出来一目了然,而且响应式断点只需要改一两个属性就能完成重排。 - 组件内部(小细节)用 Flex。比如导航栏里 logo 和菜单按钮的左右对齐、卡片内部图文的上下排列、表单标签和输入框的对齐,这种一对一的排列关系用 Flex 最自然。
我见过很多同学只学了一种布局方式就到处用,最后要么是 Grid 硬撑小卡片里的文字排列,要么是 Flex 嵌套五六层,代码看的人头大。正确思路是:能用 Grid 管"行和列"的地方,就不要用 Flex 去挤;能用 Flex 管"一排排列"的地方,就不要用 Grid 去套。
2.3 交互范围的收敛:四种交互怎么选、怎么搭
任务书说"至少三种交互",但我一开始罗列了很多想法:轮播图、回到顶部、夜间模式、滚动进度条、下拉筛选、弹窗客服……后来发现如果全做,我可能一周都写不完,而且交互之间互相抢关注度,反而让用户不知道重点在哪里。
最后我收敛出四种交互,选它们的逻辑是:
- 移动端导航菜单开关:这是"刚需"交互,小屏下必须有,否则用户根本没法导航;
- 图片懒加载:展示区图片多,懒加载能明显提升性能,而且在移动端特别实用;
- 表单校验与提交反馈:预约区是整个页面的转化目标,校验交互直接影响用户能不能完成操作;
- 锚点平滑滚动:页面有导航,用户点导航时如果直接跳转,体感很生硬,平滑滚动成本低、效果好。
收窄交互范围之后,整个项目的复杂度一下就变得可控了。这件事给我的启发是:交互不是越多越好,而是越贴切越好。 一个产品页,最终目标是让用户了解产品并预约体验,那么所有交互都应该服务于这个目标,而不是为了展示技术。
3. 实操过程:从空白文件夹到完整页面的实现记录
3.1 项目文件结构与命名规范
动手之前先建目录。这个步骤看着简单,但很多同学不重视,直接在一个文件夹里堆 index.html、style.css、script.js 三个文件就完事。我的习惯是分目录管理:
text复制fourth-homework/
├── index.html
├── css/
│ └── style.css
├── js/
│ └── main.js
└── images/
├── hero.jpg
├── product-1.jpg
├── product-2.jpg
└── product-3.jpg
类名命名我用了 BEM 的简化版思路:块名 + 元素名 + 修饰符。比如导航栏的类名是 nav,里面的容器是 nav__container,菜单按钮在移动端显示是 nav__toggle,打开状态加一个 nav__toggle--open。
之所以这么命名,不是因为 BEM 时尚,而是因为当你 CSS 写到 300 行以上时,类名是否规范直接决定你能不能在五分钟内找到要改的样式。第四次作业的代码量大概在 800 行左右,如果不规范,连自己都会迷路。
3.2 HTML 语义化:分清楚 section、article、aside
接下来写 HTML。这是我觉得这次作业对"结构"要求最高的一步。第三次作业我几乎没怎么考虑语义,满屏都是 div,第四次我特意去查了文档、看了社区里的讨论,把每个标签的用途理清楚了。
页面结构大概是这样的骨架:
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智家生活 - 智能家居产品展示</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header class="nav">
<div class="nav__container">
<a href="#" class="nav__logo">智家生活</a>
<button class="nav__toggle" aria-expanded="false" aria-label="打开菜单">
<span class="nav__line"></span>
<span class="nav__line"></span>
<span class="nav__line"></span>
</button>
<ul class="nav__menu" id="navMenu">
<li><a href="#hero">首页</a></li>
<li><a href="#products">产品</a></li>
<li><a href="#features">功能</a></li>
<li><a href="#booking">预约体验</a></li>
</ul>
</div>
</header>
<main>
<section class="hero" id="hero">
<div class="hero__content">
<h1>让家学会思考</h1>
<p>从灯光到安防,一键管理你的智能生活。</p>
<a href="#booking" class="btn btn--primary">免费预约体验</a>
</div>
<div class="hero__image">
<img src="images/hero.jpg" alt="智能家居控制台" />
</div>
</section>
<section class="products" id="products">
<h2>热门产品</h2>
<div class="products__grid">
<article class="product-card">
<img data-src="images/product-1.jpg" alt="智能灯泡" class="product-card__image lazy">
<h3>全彩智能灯泡</h3>
<p>1600 万色,支持语音控制与定时调节。</p>
</article>
<article class="product-card">
<img data-src="images/product-2.jpg" alt="智能门锁" class="product-card__image lazy">
<h3>智能指纹门锁</h3>
<p>指纹、密码、NFC 三种开锁方式。</p>
</article>
<article class="product-card">
<img data-src="images/product-3.jpg" alt="智能音箱" class="product-card__image lazy">
<h3>智能音箱</h3>
<p>环绕声场,覆盖全屋的语音助手。</p>
</article>
</div>
</section>
<section class="features" id="features">
<h2>核心功能</h2>
<div class="features__grid">
<div class="feature">
<h3>智能场景</h3>
<p>一键切换回家、离家、睡眠模式。</p>
</div>
<div class="feature">
<h3>能耗管理</h3>
<p>实时查看电量统计,自动优化能耗。</p>
</div>
<div class="feature">
<h3>远程控制</h3>
<p>不管在哪,都能随时查看家中状态。</p>
</div>
</div>
</section>
<section class="booking" id="booking">
<h2>预约线下体验</h2>
<form class="booking__form" id="bookingForm" novalidate>
<div class="form-group">
<label for="name">姓名</label>
<input type="text" id="name" name="name" required placeholder="请输入姓名">
<span class="form-group__error" aria-live="polite"></span>
</div>
<div class="form-group">
<label for="phone">手机号</label>
<input type="tel" id="phone" name="phone" required placeholder="请输入手机号">
<span class="form-group__error" aria-live="polite"></span>
</div>
<div class="form-group">
<label for="city">体验城市</label>
<select id="city" name="city" required>
<option value="">请选择城市</option>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
<option value="guangzhou">广州</option>
</select>
<span class="form-group__error" aria-live="polite"></span>
</div>
<button type="submit" class="btn btn--primary">提交预约</button>
<p class="booking__message" aria-live="polite"></p>
</form>
</section>
</main>
<footer class="footer">
<p>© 2024 智家生活 · 第四次作业</p>
</footer>
<script src="js/main.js"></script>
</body>
</html>
这里值得特别说的是 aria-live="polite" 和 aria-expanded="false" 这两个属性。作业交上去后老师批注里专门提了,说这是加分点。我当时也不太懂无障碍,但是查资料时看到有一篇讲屏幕阅读器的文章,提到"表单错误提示如果不加 aria-live,屏幕阅读器用户可能完全不知道发生了什么"。加上之后,虽然后台页面本身没任何可见差异,但对使用辅助技术的用户来说,体验是完全不同的。
还有一个小细节:图片的 alt 属性。第三次作业我基本都写 "",因为图片是装饰性的;但产品区的图片不是装饰,必须写清楚图片内容,方便读屏用户了解产品信息。这也是我在那次作业里意识到的:语义化和无障碍不是加分项,而是合格开发者的基本素养。
3.3 CSS 关键实现:变量、断点、动效细节
CSS 方面,这次作业我学到最多的不是属性,而是一种"组织感"。用了 CSS 变量做统一管理之后,再也不想回到到处硬编码颜色的写法。
我定义了如下基础变量:
css复制:root {
--color-primary: #2B6CB0;
--color-primary-dark: #1A4A7A;
--color-accent: #F6AD55;
--color-text: #333333;
--color-text-light: #666666;
--color-bg: #F7FAFC;
--color-white: #FFFFFF;
--space-sm: 0.75rem;
--space-md: 1.5rem;
--space-lg: 3rem;
--radius: 8px;
--shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
--transition: 0.25s ease;
}
这样做的好处非常直接:后面想换主题色,只需要改一个地方;想统一调整卡片间距,改 --space-md 就能全局生效。作业阶段你可能觉得这个习惯无所谓,但等协作项目里设计稿改个主色,你就知道变量有多香了。
响应式布局方面,我采用移动端优先的写法。首先,移动端基础样式先写一遍:
css复制/* 移动端基础样式 */
.products__grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-md);
padding: 0 var(--space-md);
}
.nav__menu {
display: none;
position: absolute;
top: 100%;
left: 0;
width: 100%;
background: var(--color-white);
box-shadow: var(--shadow);
}
.nav__menu.is-open {
display: block;
}
.nav__toggle {
display: flex;
flex-direction: column;
justify-content: center;
gap: 5px;
width: 40px;
height: 40px;
background: transparent;
border: none;
cursor: pointer;
z-index: 10;
}
.nav__line {
width: 24px;
height: 2px;
background: var(--color-text);
transition: transform var(--transition);
}
这里不要急着写桌面端。先把移动端的布局、字号、间距确定好,再通过媒体查询逐渐扩展到更大屏幕:
css复制/* 平板及以上 */
@media (min-width: 768px) {
.nav__toggle {
display: none;
}
.nav__menu {
display: flex;
position: static;
width: auto;
background: transparent;
box-shadow: none;
}
.products__grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* 桌面端 */
@media (min-width: 1024px) {
.products__grid {
grid-template-columns: repeat(3, 1fr);
}
}
看到没,同一个类名的样式在不同断点下被覆盖,但覆盖的只是特定属性。这种写法的好处是:移动端看到的永远是"默认值",桌面端是"增强值",思路非常清晰。
关于动效,这次我特别克制,只用了一种:transition。而且我遵循一个原则:不直接对 left、top、width、height 做动画,而是优先用 transform 和 opacity。原因很简单,transform 和 opacity 可以由 GPU 加速,动画性能好;而 left/width 等属性会触发布局计算,容易造成页面卡顿。这是我在一篇讲渲染性能的文章里看到的,后来实测在低端安卓机上差别确实很明显。
3.4 JavaScript 交互实现:菜单、懒加载、表单校验与平滑滚动
这次作业最有挑战性、也最让我学到东西的是 JavaScript 部分。我把四个交互的代码核心逻辑都梳理一遍。
移动端菜单开关
javascript复制const toggleBtn = document.querySelector('.nav__toggle');
const navMenu = document.getElementById('navMenu');
toggleBtn.addEventListener('click', function () {
const isOpen = navMenu.classList.toggle('is-open');
toggleBtn.setAttribute('aria-expanded', isOpen);
});
这里我踩了一个坑:最开始我用 if (navMenu.style.display === 'block') 来判断开合状态,结果刷新页面后状态经常不对。后来改成用 classList.toggle,状态就完全交给 CSS 去管理,JavaScript 只负责切换类名,代码量少了一半,逻辑也清晰了。
aria-expanded 不是摆设,它告诉辅助技术当前菜单是展开还是收起。我建议每次都写这一行,习惯养成了后面写任何可展开组件都不容易漏。
图片懒加载
javascript复制const lazyImages = document.querySelectorAll('.lazy');
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(function (entries, self) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('loaded');
self.unobserve(img);
}
});
}, {
rootMargin: '200px 0px',
});
lazyImages.forEach(function (img) {
observer.observe(img);
});
}
skeleton 骨架屏之类的高级操作我当时还没做,这里只是一个很基础的懒加载。但要注意两点:
- 图片的
src一开始不设置,只把真实地址放在data-src里,这样浏览器不会在页面解析时请求图片; rootMargin: '200px'意味着图片还没进入视口前 200 像素就开始加载了,体感上不会出现"滚到图片位置了还在 loading"的尴尬。
表单校验
这个部分写起来最琐碎,但也是我学到最多的。
javascript复制const form = document.getElementById('bookingForm');
form.addEventListener('submit', function (e) {
e.preventDefault();
let valid = true;
const name = document.getElementById('name');
const phone = document.getElementById('phone');
const city = document.getElementById('city');
clearErrors();
if (name.value.trim().length < 2) {
showError(name, '请输入至少 2 个字符的姓名');
valid = false;
}
if (!/^1[3-9]\d{9}$/.test(phone.value.trim())) {
showError(phone, '请输入正确的 11 位手机号');
valid = false;
}
if (!city.value) {
showError(city, '请选择体验城市');
valid = false;
}
if (valid) {
document.querySelector('.booking__message').textContent = '预约成功,我们将尽快与您联系!';
form.reset();
}
});
function showError(input, message) {
const errorEl = input.parentElement.querySelector('.form-group__error');
errorEl.textContent = message;
input.classList.add('form-group__input--invalid');
}
function clearErrors() {
document.querySelectorAll('.form-group__error').forEach(function (el) {
el.textContent = '';
});
document.querySelectorAll('.form-group__input--invalid').forEach(function (el) {
el.classList.remove('form-group__input--invalid');
});
}
表单校验最容易被忽略的一件事是:正则表达式要根据实际业务来写。手机号校验的正则 /^1[3-9]\d{9}$/ 是我查过国内手机号规则后写的,基本能覆盖目前主流的号码段。如果只是随便写一个 .{11},那校验形同虚设。
锚点平滑滚动
javascript复制document.querySelectorAll('a[href^="#"]').forEach(function (anchor) {
anchor.addEventListener('click', function (e) {
const targetId = this.getAttribute('href');
if (targetId === '#') return;
const target = document.querySelector(targetId);
if (target) {
e.preventDefault();
target.scrollIntoView({ behavior: 'smooth' });
}
});
});
这里其实可以直接用 CSS scroll-behavior: smooth 实现,我后来又加了一段 JavaScript 是为了在菜单打开时点击链接能先关掉菜单。你在实现的时候,如果只是想要平滑效果,加一行 html { scroll-behavior: smooth; } 就足够了,不需要引入 JavaScript。
3.5 提交前的自测与优化
代码写完不等于作业完成。我给自己定了一个自测流程,也是从这次开始养成的习惯:
- 用 Chrome 的 DevTools 设备模拟器,把 iPhone SE、iPhone 14 Pro、iPad、普通笔记本这几种尺寸各过一遍;
- 用真机(我借了室友的安卓机)打开页面,看看手势操作、滚动体感、文字大小是否正常;
- 跑一遍性能检测(Lighthouse),看有哪些可优化的资源;
- 用键盘 Tab 键把所有可交互元素过一遍,确认焦点可见、顺序合理。
测试过程中发现的最大问题是:Hero 区的大图在手机上加载很慢。后来我把图片压到了 200KB 以内,并设置了 width 和 height 属性避免布局偏移,Lighthouse 的分数才上去。
4. 调试现场:这周踩过的坑与排查记录
4.1 响应式布局的典型问题
图片撑破容器是我这次踩到最典型的坑。产品展示区的三张图片,我在电脑上看都正常,一换到手机浏览器,页面就出现了横向滚动条。
排查过程是这样的:我先把浏览器的宽度拉到 375px,发现滚动条还在,于是用 DevTools 逐个元素查找宽度超过视口的元素。最后发现是产品图片的 img 没有设置 max-width: 100%。图片的原始尺寸是 800×600,在窄屏上直接就把容器撑破了。
解决办法非常简单:
css复制img {
max-width: 100%;
height: auto;
}
这个全局规则是前端基础中的基础,但我写完之后才意识到,之前我从来没有主动给 img 写过这个样式。建议你在项目初期就把全局样式文件里加上这条。
另一个是断点"跳变"问题。我最初只在 1024px 设了一个断点,于是平板横屏 1024px 以下时,导航栏的菜单按钮出现,但 Hero 区的两栏布局还没有切换成单栏,导致中间有一段宽度下文字被挤得很窄、图片几乎看不到。
后来我调整成两个断点:768px 以下全单栏、768px 到 1023px 平板两栏、1024px 以上桌面三栏。这么改完,页面在任何宽度下都没有"卡在中间"的尴尬状态了。
4.2 移动端交互体验的几个细节
移动端交互的坑比样式还多。我印象最深的是:iPhone 上点击表单里 select 下拉选择框时,页面偶尔会白屏闪一下。排查后确定是 iOS Safari 对 select 焦点样式的处理问题,解决办法是给表单控件加一行:
css复制select {
-webkit-appearance: none;
appearance: none;
}
去掉系统默认外观后,下拉框的渲染完全由自己的样式接管,闪屏问题就消失了。
另一个细节是输入框 font-size 小于 16px 时,iOS Safari 会在输入时自动放大页面。我一开始以为是自己代码 bug,查了才知道这是 iOS 的默认行为。解决方法就是把 input 的 font-size 设为 16px 或更大。这个问题很隐蔽,如果你不拿真机测试,几乎不可能发现。
4.3 性能优化与无障碍自查
性能方面,第一次跑 Lighthouse 时我的移动端性能分只有 61,主要扣分项是"图片体积过大"和"未设置图片尺寸"。压缩图片、加 loading="lazy" 属性后,分数提升到 92。后来又做了一件事:把三张产品图的格式从 JPEG 换成了 WebP,文件体积进一步缩小,第二次检测性能分稳定在 96 左右。
无障碍方面的自查也发现过问题:比如导航菜单在移动端打开后,菜单项的背景和文字对比度不足(白底配浅灰色文字),对比度只有 2.8:1,远低于 WCAG AA 标准的 4.5:1。后来把文字颜色改成深灰色,对比度到了 7.6:1 才放心。这个点看起来小,但对于低视力用户来说影响非常大。
5. 复盘:第四次作业真正教会我的东西
这次作业做完之后,我最大的收获不是那几个交互写法的知识,而是一种认知上的转变:"写完"和"做对"之间有巨大的距离。
前三次作业,我基本处于"写出来就万事大吉"的状态。第四次作业因为有了自测清单、有了移动端真机测试、有了 Lighthouse 跑分,我第一次体验到什么是"以用户视角去审视自己做的东西"。比如,为什么图片要懒加载?为什么对比度要达标?为什么 aria-live 要加在错误提示上?这些问题不再是抽象的理论,而是在我亲手操作页面时实际感受到的"卡"和"看不清"。
如果再让我把这个作业做一遍,我会在以下三方面做得更好:
第一,先写完整的 HTML 再写样式。这次我有一点边写 HTML 边调 CSS 的毛病,导致后来结构微调时,样式里出现了多处重复覆盖,代码可读性受损。如果一开始就把结构定清楚,后面的样式会是另一番模样。
第二,交互状态要更完整。比如菜单按钮除了 aria-expanded,我还应该考虑点击菜单项后自动关闭菜单的效果。这个我后来加了,但一开始漏掉了。
第三,要多用 CSS 实现效果,少用 JavaScript。锚点平滑滚动完全可以用 scroll-behavior: smooth 解决,不需要写 JS;图片懒加载甚至可以直接用 HTML 的 loading="lazy" 属性。代码越少,出 bug 的概率越低。
交作业的那天晚上,我又把代码从头到尾读了一遍,顺便给每个文件都加了注释。我突然觉得,一个页面是不是用心做的,从代码的整洁程度上就能看出来。老师最后给我的批语是"结构清晰,交互完整,注意移动端的焦点样式",前两句是肯定,后面那句是新的作业方向。
那次作业之后,我养成了一个习惯:每次写完代码,都会以"一个月后的自己"的角度去读一遍代码,想想如果那时候要改这个页面,能不能立刻看懂?这个习惯一直留到了现在,帮我规避了很多后来的麻烦。
第四次作业只是一个阶段的句号。但如果你现在也正卡在某次作业或某个项目上,我想说的是:别怕慢,别怕做不好,把"作业"当成一个可以反复打磨的东西,你收获的会远远超过一次提交的分值。
