做前端这些年,我见过不少人把Flex布局的文档翻来覆去看了好几遍,真到写页面的时候还是不知道用哪个属性。后来我发现一个特别好用的练手项目:拿一个支付类应用首页当靶子,纯粹用Flexbox把它还原一遍。这个练习在不少前端学习资料里都被称为某支付应用首页布局实战,它的好处在于,首页上那点东西几乎把Flexbox的日常用法全覆盖了:顶部的三段式搜索栏、中间一排排铺开的图标区、可以横向滑动的卡片,以及底部固定的五个Tab。你只要把这几个区域做明白,工作里的绝大多数布局需求也就拿下了。
这篇文章直接把整个练习过程拆开来讲,从动手之前的布局拆解,到每个区域的关键代码,再到我踩过的坑和排查思路。适合刚学完HTML和CSS基础、对Flex的用法还停留在“会水平垂直居中”阶段的人,也适合打算系统梳理一遍Flex的老开发。我不会把文档里的属性定义重新念一遍,而是告诉你每个属性在真实页面上应该怎么用、用了以后会有什么表现,以及出了问题要怎么调。
1. 动手之前先拆解:支付类App首页到底长什么样
写代码之前,先去把目标首页截几张图,然后拿笔在纸上把这个页面分成几个水平区块。这个习惯看起来不起眼,但真的能帮你省掉后面大量改样式的返工时间。我参考的常见版本,整个首页从上到下基本是这样一个结构:最顶上是搜索栏区,紧接着是一块大的图标区,往下翻的时候会依次出现横幅广告、宫格入口、一组组的图文列表卡片,最底端是固定的Tab切换栏。每个区域内部,几乎都是一排元素做等分、居中对齐或者两边顶格。
1.1 从截图中提取布局区块的实操方法
自己拿截图和纸笔走一遍这个流程,比直接看我用文字总结要有用得多。我一般会把截图拉到绘图软件里,用矩形框把每个“能独立看成一坨”的内容框起来。这一步有两个原则:先框大结构再框小结构;一个矩形框里只放排列方向一致的内容。比如搜索栏是一个大框,里面左边的图标、中间的搜索输入框、右边的图标,是沿水平方向排列的一个小结构。到了金刚区,横着看是一行五个图标,竖着看是两行到三行,那它就是一个“先横后纵”的复合结构,得拆成外层容器和内部每个图标两层来看。
框完之后你大概率会发现,整个页面就是无数个“横向一排”叠加起来的。搜索栏是横向一排,金刚区的每一行是横向一排,列表卡片的内部也还是“头像在左、文字在右”的横向一排。这就是为什么这个练习和Flex是天生一对:Flexbox解决的正是一个方向上元素的排列和对齐问题。你先把一排解决明白,然后把一排排纵向叠进页面滚动流里,首页的骨架就出来了。理解到这一层,你后面写代码的时候脑子里始终会有“当前我在处理哪一个方向”这根弦。
1.2 哪些区域适合Flex,哪些不适合
也不是说整个首页都得用flex,有些地方明显更适合别的方案。最典型的就是整个页面的滚动容器,那是一个纵向文档流,默认的block排列就是最佳方案,没必要把body做成flex-column再套一堆东西。再比如某个严格等分的宫格,行列数量固定、每一格尺寸完全一致,用Grid会更省事。但如果你本意就是想系统练一遍Flex,那在宫格区域坚持用flex-wrap来做也完全可行,只要知道取舍在哪里:flex-wrap适合内容数量不确定、希望自动换行的场景,Grid适合希望行列结构稳定、需要对齐“行轨道”和“列轨道”的场景。
举个例子,支付首页顶部的四宫格大按钮,比如“扫一扫、付钱、收钱、乘车码”这类入口,用flex布局,四个按钮flex:1等分宽度,每个按钮内部再用一个纵向flex把图标和文字上下排好。这是一个非常适合练手的组合,因为外层是横排、内层是竖排,两个方向正好把flex的两种flex-direction都用上了。而如果换成Grid,你当然也能做,但那种情况下Grid的优势其实没那么明显,因为这里只有一排元素需要处理,用flex反而更直观。
1.3 为什么不用Float或者绝对定位
老读者可能还记得,flex普及之前,这种首页基本都是用float堆出来的。我自己早年就这么干过,踩过的坑现在还记得很清楚:子元素float之后父容器高度塌陷,得给父容器加clearfix,否则背景和边框都会穿帮;要让一个元素垂直居中,得给它设line-height或者算死margin-top;要实现几等分的多列,哪怕用百分比也得考虑border和padding会不会把宽度撑爆。Flex是一次性解决这些问题的方案:父容器一行display:flex,子项默认就会在交叉轴上被拉伸对齐,垂直居中变成一行代码,水平间距用gap就能控制,父容器也不再需要清浮动。
这个历史包袱不是用来怀旧的,而是想说明为什么这个练习值得做。你在练习里遇到的每一个“flex好像没生效”的瞬间,背后都对应着以前用float和定位方案时非常别扭的一类问题。把flex用顺了,你相当于掌握了一套“以后写布局默认先想flex”的直觉,这会直接改变你的出活速度。
2. 实战要用到的Flex核心概念,按需求讲
Flexbox的一切都是围绕主轴展开的,所以这一节我不按文档顺序讲属性,而是按你写首页时会遇到的真实需求来讲。先分清主轴和交叉轴,再搞懂尺寸分配,接着处理对齐,最后管间距。这样等你到了第三节写代码的时候,每个属性都有具体的落点,不会觉得是在背语法。
2.1 先搞清楚主轴方向,再动手
默认的主轴方向是水平方向,也就是flex-direction:row,元素从左往右排;改成column之后主轴就变成垂直方向,元素从上往下排。很多新手在写纵向排列的时候忘记改direction,导致写了一个flex容器结果里面的div还是横着排,然后对着样式表发呆半天。我自己练这个项目的时候,最常出现的错误也就是这一处。
怎么判断该用row还是column?回到上一节说的“矩形框”:如果这个区域里的元素是横着排的,用row;如果是竖着排的,用column。复杂一些的区域就反过来想,外层用大的方向,内层用和外层垂直的方向。比如金刚区的每个图标,外层要一行五个横排,于是容器用row加flex-wrap;每个图标内部,图标在上、文字在下,于是一个图标自己就是一个column的flex容器。这一横一竖两层的嵌套,几乎就是整个首页所有布局的基本单位,练熟了以后看任何页面都能迅速拆出这两层。
另外有一个值得提的细节:flex-direction:row-reverse和column-reverse会改变元素的视觉顺序,但不会改变HTML里的DOM顺序。这个特性在写一些需要考虑阅读顺序的场景时有用,比如某些设计稿里一行内容是从右往左读的,你可以不动DOM只改一个属性。不过练习首页的阶段用不太上,知道有这么回事就行,面试偶尔会问。
2.2 flex-grow、flex-shrink、flex-basis的分工
如果说主轴方向决定了“往哪排”,那“占多大”就由flex-grow、flex-shrink、flex-basis这三个属性决定。我用一句话概括它们的关系:flex-basis是元素在主轴方向上的初始尺寸,flex-grow决定空间多了的时候怎么瓜分,flex-shrink决定空间不够的时候怎么压缩。绝大多数时候你只需要用缩写flex,比如flex:1,它的完整写法是flex:1 1 0%,意思是“初始尺寸为0,多余空间全部由我吸收,空间不够允许我压缩”。这个写法常被用来实现“中间元素占满剩余宽度”,搜索栏中间那个输入框就是标准例子。
另外一个高频写法是flex:none,等价于flex:0 0 auto,意思是“我的尺寸由内容或width决定,既不增长也不收缩”。练首页时凡是你不希望被压缩的元素,比如头像、图标、右侧箭头,都应该显式给flex:none。我见过太多人写列表卡片时,右侧的小箭头被压缩成一条细线,或者图片被压扁,根因都是忘了这回事。当你明白每个值背后的含义之后,遇到“我的div怎么变小了”这类问题,第一反应就会是查flex-shrink,而不是去改width。
关于flex-basis还有一个容易忽略的细节:它默认值是auto,此时会去取width的值;一旦你设置了flex-basis,它在主轴方向上就优先于width生效。在row方向下,flex-basis:200px和width:200px效果几乎一样,但flex-basis是跟随主轴方向的,如果你把子项改成column方向,flex-basis:200px限制的就是高度了。这个特性在做竖向排版的卡片时偶尔会用到,平时理解成“这是主轴方向的尺寸”就好。
2.3 justify-content、align-items、align-content别记混
这三个对齐属性是flex出错的重灾区。我的记忆方法是:justify-content只管主轴方向的分布,align-items管交叉轴方向上每一行内部的对齐,align-content管多行作为一个整体在交叉轴方向的分布。后两者的区别最容易被忽略,align-content只有在flex-wrap让容器出现多行时才起作用,如果只有一行,你设置align-content是看不到任何效果的。
| 属性 | 作用维度 | 管什么 |
|---|---|---|
| justify-content | 主轴 | 一排内容在主轴方向的分布 |
| align-items | 交叉轴 | 当前行内部子项的交叉轴对齐 |
| align-content | 交叉轴 | 多行整体在交叉轴方向的分布 |
把这个映射到首页实例里就很直观。搜索栏内部的图标和输入框要垂直居中,用的是align-items:center,因为主轴是水平方向,交叉轴是垂直方向,align-items管的就是这个。金刚区的图标文字居中对齐,图标在文字上方,用的是内层flex容器的justify-content:center和align-items:center组合,此时内层主轴是垂直方向,justify-content负责文字和图标在垂直方向上的分布,align-items让它们在水平方向上居中。练完这两个区域,这三个属性的区别基本就刻进肌肉记忆了。
还有个值得写进笔记的小细节:容器开了flex-wrap之后,如果多行之间有明显的间距需求,用align-content:space-between或者gap都能解决,但两者处理的语义不一样。align-content控制的是“行与行之间的空格”,gap控制的则是“子元素之间的空格”,在只有一行时gap依然生效而align-content不会。后面写横向滚动卡片区域时,你会同时用到这两个属性来管理间距,到时候可以对照着看。
2.4 间距用gap还是margin
Flex布局里给子元素之间加间距,现在首选是gap。gap是容器属性,写在父元素上,会自动在子元素之间插入固定的间距,而且不会出现margin那种“相邻两个元素margin合并”或者“最后一个元素末尾多余间距”的问题。以前浏览器不支持gap的时候,大家只能用margin加负margin的花活,或者给最后一个子元素单独去掉margin,代码很不优雅。现在主流浏览器里gap已经相当稳了,直接用就好。
有一点要注意:gap在flex容器中会同时在主轴和交叉轴两个方向生效。如果你只想要水平间距,不想让垂直方向多出空隙,可以分别写row-gap和column-gap。练习金刚区的时候这就很关键:图标区域往往要求列与列之间有间距,行与行之间也要有间距,但两者的值可能不一样,这时写gap:8px 12px这种两值形式,或者单独写row-gap、column-gap,都比在每个子元素上敲margin灵活得多。
3. 分区域实操:从搜索栏到金刚区再到底部Tab
前面讲了那么多原理,这一节全部落到代码上。我按首页从上到下的顺序,把每个区域的做法和关键代码写出来。你照着敲一遍,再把参数改一改,效果很快就能出来。
3.1 搜索栏:三段式的标准做法
支付类首页顶部几乎都有搜索栏,结构是“左侧图标 + 中间搜索条 + 右侧图标”。我写的时候会先铺一个header容器,display:flex,align-items:center,padding留出上下空间,然后让左右两个图标用flex:none占住自己的位置,中间的搜索条用flex:1把剩下的宽度全部吃掉。代码如下:
<header class="header"> <div class="header-icon">≡</div> <div class="header-search"> <span class="search-placeholder">搜索</span> </div> <div class="header-icon">◎</div> </header>.header { display: flex; align-items: center; gap: 10px; padding: 10px 12px; background: #fff; } .header-icon { flex: none; width: 28px; height: 28px; /* 图标样式略 */ } .header-search { flex: 1; display: flex; align-items: center; justify-content: center; height: 32px; border-radius: 16px; background: #f5f5f5; }这段代码里有三个要点值得反复咀嚼。第一个是header用align-items:center而不是给子元素手动加margin-top,这样就算icon尺寸变了,整体依然是垂直居中的。第二个是中间搜索条内部又造了一个flex容器,所以“搜索”两个字可以随心所欲地水平垂直居中,而不会因为padding不一致偏掉。第三个是gap:10px同时管理了三个子元素之间的间距,少写了不少margin。等你把这个搜索栏的代码跑出来,对照截图看,会发现肉眼看起来和原页面非常接近,但代码量比用float的旧写法少了一大截。
3.2 金刚区:flex-wrap的经典场景
接下来是那个几行几列的图标区。我用一个section包住所有图标,display:flex,flex-wrap:wrap,然后每个图标定宽20%。为什么是20%?因为一行放五个,100%除以5等于20%。每个图标内部还是一个纵向flex:上面一个圆形的icon,下面一行文字。核心结构如下:
<section class="grid"> <div class="grid-item"> <div class="icon">扫</div> <span>扫一扫</span> </div> <!-- 重复若干次,共10个 --> </section>.grid { display: flex; flex-wrap: wrap; padding: 12px 0; } .grid-item { flex: 0 0 20%; display: flex; flex-direction: column; align-items: center; margin: 8px 0; } .icon { width: 48px; height: 48px; border-radius: 12px; background: #eee; display: flex; align-items: center; justify-content: center; }这里最值得琢磨的是flex:0 0 20%这个写法。第一项0表示不增长,第二项0表示不收缩,第三项20%表示基础宽度是父容器的20%。这样十个图标必然严格地每行五个,不会因为内容长短被挤得歪歪扭扭。如果你写的是flex:0 0 25%,那就是每行四个的排版。想改列数只要改这个百分比,非常方便。这就是flex-wrap的好处:你不用去数一行放几个然后手动换行,容器满了自动往下排。
每个图标内部我用flex-direction:column把图标和文字摆成了上下的结构,再用align-items:center让它们在水平方向居中。这里有个典型的初学者错误:以为在flex容器里要水平居中就得给子元素写margin:0 auto,其实设置flex容器的justify-content:center和align-items:center就够用了。因为图标和文字这个组合的尺寸不定,用对齐属性比用固定定位活得舒服得多。
3.3 横向滚动卡片区:让卡片保持自己的宽度
支付首页在金刚区下面通常会有一个可以左右滑动的小卡片区,比如横幅广告或者各种消费券。这个区域是练习flex-shrink的好素材。容器依然是display:flex,但加了overflow-x:auto,每个卡片写flex:0 0 240px,意思是每张卡片的宽度固定为240px,绝对不会因为容器放不下而收缩。这样你就能保证用户滑动的每一屏里,卡片宽度稳定、露出半张下一张卡作为视觉提示。
.card-scroll { display: flex; gap: 12px; overflow-x: auto; padding: 12px; scroll-snap-type: x mandatory; } .card { flex: 0 0 240px; height: 96px; border-radius: 8px; background: #f5f5f5; scroll-snap-align: center; }我在这里顺手加了scroll-snap-type和scroll-snap-align,这不是flex的内容,但实操的时候很有用:它能让滑动结束后卡片自动吸附对齐,手感接近原生应用的滚动效果。如果你做的是纯flex练习,这两行可以去掉,不影响布局本质。还有一个细节是容器加了padding之后要注意滚动条的位置,如果不想让滚动条贴着最左边,可以把padding写在容器上,让第一张卡和左边缘之间也有留白。这个区域你能实际体会到gap和flex-basis协同工作的状态。
3.4 图文列表卡片:左固定右自适应的万能结构
这个结构在首页里反复出现,头像在左、标题和描述在中间、箭头在右。它的代码是所有flex布局里复用率最高的,值得写得规范一点。我把外层li设置成display:flex和align-items:center,左边缩略图flex:none,中间内容区flex:1,右箭头flex:none。内容区内部如果还有多行文字,可以再嵌一个column方向的flex,或者直接让标题、描述用block元素上下排。
<section class="list"> <div class="list-item"> <div class="list-thumb"></div> <div class="list-body"> <div class="list-title">账单</div> <div class="list-desc">查看每笔收支明细</div> </div> <div class="list-arrow">›</div> </div> <!-- 其他列表项 --> </section>.list-item { display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: #fff; } .list-thumb { flex: none; width: 44px; height: 44px; border-radius: 8px; } .list-body { flex: 1; min-width: 0; } .list-title { font-size: 16px; line-height: 22px; } .list-desc { font-size: 12px; color: #999; line-height: 18px; } .list-arrow { flex: none; color: #ccc; }注意我给list-body加了min-width:0。这是一个非常关键的细节:flex子项的min-width默认值是auto,也就是说子项的最小宽度不会小于它内容的宽度。如果描述文字很长,内容区会被文字撑开,把右侧箭头挤出容器,或者把整个卡片撑爆。设置min-width:0之后,内容区允许被压缩到比内容更窄,这时内部的文字才会按正常规则换行截断。你在写列表、写标签、写任何“中间自适应+右侧固定”的结构时,这一行都是救命的。
3.5 底部TabBar:五个Tab怎么均分
底部Tab是固定定位的,里面五个Tab要平均占满整个宽度。实现方法很简单:容器display:flex,每个Tab flex:1,配合text-align:center。flex:1会让每个Tab的宽度相同,哪怕文字长度不一样,只要文字使用了单行截断不会把容器撑开,整体就能保持均分。这五个Tab的图标用纵向flex来排列,和金刚区图标的思路完全一样:外层Tab横向均分,内部图标在上、文字在下。
.tabbar { position: fixed; left: 0; right: 0; bottom: 0; display: flex; background: #fff; border-top: 1px solid #eee; padding-bottom: env(safe-area-inset-bottom); } .tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 6px 0; }这里有两个实操中容易忽略的点。一个是底部安全区:现在很多手机屏幕有底部横条和圆角,为了让Tab不贴到最底下,需要加padding-bottom:env(safe-area-inset-bottom)。这个和flex无关,但你不加的话页面在全面屏手机上看会很难受。另一个是fixed定位的Tab会遮挡页面内容,所以最外层的页面容器要预留一个和Tab高度相等的padding-bottom,比如60px,否则最后一块内容会被Tab盖住看不全。这些细节叠加起来,你才能交付一个真正能上真机看的页面。
4. 高频问题与排查实录
这个章节是练习过程中最值钱的部分。我把练这个首页时大家最容易踩的坑整理成了一张速查表,后面每个问题再展开说一下排查思路。
| 现象 | 常见原因 | 排查顺序 |
|---|---|---|
| 子元素被压扁 | flex-shrink默认是1 | 看容器宽度,给子项加flex:none |
| 垂直居中无效 | 用了align-content而非align-items | 确认主轴方向,确认是否多行 |
| 最后一行不齐 | space-between遇到非整行数量 | 定死每行数量或用占位符 |
| 文字撑破容器 | min-width默认为auto | 给内容区加min-width:0 |
| gap没生效 | 版本过老或写错属性名 | 检查浏览器支持,检查是否flex容器 |
4.1 子元素被压扁,不是width的锅
练这个项目时最常见的现象:图片或图标变窄了,明明设置了width:48px。检查点往往不是width,而是flex-shrink。因为flex子项的默认flex-shrink是1,容器空间不足时它会收缩,width只是它的“期望值”,不是“底线值”。解决办法要么给这个元素加flex:none,要么在容器里给整体调整宽度。排查的顺序我建议是:先看容器有没有宽度溢出,再看子项有没有被嵌套flex影响,最后直接给子项加flex:none看效果,基本两分钟就能定位。
4.2 justify-content:center没让内容居中?
这种情况要分两个方向想:如果主轴是row,justify-content:center是水平居中;如果容器里只有一行,你用了align-content:center想垂直居中,大概率没效果,因为align-content管的是多行作为整体的分布,单行时它不干活。正确的垂直居中要看align-items。我见过不少人在这两个属性之间来回试,最后发现是flex-direction的问题。写代码前先默念三遍“主轴、交叉轴、多行”,能少走不少弯路。
4.3 图标排列到最后一行少一个
金刚区十一个图标的情况下,最后一行会只排一个,用justify-content:space-between的时候这个孤零零的图标会跑到最左边,看起来特别别扭。处理手法有几种:一是让每一行的数量是固定的,比如统一按20%宽度排五个,最后多余的元素用占位符补齐;二是改用justify-content:flex-start配合每项固定的margin-right,最后一项的margin可以通过:nth-child或者容器负margin收掉;三是接受视觉上的左对齐。平时练习我推荐第一种,因为最简单,而且能让你顺便思考“如何用空div占位”这种小技巧。
4.4 文字把flex布局撑破
这个必须单独拿出来说:flex子项里的文字默认不会自动换行截断,而是会撑开父容器。遇到“文字太长把右侧图标挤出去了”,给文字所在的那个flex子项加上min-width:0,或者给文字本身加overflow:hidden、text-overflow:ellipsis、white-space:nowrap的组合。首页的标题和描述一般都不会太长,但真实项目里这条经验几乎天天用得上,提前练好不亏。
4.5 1px边框怎么处理才像真的
视觉还原的时候,支付类首页里有很多细边框,如果直接写border:1px solid #eee,在普通屏幕上还行,在高分辨率屏幕上看会偏粗偏脏。常见的做法是给元素加::after伪元素,用transform:scaleY(0.5)来模拟半像素的线,或者用box-shadow:0 -1px 0 #eee来画。前者语义清楚,后者的代码更短。这个已经不是flex的范畴,但做视觉还原练习必然要碰到,提前了解几条方案,能让你的还原度上一个台阶。
4.6 排查工具要会用
调试flex布局,浏览器开发者工具是最好用的帮手。Elements面板里点击一个flex容器,会看到每个子元素的flex信息,还能直接在可视化面板上修改justify-content、align-items等属性,实时看效果。我练习的时候基本是开着开发者工具边改边看的,比来回刷新页面试错快太多。遇到搞不懂的间距来源,直接看Computed面板里的gap和margin,很多“莫名其妙多出来的空隙”都是padding或者gap设重复导致的。
5. 扩展思路与我的个人习惯
代码全部跑通之后,这个练习其实还能往前走两步。第一步是把练习稿抽象成自己的组件,第二步是形成一套固定的写法套路。这两步做完,你就不只是“照着还原了一个页面”,而是真正把flex变成了自己的基本功。
5.1 把练习稿整理成自己的组件
做完首页之后,别急着丢到一边。我建议你把这几个区域抽成独立的CSS类,比如.header、.grid、.card-scroll、.list-item、.tabbar,然后试着在一个空页面里不写原本的HTML结构,徒手把这些类用在新的内容上。如果新的内容也能正确排布,说明你抽象出来的组件是可复用的。这个动作的意义很大,等于把一次“抄作业”变成了自己的方法库,以后再写后台管理页面、移动端H5、活动落地页,直接复用这些结构就行。我现在的日常工作中,列表卡片和横向滚动卡片的基本样式就是从当年这个练习里沉淀出来的。
5.2 我写Flex的固定顺序
如果让我分享一个最具体的个人经验,那就是写Flex布局的固定顺序:先定display:flex,再想flex-direction,然后对齐属性,最后处理flex-basis、flex-grow、flex-shrink和gap。这个顺序看起来简单,但能保证你不是想到哪写到哪。很多人一上来就写justify-content,回头发现方向不对又全部推翻,代码看起来乱,排查也麻烦。按顺序来,基本每个容器都是一次成型。对于初学者,我甚至建议你每写一个flex容器就在注释里标一行“主轴:横向;子项:三个”这样的小笔记,等熟练了再删掉。
5.3 最后分享一个调试小窍门
调试flex布局时我喜欢临时给每个flex子项加一个醒目的背景色,比如background:rgba(0,0,255,0.1),这样每个元素的边界一目了然,哪里多占了、哪里没对齐,一眼就能看出来。定位到问题之后再把背景色删掉。这个方法看起来笨,但比盯着代码推理快得多,尤其是嵌套很多层flex的时候。另一个小习惯是给容器加outline而不是border,因为outline不影响布局尺寸,不会导致改样式时把布局挤乱。两个技巧配合起来,调试效率能翻倍。
练完这个首页,你对flex的掌控力基本就够应付生产环境里七八成的布局需求了。剩下两成,多是一些浏览器兼容性或者复杂交互相关的细节,等到具体项目里遇到了再查资料也不迟。我个人练完最大的收获不是记住了多少属性,而是形成了“先拆结构、再想方向、最后调对齐”的肌肉记忆。这一步走完,后面的路会顺很多。