做一个视频社区首页的静态还原听起来像是新手练手的第一课实际上真要抠到能直接交付的完成度里面藏着的东西比想象中多得多。我用 HTML5 加 CSS3 从头撸了一遍一个典型的弹幕视频社区首页没有引入任何框架、没有用组件库、没有一行 JavaScript 负责布局全部靠语义化标签加 Flex/Grid 撑起来。整套东西解决的是一类很具体的问题设计稿给到你手上怎么在两天内把它变成结构干净、能自适应、后期别人接手也不骂人的页面。适合刚学完盒模型想找个像样的项目练手的朋友也适合写了几年业务、但布局全靠框架、想回头把 CSS 基本功重新捋一遍的同行。下面我把整个过程的思考、参数推导、代码写法和踩过的坑完整摊开讲。1. 动手之前为什么值得用纯 HTML5CSS3 还原一个视频站首页1.1 这个项目的真实定位不是临摹是布局能力的体检很多人把还原某网站首页当成临摹作业找张截图对着描能对上就行。我一开始也这么想做完第一版发给朋友看他问了一句你这页面窗口拉到 900px 宽会怎么样我当场愣住。那次之后我才明白这类项目的价值根本不在像不像而在它逼你把三件平时会被框架遮住的事情想明白结构怎么分层、尺寸从哪来、变化时谁先动。视频社区首页恰好是个信息密度极高的页面。它上面有固定导航、搜索框、轮播推荐、分区入口、视频卡片流、排行榜侧栏、页脚横向和纵向的布局模式几乎全覆盖。你在这一个页面上把 Flex 和 Grid 都用一遍基本上等于把日常九成的布局场景过了一遍。比起拿十个简单卡片练手做一个这样的综合页面效率高得多。我这里说的完整版指的是包含这些内容完整的分区结构、可切换的 CSS 变量主题、三档响应式断点、卡片 hover 动效、加载骨架屏动画、以及一份能直接抄的尺寸对照表。没有交互逻辑不做数据请求纯静态。原因很简单加上脚本之后注意力会被逻辑带走而我想练的是布局本身。1.2 技术选型的取舍为什么这次坚决不引入框架用不用框架这个问题我的判断标准一直是这次我要练什么。如果目标是赶工期交付业务那当然用熟悉的那套省下来的时间去做更有价值的事。但这个项目的目标就是补齐基础引入框架等于把要练的部分外包出去了。具体到不用的理由有三条。第一Tailwind 这类原子化方案会让你失去对一个组件到底由哪几条规则构成的整体感知写久了只会拼 class。第二组件库自带的栅格和间距体系是别人嚼过的你照着用不会错但也不会知道为什么是 8px 而不是 10px。第三也是最实际的一点纯手写 CSS 会让你被迫直面层叠、优先级、继承这些平时靠!important糊弄过去的东西而这些东西恰恰是后期维护成本的大头。当然也不是什么都自己造。我把能用原生能力替代的部分全部用原生主题色用 CSS 自定义属性栅格用 Grid等分布局用 Flex动画用 transition 和 keyframes。这些能力现在的浏览器支持度已经非常好了一个不需要考虑老版本浏览器的静态页面完全没必要再引一层抽象。1.3 语义化标签不是洁癖是实打实省事刚开始写 HTML 的时候我也觉得div classheader和header没区别反正样式一样。写到第三个页面的时候我改主意了。语义化标签给我带来的实际收益有两条都很功利。第一条是样式作用域天然收窄。header、nav、main、aside、footer、section、article这些标签在一个页面里的数量是受控的我可以直接写header { ... }而不用担心污染别处选择器层级能压到很浅。用全 div 的方案你就得靠 class 命名去区分命名一乱样式就开始互相打架。第二条是键盘操作的顺序。视频站首页的导航、搜索、卡片列表如果全用 div 加点击事件Tab 键根本走不过去。用a和button写浏览器自动给你焦点顺序和回车触发一行 JS 都不用。我做的这版虽然没交互但结构上已经为后续接逻辑留好了口子。提示语义化标签不要为了用而用。一个纯粹用来做视觉包裹的容器老老实实用 div 就行硬套section反而会让大纲变得莫名其妙。2. 结构先行把设计稿拆成可计算的栅格2.1 从上到下切五块先把边界画出来拿到设计稿我做的第一件事不是开编辑器是拿支笔在打印出来的稿子上画横线。这一步花十分钟能省后面两小时。画完的结果是这样五块区块对应标签是否吸顶高度策略顶部导航栏header nav是position: sticky固定 64px轮播推荐区section否按aspect-ratio撑开分区快捷入口nav否内容撑开两行网格主内容区卡片流 侧栏排行榜mainaside否Grid 两列右侧固定宽页脚footer否内容撑开这个划分的关键在主内容区那一行。卡片流和右侧排行榜是并列的但两者宽度策略完全不同左边要能跟着窗口伸缩右边是固定 300px 不动。这种一浮动一固定的组合用 Grid 的grid-template-columns: 1fr 300px一行搞定用 Flex 就得给左边加flex: 1; min-width: 0;还要处理溢出。所以主区我选 Grid别的区选 Flex各取所长。顺便提醒一句min-width: 0这个细节很多人栽过跟头。Flex 子项默认min-width: auto里面如果有长标题不换行整个容器会被撑破页面上表现为右侧栏被挤出屏幕。这个问题我后面还会细说。2.2 容器宽度和列数这两个数必须推出来而不是拍出来设计稿标注的内容区宽度是 1200px这是很多桌面端站点的常见取值理由是 1440px 屏幕下左右各留 120px 呼吸感1280px 屏幕下也还塞得下。但 1200px 只是个基准真正决定布局的是这个宽度里放几列卡片。我的推导过程是这样的。先定左右内边距 24px因为视频卡片本身有封面图贴边会显得很挤。再定卡片之间的间距 20px这个数比 16px 松、比 24px 紧正好让相邻封面之间有明显分界又不至于松散。然后试列数5 列(1200 − 24×2 − 20×4) ÷ 5 (1200 − 48 − 80) ÷ 5 1072 ÷ 5 214.4px6 列(1200 − 48 − 100) ÷ 6 1052 ÷ 6 ≈175.3px175px 宽的卡片封面按 16:9 算高度只有 98px标题两行之后整个卡片高度不到 160px信息太挤封面上的人物和小字基本糊掉。214px 对应的封面高度是 214.4 × 0.5625 ≈ 120.6px这个尺寸下封面内容能看清标题两行加作者一行刚好排得下。所以定 5 列。定完 5 列所有衍生尺寸都能算出来了参数计算式结果卡片宽度(1200 − 48 − 80) ÷ 5214.4px封面高度214.4 × 9 ÷ 16120.6px卡片总高估120.6 8 20×2 18≈ 186px单屏可见行数视口 800px 高(800 − 64 − 轮播区) ÷ 行高约 2 行这里我要强调一件事这些数字不是让你写死在 CSS 里的。它们的用途是让你心里有底知道在大屏下页面长什么样然后实际代码里用minmax(214px, 1fr)让浏览器自己算。写死宽度是响应式的头号杀手。2.3 建一张变量表把魔法数字全部收口我见过太多项目同一个间距在十个地方出现值分别是 16、16px、1rem、15px改一次要全局搜索。这次我一开始就建了一张变量表所有重复出现的数值只留一个来源。:root { /* 尺寸 */ --container-max: 1400px; --content-width: 1200px; --gutter: 24px; --gap-card: 20px; --radius-sm: 4px; --radius-md: 8px; --radius-lg: 12px; /* 字号 */ --fs-xs: 12px; --fs-sm: 13px; --fs-md: 14px; --fs-lg: 16px; --fs-xl: 20px; /* 主题色 */ --brand: #00a1d6; --brand-hover: #00b5e5; --text-primary: #18191c; --text-secondary: #61666d; --text-muted: #9499a0; --bg-page: #ffffff; --bg-elevated: #f6f7f8; --bg-hover: #f1f2f3; --line: #e3e5e7; }字号阶梯我特意用了 12/13/14/16/20 这套而不是常见的 12/14/16/20/24。原因是在实际的视频卡片里作者名和播放量这类辅助信息用 13px 比 14px 更合适用 12px 又太小中间插一档能少很多这个字到底多大的纠结。这种细节只有真排过版才知道设计稿上通常标得很粗。变量定完之后还有一个动作很关键给颜色也分语义层。--brand是品牌色--text-secondary是次级文字色这些命名是语义的不是--blue或--gray。道理很直接哪天主题换成粉的你只需要修改变量的值不用去改名字代码也不会变成叫 blue 但是粉色的鬼东西。3. 骨架落地HTML5 结构怎么写才不留后患3.1 顶部导航吸顶、层级、和那条不好处理的搜索框导航栏看着简单实际是整页最容易出细节问题的地方。结构上我用header包nav内部再分左中右三组header classsite-header nav classnav-inner aria-label主导航 a classnav-logo href/ span classlogo-mark aria-hiddentrue/span span classlogo-text某弹幕视频社区/span /a ul classnav-links lia href# aria-currentpage首页/a/li lia href#番剧/a/li lia href#直播/a/li lia href#游戏中心/a/li lia href#会员购/a/li /ul form classnav-search rolesearch input typesearch placeholder搜索你感兴趣的内容 aria-label站内搜索 button typesubmit aria-label搜索搜索/button /form /nav /header吸顶用的是position: sticky; top: 0;而不是fixed。这个选择很多人不理解。fixed会把元素从文档流里抽走你必须给下面的内容补一个等高的padding-top一旦导航高度随断点变化那个 padding 就得跟着改很容易忘。sticky则保留原位置只在滚动到阈值时粘住不需要补偿。唯一的坑是sticky要求所有祖先元素不能有overflow: hidden这条一定要记住我第一次做的时候外层包了个overflow: hidden的容器吸顶直接失效查了半小时。搜索框那部分常见的做法是input加button各写各的然后用负 margin 拼起来。我不推荐。更稳的写法是把它们塞进一个display: flex的容器让容器负责圆角和边框input设flex: 1; border: none; outline: none;button定宽。这样输入框和按钮天生就是一体聚焦态只需要在容器上做:focus-within一个选择器解决两个元素的高亮同步问题。.nav-search { display: flex; align-items: center; width: 380px; height: 38px; padding: 0 4px 0 14px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg-elevated); transition: border-color .2s, background-color .2s; } .nav-search:focus-within { border-color: var(--brand); background: var(--bg-page); } .nav-search input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; font-size: var(--fs-md); color: var(--text-primary); } .nav-search button { width: 56px; height: 30px; border: 0; border-radius: 6px; background: var(--brand); color: #fff; font-size: var(--fs-sm); cursor: pointer; }min-width: 0又出现了。搜索框里的 input 是 flex 子项如果不加这一行它的默认最小宽度是内容宽度长 placeholder 或者用户输入很长内容时整个搜索框会被撑开顶破导航。这个问题在移动端尤其明显因为那里搜索框宽度是 100%。3.2 轮播区和推荐区不用 JS 怎么写结构静态页面没有真正能点的轮播但结构要留好。我的做法是外层section内部一个div.carousel-track用display: flex横排三到五张图overflow: hidden只露出一张然后加一个装饰性的指示点列表。这样以后接 JS 时只需要改transform: translateX()就能滑动结构不用动。section classcarousel aria-label推荐内容 div classcarousel-viewport ul classcarousel-track li classcarousel-item a href# img srcbanner-1.webp alt春季新番专题推荐 width1200 height380 loadingeager /a /li li classcarousel-item a href# img srcbanner-2.webp alt原创动画征集活动 width1200 height380 loadinglazy /a /li /ul /div ol classcarousel-dots aria-hiddentrue li classis-active/li li/li li/li /ol /section这里有两个细节值得说。第一个是width和height属性必须写。很多人以为 CSS 里定好宽高就够了其实 HTML 属性是给浏览器在图片加载前预留占位用的。少了这两个属性页面会在图片加载时整体跳动体验极差。这两个值不要求和实际图片一致只要比例对就行剩下的交给 CSS。第二个是loading属性。首屏第一张用eager后面的用lazy。这是个几乎零成本的首屏优化尤其在这种图片密集的页面上效果很明显。分区快捷入口那部分我用nav包一层内部是ul加li每项一个图标加文字。布局用grid-template-columns: repeat(auto-fill, minmax(96px, 1fr))这样无论放多少个分区都能自动换行不用数。3.3 视频卡片DOM 结构决定了后期改需求有多痛卡片是整个页面出现次数最多的组件它的结构一旦定歪后面改起来就是灾难。我见过写成这样的div classcard img srccover.webp div classtitle某某视频的标题/div div classauthor某个UP主/div div classmeta12.3万播放/div /div这种写法的问题是标题和作者没有关联关系封面点不进去整个卡片除了 div 没有语义。我用的是article包a的思路article classvideo-card a classcard-link href# figure classcard-cover img srccover-01.webp alt视频封面某游戏实况 width320 height180 loadinglazy span classcard-duration12:34/span span classcard-stat span classstat-play12.3万/span span classstat-danmu842/span /span /figure h3 classcard-title这个关卡我打了三十遍才过最后一秒差点翻车/h3 /a a classcard-author href# span classauthor-name某个创作者/span /a /article关键点在这封面上的时长、播放量、弹幕数都放在figure里因为它们属于封面这层视觉标题放在a里面因为点标题应该进视频页作者单独放一个a因为点作者应该进个人主页。三个不同的跳转目标就不能共用一个父级链接这是 HTML 不允许的a不能嵌套a。这种结构能撑住后续加收藏按钮稍后再看这类需求只要往合适层级插就行。还有一点h3用在这里是合适的因为它在main里面属于第三层标题页面h1是站点名h2是推荐视频这类区块名。标题层级这事很多人随手写div就完了等到要接无障碍或者做 SEO 才发现全乱套。注意figure默认带margin: 1em 40px一定要在重置样式里清掉否则卡片会莫名其妙缩进。4. 样式实现把 CSS3 的几个能力用在刀刃上4.1 Flexbox 负责一维对齐别拿它干 Grid 的活我对 Flex 和 Grid 的分工有一条明确界线只在一个方向上排布用 Flex两个方向都要控制用 Grid。这条界线让选型变得毫不犹豫。导航栏用 Flex因为它是横向一维排布.nav-inner { display: flex; align-items: center; gap: 24px; height: 64px; max-width: var(--container-max); margin: 0 auto; padding: 0 var(--gutter); } .nav-links { display: flex; gap: 4px; margin-right: auto; } .nav-links a { display: block; padding: 6px 12px; border-radius: var(--radius-sm); color: var(--text-primary); font-size: var(--fs-md); text-decoration: none; transition: background-color .2s, color .2s; } .nav-links a:hover { background: var(--bg-hover); } .nav-links a[aria-currentpage] { color: var(--brand); font-weight: 600; }margin-right: auto这个技巧值得单独讲。导航左中右三块左边 logo 固定宽中间链接组右边搜索框定宽 380px。要让链接组紧贴 logo、搜索框靠右最简单的办法就是给链接组加一个margin-right: auto它会吃掉剩余空间把后面的搜索框推到最右。比给搜索框加margin-left: auto更好因为如果中间内容变多了前者的行为更符合直觉。gap在这里也比以前用margin-right加:last-child { margin-right: 0 }清爽得多现在主流浏览器都支持没有理由不用。4.2 Grid 做卡片流一行 auto-fill 干掉所有断点卡片列表是 Grid 的最佳战场。我前面算出的 214px 只是个参考值实际写法是.video-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(214px, 1fr)); gap: var(--gap-card); }这一行的威力在于它会自动算出当前容器宽度能放几列并且把剩余空间均分给每列。1200px 容器下算出来是 5 列缩到 900px 变成 4 列缩到 600px 变成 2 列全自动一个媒体查询都不用写。这是我最推荐给新手的布局写法比手写一堆断点可靠得多。auto-fill和auto-fit的区别要搞清楚这两个经常被混用。auto-fill会保留空轨道如果容器里只有 3 张卡片但能放 5 列剩下 2 列的位置是空的卡片宽度不变。auto-fit会把空轨道折叠卡片会被拉伸铺满。视频列表我希望卡片保持固定视觉宽度所以选auto-fill。如果做的是只有少量卡片的推荐位、希望它们均匀铺满那就该用auto-fit。主内容区那一层的 Grid 是这样的.main-layout { display: grid; grid-template-columns: 1fr 300px; gap: 32px; max-width: var(--content-width); margin: 0 auto; padding: 0 var(--gutter); align-items: start; }align-items: start这行不加的话右侧栏会被拉伸到和左侧卡片流一样高看起来空空荡荡。加了之后它按内容高度收缩这才是对的。4.3 毛玻璃、渐变、阴影克制是唯一的美德这类项目最容易翻车的地方不是布局是特效堆太多。我给自己定了三条规矩阴影只用两级、渐变只用在遮罩、毛玻璃只用在一处。阴影分两级--shadow-sm用于卡片默认态--shadow-md用于卡片 hover 态。层级越多越乱两级够用。:root { --shadow-sm: 0 1px 2px rgba(0, 0, 0, .04); --shadow-md: 0 8px 24px rgba(0, 0, 0, .10); }注意--shadow-md的偏移和模糊都比--shadow-sm大得多因为它要表达浮起来了。如果只是把透明度加深视觉上不会有层次感看起来就是卡片变脏了。渐变在这个页面里只有一个正经用途封面底部的那层黑色遮罩。因为时长和播放量是白色文字直接压在浅色封面上根本看不清。做法是给figure加一个伪元素.card-cover { position: relative; margin: 0; border-radius: var(--radius-md); overflow: hidden; aspect-ratio: 16 / 9; background: var(--bg-elevated); } .card-cover::after { content: ; position: absolute; inset: 0; background: linear-gradient( to top, rgba(0, 0, 0, .55) 0%, rgba(0, 0, 0, .15) 22%, rgba(0, 0, 0, 0) 45% ); pointer-events: none; }三个色标的位置是调出来的。0% 处 55% 的黑保证底部文字有足够对比度22% 处快速衰减到 15%45% 之后完全没有这样封面中间和上半部分不被污染。如果直接写linear-gradient(to top, rgba(0,0,0,.6), transparent)你会发现过渡太线性中间一大片都发灰。pointer-events: none也是必须的否则遮罩会挡住封面上的点击。毛玻璃我用在了吸顶导航上而且是半透明的.site-header { position: sticky; top: 0; z-index: 100; background: rgba(255, 255, 255, .82); backdrop-filter: saturate(180%) blur(12px); -webkit-backdrop-filter: saturate(180%) blur(12px); border-bottom: 1px solid var(--line); }saturate(180%)配blur(12px)是苹果那套视觉语言里常见的组合前者防止模糊之后颜色发灰后者控制模糊半径。12px 是个平衡点再大就糊成一片再小又没有透的感觉。-webkit-前缀现在还需要留着因为部分环境仍然依赖它。提示backdrop-filter是有性能代价的尤其在滚动时。整页只在导航这一处用如果给每个卡片都加上中低端设备上滚动会明显掉帧。4.4 动效hover 用 transition骨架屏用 keyframes卡片 hover 的效果我改了三版。第一版是transform: scale(1.03)做完发现卡片会互相挤因为scale虽然不改变布局尺寸但视觉上溢出了旁边的卡片被盖住。第二版改成只改阴影又太平淡看不出反馈。第三版是组合方案.video-card { border-radius: var(--radius-md); transition: transform .25s cubic-bezier(.2,.8,.3,1), box-shadow .25s cubic-bezier(.2,.8,.3,1); } .video-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-md); } .card-cover img { display: block; width: 100%; height: 100%; object-fit: cover; transition: transform .35s ease; } .video-card:hover .card-cover img { transform: scale(1.06); }上浮 4px 是轻微的不会影响周围元素封面图内部放大 1.06 倍用overflow: hidden裁掉溢出部分视觉上像镜头推近。两个动效的时长不一样容器用 250ms图片用 350ms制造一点层次错落感比两者同步更有质感。缓动函数我用的是cubic-bezier(.2,.8,.3,1)这条曲线起步快、尾部长是那种弹一下然后稳稳停住的感觉。默认的ease太温和ease-out又太平微交互上这条曲线差别很明显。骨架屏的动画是这次特意加的因为视频站首页加载慢是常态有个占位能显著降低焦虑感.skeleton { position: relative; overflow: hidden; background: var(--bg-elevated); border-radius: var(--radius-md); } .skeleton::after { content: ; position: absolute; inset: 0; transform: translateX(-100%); background: linear-gradient( 90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, .6) 50%, rgba(255, 255, 255, 0) 100% ); animation: shimmer 1.4s infinite; } keyframes shimmer { 100% { transform: translateX(100%); } }用translateX而不是动background-position是因为前者能走 GPU 合成后者会触发重绘。这一个选择在卡片数量多的时候差别能到十几帧。4.5 主题切换CSS 变量 属性选择器零脚本实现纯 CSS 也能做主题切换虽然需要一个checkbox配合。做法是把深色主题的变量写在[data-themedark]下[data-themedark] { --text-primary: #e8eaed; --text-secondary: #a2a7ad; --text-muted: #6b7076; --bg-page: #16181c; --bg-elevated: #1f2226; --bg-hover: #282c31; --line: #2c3035; --shadow-md: 0 8px 24px rgba(0, 0, 0, .45); }然后在页面根元素上切>media (max-width: 1000px) { .main-layout { grid-template-columns: 1fr; } .main-layout aside { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; } } media (max-width: 760px) { :root { --gutter: 16px; --gap-card: 16px; } .nav-links { display: none; } .nav-search { width: auto; flex: 1; } } media (max-width: 520px) { :root { --gap-card: 12px; --fs-md: 13px; } .video-grid { grid-template-columns: repeat(2, 1fr); } .card-title { -webkit-line-clamp: 2; } }注意断点里我优先改的是变量而不是直接覆盖组件样式。这个习惯能让改动集中也避免了断点里改了另一个断点忘了改的问题。5.2 标题截断两行省略的完整写法卡片标题必须截断否则高度不齐整个网格会变得参差不齐。两行省略的标准写法是.card-title { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; font-size: var(--fs-md); line-height: 20px; height: 40px; color: var(--text-primary); word-break: break-all; }几个要点。line-height必须和height配合两行就是line-height × 2这样卡片高度是确定的网格才整齐。word-break: break-all是为了应对超长英文或连续符号不加的话很长的一个单词会把容器撑破。line-clamp: 2是标准属性虽然现在还要靠-webkit-前缀那套生效但两个都写上更保险。5.3 封面自适应的三种写法我为什么选 aspect-ratio封面要保持 16:9有三条路。老办法是padding-top: 56.25%的绝对定位撑高法兼容性最好但结构要多一层。中等办法是给图片写死高度。新办法是aspect-ratio。我选第三种因为结构最干净.card-cover { aspect-ratio: 16 / 9; overflow: hidden; } .card-cover img { width: 100%; height: 100%; object-fit: cover; object-position: center; }object-fit: cover是关键它保证图片按比例铺满容器、多余部分裁掉不会被拉伸变形。object-position: center让裁剪从中心开始通常人物和主体都在中间裁掉边缘最安全。如果封面是横构图但主体偏左可以单独给它写object-position: left center。padding-top那个老办法现在其实没必要了aspect-ratio的支持度已经完全够用代码可读性还高一大截。5.4 移动端的三个隐形坑第一个坑是横向滚动条。页面上经常莫名其妙多出一条横向滚动条原因是某个元素宽度超过了视口。排查方法很简单给*临时加outline: 1px solid red一眼就能看到谁超了。常见元凶是 100vw 的元素100vw 包含滚动条宽度比可视区宽、负 margin、以及没设max-width: 100%的图片。第二个坑是点击区域太小。移动端手指的触控精度远低于鼠标导航项、作者名、时长标签这些如果只有文字那么大很难点中。我的做法是给可点元素统一加padding撑到至少 40px 高同时用position: relative加伪元素扩大热区视觉上不变但实际可点范围变大。第三个坑是字体自动放大。部分移动端浏览器在横屏或者表单聚焦时会自动放大字号导致布局错乱。解决办法是在html上设-webkit-text-size-adjust: 100%;并且输入框的font-size不要小于 16px。注意overflow-x: hidden是治标不治本的做法。它能藏住滚动条但页面依然可以滑动而且position: sticky会因此失效。老老实实找出那个溢出元素修掉才对。6. 踩坑实录与排查速查表6.1 问题速查表下面这张表是我在整个过程中真实遇到并解决的问题按现象、原因、解法三列整理遇到同类问题可以直接对。现象根因解决方式导航吸顶完全失效某层祖先有overflow: hidden找到并移除或改用fixed加等高 padding右侧栏被内容挤出屏幕Flex 子项默认min-width: auto给可伸缩子项加min-width: 0卡片高度参差不齐标题行数不定固定heightline-clamp图片加载时页面抖动没写width/height属性补上属性比例对即可封面比例失真只设了宽或高aspect-ratioobject-fit: cover深色模式下有白块某处颜色写死了十六进制全局查找色值收进变量滚动时掉帧backdrop-filter用太多只保留导航一处卡片间距忽大忽小用了margin而非gap统一改用gap移动端莫名横向滚动元素超出视口临时 outline 定位溢出元素阴影看起来脏只调透明度没调模糊半径增大模糊和偏移拉开层级两行省略在小屏失效height未随字号调整用line-height × 行数计算hover 时卡片互相遮挡用scale放大整体改用translateY上浮这张表我建议存下来尤其前两条是新手最容易卡住而且最难自己查出来的。6.2 几条文档里不会写的实操心得第一个心得关于命名。我早期用 BEM写着写着就变成.video-card__cover__img--hover这种三级嵌套反而更难读。后来我改成两层封顶组件名加一层元素名就够状态用属性选择器表达。比如卡片激活态写[aria-currentpage]而不是.is-active这样状态和 HTML 语义绑定比另起一个 class 更不容易出错。第二个心得关于调试顺序。页面乱了不要一上来就翻代码先在浏览器里把可疑元素的outline打开看盒子在哪、多宽多高。九成的问题看一眼盒子就明白了。我现在的习惯是常驻一段调试样式用一个快捷键切换.debug * { outline: 1px solid rgba(255, 0, 0, .3); }第三个心得关于先粗后细。我第一版是分区块的纯色方块什么内容都不放就把整个页面的五块区域用背景色块摆出来确保大框架在三种屏幕宽度下都是对的。框架定了之后才往里填内容。这个顺序比一上来就精雕细琢某个卡片要快得多因为如果框架要改你之前雕的细节全白费。第四个心得关于图片资源。练习项目千万不要去抓线上的原图一是版权问题二是那些图动辄几百 KB本地打开都卡。用占位图服务或者自己生成几张同比例的纯色图就够了练的是布局不是图片处理。6.3 加载性能上的几个低成本优化虽然是个静态页面但做完之后我还是顺手处理了几处成本极低效果肉眼可见。图片全部换webp同样画质下体积能小一半以上。写img的时候把decodingasync加上让解码不阻塞主线程。首屏的轮播图用fetchpriorityhigh非首屏一律loadinglazy。这几条加完本地用慢速网络模拟打开首屏时间从两秒多降到了一秒出头。CSS 本身不用做特殊处理但我把变量声明放在最前面把关键的首屏样式导航、轮播、卡片栅格骨架放在文件前三分之一逻辑上让浏览器更早拿到这些规则。这个做法在真实网络环境下有微弱但不是零的收益。字体方面我一个都没引外部字体全部用系统字体栈body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, sans-serif; }理由很实在。中文字体文件动辄几 MB为了一个练习项目去加载收益远小于代价。系统字体栈在主流平台上显示效果都够看而且首屏零等待。整套东西做完我最深的体会是布局能力的分水岭不在你会不会用某个属性而在你能不能在动手之前就把尺寸关系想清楚。那些算出来的 214.4px、120.6px、56.25%看起来琐碎但它们是整个页面在任意屏幕宽度下都不散架的底层依据。下次你再拿到一张设计稿先别急着写代码拿支笔横着画五条线把容器宽度和列数推出来剩下的就是体力活了。