第二次作业,在web前端这条路上是一个特别微妙的分水岭。第一次作业大家基本都在“复刻”一个静态页面,标签会用、颜色会调,就算过关了。可到了第二次作业,页面开始变长、模块开始变多、导航要能跳、表单要有反馈,很多人就是从这个阶段开始掉队的。这篇东西不是给你抄答案的,而是帮你把“第二次作业”这五个字拆开来看——它到底在考核什么、为什么你的页面总是跟设计稿差那么一点、以及那些在你提交前最容易踩进去的坑。无论你是在校学生、培训机构学员,还是准备冲一把web前端开发技能大赛试题的选手,这套基本功都值得你从头捋一遍。
1. 第二次作业到底在练什么
1.1 为什么说这次作业是分水岭
第一次作业的典型题目是“做一个个人介绍页”或者“仿写一个静态首页”,结构上基本是一张长图从上往下摆,顶多中间加个背景色。这时候你写HTML,本质是在练“标签记忆”——h1是标题、p是段落、img是图片,写对了就赢了。
第二次作业不一样。页面通常会被拆成多个区块:顶部导航、横幅区、内容卡片列表、表单、底部信息。这些区块之间要排布整齐、间距统一、滚动时层次分明。这逼着你从“认识标签”进化到“组织结构”,本质是让你建立三种核心能力:语义化结构能力、布局控制能力、基础交互能力。特别提一句,这三项正是各类技能竞赛“web前端开发”赛项里反复出现的考核维度——它不是老师随口布置的,而是在模拟真实前端开发的日常形态。
1.2 拆开看看:作业需求里的三类常规诉求
你拿到的作业描述,翻来覆去大概率逃不出这三类:
| 模块 | 典型内容 | 背后技术点 |
|---|---|---|
| 导航区块 | Logo + 菜单 + 按钮 | Flex布局、垂直居中、hover反馈 |
| 内容区块 | 卡片列表、图文混排、栅格 | 盒模型、flex-wrap、gap间距 |
| 表单区块 | 输入框、选择、提交按钮 | 表单控件、事件监听、校验反馈 |
这三种形态几乎覆盖了日常网页中80%以上的界面模式。你如果能在第二次作业里把这三块做得干净利落,后面学什么框架都会轻松得多——因为框架解决的是“怎么少写代码”,但结构拆解和样式组织,永远是自己的内功。
1.3 技术选型:这次作业别急着上框架
我见过不少学生,第二次作业就直接引Bootstrap或者Element UI,拖几个现成组件进去,效果图看起来确实高级。但我必须泼一盆冷水:这个阶段用框架,等于把这次作业最重要的训练目标给丢了。你把别人的CSS一套,确实很快能拼出一个好看的页面,可一旦要你改布局、调间距、适配某个特殊尺寸,你就会发现无从下手,因为你根本没看到底层是怎么工作的。
第二次作业的意图,是让你亲手完成页面“从零到一”的搭建:自己写reset、自己算间距、自己控制换行。哪怕你最后实现的视觉效果不如框架精致,这段亲手踩坑的过程才是真正长本事的地方。框架、组件库、CSS预处理器,都留到下一次作业再去碰。
2. 拿到设计稿,先把结构拆明白
2.1 从“看图”到“建骨架”的拆块思路
很多同学上手就写代码,结果写一半发现结构和样式拧巴在一起,改一处崩三处。我的经验是:动手之前先拿纸笔把设计稿拆一遍。找大块、找重复项、找层级关系——哪个区域是全页只出现一次的?哪个卡片结构是重复了很多遍的?重复的做成组件化结构,只出现一次的先定好容器。
比如你拿到一个带导航、横幅、三张卡片、一个表单的页面。
结构应该是:
<header class="site-header"> <div class="container nav-wrap"> <h1 class="logo">标志</h1> <nav class="main-nav"> <ul> <li><a href="#intro">介绍</a></li> <li><a href="#cards">服务</a></li> <li><a href="#contact">联系</a></li> </ul> </nav> </div> </header> <main> <section class="banner">...</section> <section id="cards" class="cards-section">...</section> <section id="contact" class="contact-section">...</section> </main> <footer class="site-footer">...</footer>这里用header、nav、main、section、footer这种语义化标签,不只是为了“规范”,而是让浏览器、屏幕阅读器和搜索引擎能直接理解页面结构。技能大赛的评分细则里,语义化标签也经常是明确的加分项或扣分项。
2.2 class命名别乱来,后期改样式全靠它
第二次作业的代码量,还没大到逼你去学BEM方法论,但你至少应该建立一套固定的命名习惯。
一个很实用的原则是“块名-元素名--状态”:比如卡片区域里,外层卡片叫card,卡片的标题就叫card__title,被选中的状态就叫card__title--active。这样做的好处很直接:当你回头改样式时,看到class名就能立刻定位到页面里的哪个模块、哪个元素。
反面案例我也见过不少:用“.div1”、“.box2”、“.a3”这种没有语义的命名,写作业的时候勉强能跑,隔两天再看,自己都认不出哪个是哪个。作业可以交,但代码习惯一旦养歪,后面项目大了根本没法维护。
2.3 全局样式第一刀:reset与盒模型
浏览器默认样式一直是布局错乱的第一大来源:ul自带padding、h1自带margin、body默认8px边距。所以在写任何布局之前,都要先做一步“清零”。
* { margin: 0; padding: 0; box-sizing: border-box; }这条路只要跑通,你的尺寸焦虑能少掉一半。 :root里设置字体基线,也是一个职业习惯:
:root { --primary-color: #2c6dfe; --text-main: #1a1a1a; --text-muted: #666; --gap-base: 16px; } html { font-size: 16px; } body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.6; color: var(--text-main); }把颜色、间距、字体都抽成变量,你的第二次作业可能还看不出多大优势,但当你写第三、第四次作业时,改主题色只需要动一个地方。这种习惯越早建立越好。
3. 布局实现的核心细节
3.1 导航栏与垂直居中的一套黄金写法
导航栏是第一道坎。用flex就是最直接的解法:
.nav-wrap { display: flex; align-items: center; justify-content: space-between; max-width: 1200px; margin: 0 auto; padding: 0 24px; } .main-nav ul { display: flex; gap: 24px; list-style: none; }注意几个细节:align-items: center同时解决了logo和菜单的垂直居中对齐;max-width 加上 margin: 0 auto 让导航内容在宽屏时居中,而不是贴着屏幕左右边缘;gap 替代 margin,让菜单项之间保持等距。
很多人喜欢用float做横向排布——不是不能用,而是float本来就是为“文字环绕图片”设计的,拿来做布局你得反复清浮动、算宽度、处理高度塌陷。与其跟这些历史遗留问题缠斗,不如直接用flex一条路走到底。
3.2 flex-wrap与卡片间距:别让内容撑破布局
卡片列表是第二次作业里最容易“看起来还行、一缩放就崩”的部分。
.cards-section { display: flex; flex-wrap: wrap; gap: 24px; } .card { flex: 0 1 calc((100% - 48px) / 3); min-width: 260px; }这里每个值都有讲究:flex: 0 1 表示“不拉伸、可收缩”,基础宽度是计算好的三分之一(减去的48px是三列间两个24px的gap);min-width: 260px 保证在窄屏上卡片不会缩到没法看的程度,一旦小于260px,flex-wrap就会自动把卡片换行到下一行。
这比用百分比纯硬算要稳得多。实际倒腾过的同学应该体会过:百分比布局面对不同屏幕宽度时,经常出现卡片被挤压变形或者间距跑偏的问题。flex-wrap配合gap,效果立刻正常,而且代码量少。
3.3 什么时候改用 Grid 更合理
Flex擅长一维排列,Grid擅长二维排布。第二次作业里,如果遇到比较规整的栅格——比如“一行两列,左边图片右边文字”这种重复结构,用Grid写起来会更利落:
.feature-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; }这比用flex一行一行控制对齐来得直观。但建议你先别急着学花活,flex练熟了再去用grid也不迟。很多炫技式的grid写法,在比赛代码评审里并不加分,反而容易因为对不同浏览器支持度不够,白白丢分。
4. 表单基础交互:第二次作业加一点JS才完整
4.1 为什么要用addEventListener而不是onclick
第二次作业里,最容易拿到“出彩”评价的,是做一个看起来“活”的交互。表单校验是性价比最高的选择:它既有明确输入,又有明确反馈。但写法上有讲究。
很多人一开始会写οnclick="checkForm()",把函数挂到全局。这个写法的问题在于:第一,多个事件绑定会互相覆盖;第二,全局函数污染;第三,内联事件在代码审查里通常被视为不推荐。
简单绕开就行:
const form = document.querySelector('#contact-form'); const username = document.querySelector('#username'); username.addEventListener('blur', function () { validateUsername(); });addEventListener可以给同一个元素挂多个事件监听器,彼此互不干扰,也方便解耦。以后你在框架里写代码,这种事件处理思维也是直接通用的。
4.2 一次表单校验的完整实现思路
拿最常见的“用户名”输入框举例,校验要覆盖三种情况:不能为空、长度大于等于3、只能是字母和数字。
function validateUsername() { const value = username.value.trim(); const errorEl = document.querySelector('#username-error'); if (value === '') { errorEl.textContent = '用户名不能为空'; return false; } if (value.length < 3) { errorEl.textContent = '用户名至少需要3个字符'; return false; } if (!/^[a-zA-Z0-9]+$/.test(value)) { errorEl.textContent = '用户名只能包含字母和数字'; return false; } errorEl.textContent = ''; return true; }这里其实可以做两个层次的反馈:一个是在blur失焦时检查——用户填完离开输入框后立刻提示;一个是在input输入过程中实时反馈——用户边输入边看到状态。实战时,blur和input两种事件可以同时挂。
HTML5原生也有required和pattern属性,可以用来兜底,但原生校验的弹窗样式你控制不了,跨浏览器的表现也千奇百怪。自己写校验提示,才能和页面设计风格统一。
4.3 事件处理里的两个隐藏深坑
一个坑:在form里的button如果不写type="button”,会被当成提交按钮。点击后页面默认执行提交逻辑,你还没看到校验结果,整页就刷新了。这个坑特别隐蔽,因为你在单独打开HTML调试时,form没有action属性,浏览器刷新页面过后一切看起来“正常”,你反而找不到问题在哪。
另一个坑:向页面上展示用户输入内容时,用innerHTML直接拼接会有明显的注入风险。
// 可选方案:当作纯文本插入 errorEl.textContent = '输入不合法';哪怕在客户端页面里,也尽量不用innerHTML去插入用户可控的内容。改成textContent,内容统一按纯文本处理,安全得多。这个习惯在以后的vue/react项目里同样适用——凡是需要渲染用户输入的地方,先把“内容”和“HTML结构”分开,这个意识越早建立越省事。
5. 提交前的自测清单与经典翻车现场
5.1 三个必测动作,帮你提前拦下八成问题
我第一次做类似作业时,习惯只在自己电脑的大屏幕上反复看,觉得“挺完美”,结果一交上去就被打了回来。后来学乖了,每次提交前固定做三件事:
| 测试动作 | 能发现的问题 |
|---|---|
| 把浏览器窗口缩到375px宽度 | 是否有横向滚动条、卡片是否挤爆、导航是否乱掉 |
| 浏览器页面缩放调大到150% | 文字是否溢出容器、按钮是否被压弯 |
| 换一个浏览器打开(Chrome之外至少过一遍Firefox或Edge) | 样式兼容性、字体渲染差异 |
尤其是第一条:页面出现横向滚动条,通常意味着某个容器宽度超过了视口。这时候不要在浏览器里乱猜,打开开发者工具面板,选中元素查看盒模型,就能一眼看出到底是哪个块撑破了页面。
5.2 经典BUG:外边距塌陷
这个坑几乎人人都会遇到,但很多人遇到后只能靠“加个border试试”来瞎蒙。它的原理是:垂直方向上,相邻元素或者父子元素的margin会被合并,取较大的那个值。比如说,你在section里放了一个margin-top: 40px的h2,理论上section顶部应该被推下去40px,实际效果却是h2把section的margin也给带跑了,整体看起来只有那40px的偏移,位置还对不上。
解决方案有三个:
/* 方案一:父级加内边距 */ .parent { padding-top: 1px; } /* 方案二:父级加边框 */ .parent { border-top: 1px solid transparent; } /* 方案三:父级开启BFC */ .parent { overflow: hidden; }大多数情况下,我会推荐优先用padding而不是margin去控制子元素与父容器之间的距离。因为padding不会产生塌陷,直观且易维护。
5.3 经典BUG:图片底部留白和Flex子项溢出
图片底部莫名其妙多出几像素空隙,是另一个高频问题。原因很简单:img默认是inline元素,基线对齐导致底部留白。一行img { display: block; }就根治了。
Flex子项溢出就稍复杂些。常见场景:三个flex子项,本来设置好三等分宽度,结果某个卡片里文字太长,直接把整个布局撑破,出现横向滚动。排查思路是先看子项是否允许收缩:flex-shrink默认值是1,如果被设成了0,那无论容器怎么变,它都不会缩小,溢出几乎是必然的;再看是否有固定宽度属性,比如给子项定了宽度400px,但这个宽度在当前屏宽下根本放不下。
这类问题,我的固定排查套路是:开浏览器开发者工具,逐个选中父容器和子元素,盯着盒模型的高亮区域看——绿色是padding、蓝色是content、橙色是margin。哪一层超出了父级边界,立刻就能锁定。
6. 关于这次作业,我的一点真心话
我自己在带新人时,一直强调一句话:第二次作业最值钱的不是结果,而是你为它熬过的每一个“为什么”。结构写对,远比你用高超的技巧去做一个完美像素的复刻更重要。如果你交上去的页面能在375px宽度下不乱、在150%缩放下不破、去掉样式后依然有清晰的阅读顺序,那这份作业就已经能拿出手了。
做完这次作业之后,如果你还有精力,我建议顺手做两件事:第一,把页面里的导航切换交互改成事件委托的方式处理,感受一下事件冒泡的威力;第二,把表单里的校验逻辑改为一个可复用的函数,传入不同的校验规则,返回对应的错误信息。这两个小改动会把你从“完成作业”推向“写出通用方案”的层级。等你真正开始处理复杂项目时,回看这次作业里写下的每一行代码,你会感谢那个愿意改三遍的自己。