
第二周作业听上去很轻巧我却为它熬了三个晚上。倒不是题目本身有多难——用 HTML 和 CSS 做一张个人品牌首页包含导航、介绍、作品展示和联系表单移动端要正常浏览至少做一个 JavaScript 交互效果。就三行字没了。没有设计稿、没有指定宽度、没有交互的详细要求。我在这个行业写了几年业务代码可真正从零搭一个前端页面反倒是最容易手生的地方。这篇既是给这周的自己做个记录也想把课程里散落的语义化标签、Flex/Grid 布局、响应式、事件监听这些知识点用一个真实项目串起来。如果你也在上前端启蒙课或者正憋着做一个练手小站希望这篇能给你省一点绕路的时间。1. 第二周作业长什么样需求拆解比写码更花时间1.1 原始需求只有三行原话大概是这样的用 HTML CSS 制作一个个人品牌首页包含导航、介绍、作品展示和联系表单页面需在移动端可正常浏览至少实现一个 JavaScript 交互效果。第一眼觉得简单第二眼开始犯难。个人品牌首页到底放什么内容移动端正常浏览是指 iPhone 还是安卓宽度按 375 还是 390 算至少一个交互要的是按钮变颜色还是得有状态管理这些问题的答案作业里一个字都没有。我当时的做法先不写代码把这三行需求当做一个产品需求来拆。我拿草稿纸把这页到底要回答什么问题列出来——我是谁、我做过什么、别人怎么联系我。一个能自洽的个人首页绕不开这三件事。1.2 把页面翻译成信息架构我按这个思路把页面拆成了四个区顶部的导航栏、Hero 介绍区、作品展示区、联系表单区再加一个页脚。然后给每个区标注它对应的技术点这样写代码的时候不会漏页面区块要表达的信息准备用到的技术导航栏用户能跳转到哪些区域nav ul li aFlex 布局移动端汉堡按钮Hero 介绍区我是谁、我的定位语唯一的 h1、一句话副标题、头像 img、行动按钮作品展示区我做过的代表项目article 卡片、h3 标题、Grid 自适应网格联系表单区怎么找到我label input textarearequired 校验JS 提交反馈页脚版权与联系方式time、address、社交链接拆完之后我意识到这门课第二周真正想练的其实是把需求翻译成结构的能力而不是把标签背下来。1.3 交作业前先给自己写验收清单这是我后来觉得最值的一步。在动手前我把作业的完成标准写成了下面几条桌面端和 375px 宽的移动端都没有横向滚动条汉堡菜单在移动端可以点开、点链接后能收起点击导航锚点后标题不会被固定导航栏挡住邮箱填错、姓名为空、留言太短时表单都有明确提示页面在离线加载时图片有替代文字不会出现空白大窟窿。有了这份清单后面每做完一步就去对一遍效率比闷头写高很多。2. 页面骨架我用了哪些语义化标签以及为什么2.1 先铺 DOM再谈样式从课程第一周我就被反复提醒先 HTML 后 CSS先结构后美化。这周我忍住了直接开写样式的冲动先把骨架铺出来body header classsite-header a classbrand href#introA Feng/a button classnav-toggle aria-expandedfalse aria-label打开导航菜单菜单/button nav classmain-nav aria-label主导航 ul lia href#intro关于我/a/li lia href#works作品/a/li lia href#contact联系/a/li /ul /nav /header main section idintro h1你好我是 A Feng/h1 p一名正在自学前端、顺便把作业当产品做的开发者。/p img srcassets/images/avatar.jpg altA Feng 的半身照 a href#works classbtn看看我的作品/a /section section idworks aria-labelledbyworks-title h2 idworks-title作品展示/h2 div classworks-grid article h3个人博客/h3 p记录学习笔记和技术踩坑的独立博客。/p /article article h3待办事项工具/h3 p用原生 JavaScript 实现的轻量任务管理小工具。/p /article /div /section section idcontact aria-labelledbycontact-title h2 idcontact-title联系我/h2 form idcontact-form novalidate div label forname姓名/label input typetext idname namename required /div div label foremail邮箱/label input typeemail idemail nameemail required /div div label formessage想说的话/label textarea idmessage namemessage rows4 minlength10 required/textarea /div button typesubmit发送/button /form /section /main footer p© time datetime20252025/time A Feng/p address邮箱a hrefmailto:helloexample.comhelloexample.com/a/address /footer /body这里有一个我后来才完全理解的细节整个页面只能有一个 h1而且它应该放在最能代表页面主题的 Hero 区。品牌名我用的是 a 标签不是 h1——导航栏里再放一个 h1 会让标题层级乱掉读屏用户切到标题列表时会看到一堆同级标题。2.2 每块内容的标签怎么选作品卡片我用了 article而不是 div。原因倒不复杂article 表示一段可以独立成篇的内容每张作品卡片拿到任何地方都能自解释div 只是无语义的分块容器。课程里也提过能表达语义的时候优先用语义标签class 和 id 只是样式钩子。表单部分是我返工最多的地方。一开始我图快只写了 placeholder后来在课程社区的帖子里看到placeholder 在输入框聚焦时会消失也无法替代 label屏幕阅读器根本读不到。于是我把每个字段都补上了 label并让 for 和 input 的 id 一一对应。这个习惯现在看起来简单当时却是我自己踩过才记住的。2.3 被我忽略、最后返工的三个细节第一处锚点。导航链接指向 #works、#contact这个没问题可 section 的 id 我一开始写在内部的一个 div 上链接怎么点都不跳定位了半天才发现 id 放错了元素。第二处时间。页脚版权我直接写了一行2025 A Feng后来改成 time datetime让机器也能读。第三处表单的 novalidate。我一开始没加这个属性浏览器会在点击提交时先跑一轮原生校验但我又自己写了一份校验逻辑两边同时生效提示长得又不一样。最后给 form 加了 novalidate把校验全都收口到 JavaScript 里行为才可控。3. CSS 布局Flex 与 Grid 各管一段别把它俩当二选一3.1 导航用 Flex一维的事别复杂化我见过不少同学把导航也做成 Grid三列两列硬凑结果代码看得人头皮发麻。导航本质上就是一行内容左边品牌右边菜单中间用 space-between 撑开。用 Flex 是把这个一维问题说得最直白的方式.site-header { display: flex; align-items: center; justify-content: space-between; padding: 0 var(--space); } .main-nav ul { display: flex; gap: 2rem; list-style: none; margin: 0; }这里多说一句 gap。以前做间距要靠 margin 再加 first-child 重置现在 Flex 和 Grid 都原生支持 gap代码干净很多兼容性也够用。不要再用负 margin 那套老打法。3.2 作品墙用 Gridauto-fill、auto-fit 与 minmax 的配合作品展示我用 Grid核心就一行.works-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 24px; }这句的意思是按容器宽度自动生成若干列每一列最小 280px有多余空间再均分。375px 的手机上是单列七八百像素的平板自然变两列宽屏变三列完全不用为列数写媒体查询。不过这里有个坑auto-fill 和 auto-fit 长得像行为不一样。auto-fill 在项目较少时会保留空轨道auto-fit 则会把空轨道折叠让已有的卡片尽量铺满整行。我一开始用的 auto-fill两张作品卡片在宽屏下右边空出一大块整个页面显得很空。改成 auto-fit 就好了。3.3 断点、锚点偏移与 CSS 变量断点我最终只保留了一个768px。在这个宽度以下导航收成汉堡按钮以上展开成正常菜单。不要背iPhone 是 375、iPad 是 768这种死数字要看你的内容在哪个宽度开始挤成一团。我一开始设了 1024、768、480 三个断点结果同一个页面在 800px 时已经很难看再到 768px 才切换于是发现断点是内容驱动不是设备驱动。还有一个让我排查了很久的问题导航是 fixed 定位点击锚点跳到 section 时标题会被导航盖住。我一度以为是 JavaScript 写错了后来才发现只要在 CSS 里加一行就行html { scroll-padding-top: 88px; }这个值等于导航栏高度加一点余量。类似的小问题还包括我把主题色反复写死最后改成 CSS 变量统一管理:root { --color-primary: #2563eb; --space: 1.5rem; --radius: 12px; }换主题色只需要改一处变量这种收益在第二周作业里感受还不明显等页面变大就非常值了。4. JavaScript 交互菜单、滚动高亮与表单校验作业要求只做一个交互我做了三个汉堡菜单、滚动高亮、表单校验。原因很直接——前两周还没讲框架能练原生 API 的机会都在这里了。4.1 汉堡菜单button 从哪里来事件绑到哪里去汉堡菜单第一版我写得很随意用一个 div 加 onclick点起来也能开合但被批了一顿。div 不天然支持键盘操作读屏也读不到可访问性上是硬伤。后来改成 button样式上看起来一样但天生可以被 Tab 聚焦、按回车触发。const toggle document.querySelector(.nav-toggle); const nav document.querySelector(.main-nav); toggle.addEventListener(click, () { const expanded toggle.getAttribute(aria-expanded) true; toggle.setAttribute(aria-expanded, String(!expanded)); nav.classList.toggle(is-open); });踩坑记录有一段事件怎么点都只触发一半——菜单开了又立刻关Console 里也没有报错。排查后发现在 index.html 底部把同一个脚本引了两次每个按钮上挂了两份监听点一下等于点两下。把重复引入删掉就好了。这个错误听起来很蠢但本地预览时如果同时开了两个 dev server或者把 script 标签复制粘贴过真的很容易发生。另外就是点击菜单里的链接后菜单没有自动收起。因为 a 的默认跳转和菜单的关闭是两件事得在链接上再补一段关闭逻辑或者委托给 nav 统一监听。4.2 导航高亮三种方案里我为什么选了最笨的滚动高亮我对比了三种做法IntersectionObserver性能好但 rootMargin 调起来很磨人回调触发时机在不同浏览器里有点差异每个 section 外挂 scrollspy 库引入简单但为了两三个锚点引入一个依赖不值scroll 事件 遍历 section 位置代码直白只有十几个元素性能完全够。我选了第三种。核心逻辑是先拿到当前滚动位置再看哪些 section 的顶部已经越过这个位置最后给对应的导航链接加 active 类const sections Array.from(document.querySelectorAll(section[id])); const links Array.from(document.querySelectorAll(.main-nav a)); function setActive() { const pos window.scrollY 90; let current sections[0].id; for (const section of sections) { const top section.getBoundingClientRect().top window.scrollY; if (top pos) current section.id; } links.forEach((link) { const active link.getAttribute(href) # current; link.classList.toggle(active, active); if (active) { link.setAttribute(aria-current, page); } else { link.removeAttribute(aria-current); } }); } window.addEventListener(scroll, () { if (window.ticking) return; window.ticking true; requestAnimationFrame(() { setActive(); window.ticking false; }); }); setActive();这里的 90 是导航栏高度加一点缓冲区让当前 section的判断更符合视觉直觉。用 getBoundingClientRect().top window.scrollY 而不是 offsetTop是为了避免 offsetParent 的坑——我页面里有元素带了 position: relative直接用 offsetTop 会让高亮位置整体偏移这个坑排查了很久。4.3 表单校验别让页面刷新把你的输入带走表单校验的初版特别简单把必填和格式检查交给 HTML 的 required 和 typeemail然后在 submit 事件的回调里判断。但我漏写了 e.preventDefault()点了发送按钮后页面直接刷新所有输入瞬间清空。那一刻的心情凡是经历过的人都懂。后来我把校验收口到 JavaScript用 validity 对象区分不同错误类型const form document.querySelector(#contact-form); form.addEventListener(submit, (e) { e.preventDefault(); const name document.querySelector(#name); const email document.querySelector(#email); const message document.querySelector(#message); let valid true; valid checkField(name, name.validity.valueMissing ? 请填写姓名 : ) valid; valid checkField(email, getEmailError(email)) valid; valid checkField(message, getMessageError(message)) valid; if (valid) { showToast(提交成功稍后回复你); form.reset(); } });getEmailError 和 getMessageError 只是根据 validity 返回中文提示字符串checkField 负责把提示渲染到字段下方并加样式逻辑很简单。我自己比较满意的一点是错误提示不是用 alert 弹窗而是在对应字段下方出现一行小字并且只有用户离开这个字段或者尝试提交之后才显示。这样页面刚加载时不会一大堆红色提示铺脸体验比纯 CSS 的 :invalid 默认行为可控得多。5. 真机调试DevTools 说好的手机全翻车5.1 localhost 不够要和手机连同一 WiFiChrome DevTools 的移动端模拟可以解决大部分宽度问题但解决不了真实触摸、真实浏览器内核和真实输入法带来的问题。我这周最大的进步是学会了用真机调试方法很简单在项目目录跑python -m http.server 8000或者npx serve电脑和手机连同一个 WiFi手机浏览器访问http://电脑局域网IP:8000。不用每次改完代码都推 GitHub Pages 再等两分钟本地热更新直接看效果。真机测一轮回来你会对移动端正常浏览这六个字产生全新的敬畏。5.2 100vh、字体缩放和微信浏览器典型的翻车有三处。第一处是 100vh。我用min-height: 100vh撑 Hero 区桌面端一切正常到了 iPhone 上地址栏一收一放页面底部忽高忽低要么被地址栏顶上去要么留出一截空白。原因是移动端浏览器的可视高度是动态的100vh 不等于用户真正看到的高度。现在的解决方案是.hero { min-height: 100vh; } supports (height: 100dvh) { .hero { min-height: 100dvh; } }第二处是输入框聚焦自动放大。iPhone 上只要输入框字号小于 16px聚焦时就会自动放大页面。我把输入控件统一设成 16px问题消失。第三处是微信内置浏览器。同事帮我打开链接反馈字怎么这么小。后来发现是 iOS 微信 WebView 对 font-size 的调整策略不同在 html 上加一句html { -webkit-text-size-adjust: 100%; }就稳了。这类兼容问题不真机测光看 DevTools 是绝对看不出来的。5.3 部署到 GitHub Pages 后的路径玄学本地一切正常部署到 GitHub Pages 后样式全丢这是每个用 GH Pages 的新手都会遇到的场景。原因多半是路径写成了绝对路径/assets/css/style.css。如果你的仓库地址是用户名.github.io/仓库名/浏览器会把/assets当成域名根目录去请求结果 404。我的做法是统一改成相对路径assets/css/style.css因为页面只有一层相对路径最简单可靠。如果页面嵌套层级变多可以考虑base标签但那又牵扯到锚点行为需要单独测。CSS 文件内部引用图片时也要注意url() 是相对于 CSS 文件本身的位置不是 HTML 页面的位置。我一开始在 style.css 里写了url(../images/avatar.jpg)目录差了一层本地居然没发现部署上去才爆。图省事之前先把目录结构画出来。6. 交完作业的复盘第二周到底在考什么6.1 扫了全班作业后看到的三种通病作业好歹交了我也顺手把班里同学的其他作业翻了个遍。不是闲逛是真想看看自己哪里落后了。看完三道通病浮出来一是标签看着像语义化实际不是。大家都用了 nav、section、article但细看一个页面冒出来两三个 h1或者把标题放进 span 里只是为了让字变大。表面合规和真正合规之间隔着对标签含义的理解。二是移动端能打开不等于能用。很多页面在手机上只是被等比缩小点导航没反应或者横向滚动条拖起来费劲。响应式的本质不是让内容变小而是让布局适应可用空间。三是交互加了但状态不闭环。按钮点了有颜色反馈但放开之后没变化表单提交后无任何提示用户不知道成功没有。这段让我意识到交互要考虑的是反馈和异常场景而不是能不能触发。6.2 给自己补的验收意识对照别人的问题我再核对自己交上去的作业有三点做得不错也有两个明显的凑合一是作品的文案内容是我临时编的比重不大但显得很空二是网页标题用了页面主题词但描述不完善社交分享预览不友好。这些都不影响打分但影响作品感。所以我复盘时给自己订了一条新规矩任何作业交付前先写一份 5 到 10 行的验收清单把可以打开升级成在什么设备上打开、什么操作下不报错、反馈是否清晰。这条规矩听起来像写测试用例但用在个人项目和作业上同样有效因为它逼你把模糊的完成标准变成可验证的条目。6.3 写在最后如果一定要给这周一个总结我的体感是项目里返工最多的时刻往往是动手前脑子里最模糊的时刻。第二周作业放在整条课程线里看练的不是标签和属性的记忆力而是给一个模糊需求能不能自己补齐边界、拆出结构、再逐条验证的能力。这个能力往下第三周的 JavaScript 进阶要用往后真正的项目里更要用。这周的三个晚上花得不亏。