布局实验室 Layout Lab
本章目录 / 案例进度

第 5 章 · 案例实战

从零设计一个博客首页

前四章的方法在这一章收束成一份可运行的成品。四个阶段、四份交付物:需求与线框 → 骨架选型 → 完整实现 → 逐块精修。结尾附一份可以直接拿去用的验收清单。

4 个知识点 3 个可交互演示 约 30 分钟
5.1

先回答「谁来读、来干什么」

布局的每一个取舍都应该能追溯到一条需求。没有需求约束的「好看」只是碰运气。

讲解

案例背景

「远山手记」是一份个人技术博客,长期更新设计与前端话题。作者自己写、自己排版,没有运营团队,也没有广告位。改造前的首页是「一栏长文一路铺到底」,问题不是不好看,而是没法用:读者找不到入口,老读者也看不到「有没有更新」。

两类读者,两种任务

读者 他们来干什么 布局必须提供
第一次来的访客 30 秒内判断「这里有没有我要的东西」,决定要不要留下来。 一个明确的精选区 + 一眼可扫的文章标题列表 + 清晰的文章主题标签。
回访的老读者 快速确认「有没有新文章」,然后直接点进去。 稳定的最新文章位置(不要每次改版都换地方)+ 归档入口 + 订阅入口。

约束条件

  • 技术约束:纯静态、无构建、无图片素材 —— 所以缩略图用 CSS 渐变生成,页面必须脱网可用。
  • 内容约束:文章数量会持续增长,因此列表必须能「长得下去」,不能是一个写死高度的大区块。
  • 性能与无障碍约束:移动端优先、正文可读、键盘可达、尊重「减少动态效果」的系统设置。

把需求翻译成布局决定,是这一节的核心动作。「老读者要快速确认有没有更新」→ 最新文章列表必须位于 DOM 与视觉的双重起点;「缩略图没有素材」→ 用 aspect-ratio 固定的渐变色块,反而让版面更耐看、更不挑内容。

关键代码

线框:区域与跨列关系 css
/* 线框阶段的产出:一张能表达优先级与跨列关系的图 */
  .blog {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);   /* 正文 : 侧栏 = 2 : 1 */
    gap: 24px;
  }

  /* 通栏区:用 1 / -1 跨满所有列,不依赖具体列数 */
  .blog > .hero,
  .blog > .footer { grid-column: 1 / -1; }

  /* 侧栏吸顶:读者滚动正文时次级入口始终可见 */
  .blog > .sidebar {
    align-self: start;
    position: sticky;
    top: 24px;
  }

  /* 侧栏里的卡片等距堆叠,用 Flex 而不是 Grid —— 一维排布 */
  .sidebar { display: flex; flex-direction: column; gap: 16px; }
  
  • 2fr : 1fr 的比例来自需求:正文是主任务,侧栏是辅助 —— 不是随手写的。
  • 侧栏 sticky 在长列表页面里价值很高,但要配 align-self: start,否则 Grid 项目默认拉伸会让 sticky 失效(本站的章节目录就是同一个坑的实例)。
  • 线框阶段就写得出这几行 CSS —— 这正是 Grid 的价值:版式可以先用代码描述,再填内容。

动态演示

Hero · 精选一篇文章 + 摘要 + 元信息
文章流4 篇,标题 + 摘要 + 标签
侧栏订阅 / 关于 / 标签
页脚版权 + 次级入口
显示 / 隐藏区域

实时输出:上面每一步操作对应的 CSS css

试试关掉侧栏:正文立刻获得满宽 —— 但请注意读数里那句取舍说明。线框阶段的价值就在于,你可以用三秒钟否定一个方案,而不必先把它写出来。

5.2

三个骨架方案,一次说清取舍

把候选方案并排画出来、把代价写在旁边,决策就不再依赖直觉。

讲解

选型的三个维度

  • 阅读体验:正文行宽是否落在可读区间(2.1)?这是硬指标。
  • 信息容量:一屏能承载多少个「入口」?这决定了新访客的留存。
  • 降级成本:窄屏时要拆掉几层结构?每拆一层就多一个断点、多一处可能出错的地方。

把三个方案放到这三个维度上量一遍,结论其实很明确:方案 B(两栏)在三个维度上的总和最优。本文这个博客的内容量还没到需要三栏的程度,而纯单栏又装不下「归档 + 订阅 + 标签」这三个入口。

最终决策:两栏为主,宽屏升级到三栏。默认(窄屏)单栏 → 600px 起两栏 → 1200px 起加上左侧归档栏变成三栏。选择三栏而不是「两栏 + 把归档塞进侧栏」,是因为归档在宽屏上需要常驻可见(老读者的高频入口),而侧栏已经有订阅与标签两个模块了。

关键代码

最终骨架:三段式 + 三档断点 css
/* 默认:手机单栏 */
  .f-grid {
    display: grid;
    gap: 24px;
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "stream" "side";
  }

  /* 600px:平板两栏 —— 正文为主,侧栏 220px */
  @media (min-width: 600px) {
    .f-grid {
      grid-template-columns: minmax(0, 1fr) 220px;
      grid-template-areas: "stream side";
      gap: 32px;
    }
  }

  /* 1200px:桌面三栏 —— 归档栏登场 */
  @media (min-width: 1200px) {
    .f-grid {
      grid-template-columns: 180px minmax(0, 1fr) 240px;
      grid-template-areas: "rail stream side";
    }
    .f-rail { display: grid; }        /* 归档栏只在最宽档出现 */
  }

  /* 三段式:DOM 顺序 = 内容优先级,视觉顺序由 areas 决定 */
  /* 归档栏放在 DOM 的第二位?不行 —— 它应当是 <aside>,
     因此 HTML 里写在 main 之后,靠 grid-area 放到左侧 */
  
  • 三个断点对应三种结构,而不是三套样式 —— 每档只做「加一栏」,没有任何撤销操作。
  • .f-rail 默认 display: none:它在窄屏下不存在(内容是归档列表,属于低频信息),宽屏才出现。用显示/隐藏而不是重排,可以让窄屏的阅读顺序保持干净。
  • 对照第 4 章:这里的结构与 4.1 演示里跑的正是同一份代码。

动态演示

方案 A · 单栏

  • 阅读体验最好,行宽容易控制
  • 实现与降级成本最低
  • 但归档 / 订阅 / 标签三处入口无处安放

方案 B · 两栏(推荐)

  • 正文行宽仍可控制在可读区间
  • 侧栏承载订阅 / 关于 / 标签
  • 窄屏只需一次「侧栏下移」的降级

方案 C · 三栏 + 通栏头尾

  • 信息容量最大,导航可常驻
  • 900px 以下会挤,需要两处断点降级
  • 对当前内容量来说,主内容被压得过窄
选择方案

切换三个方案,被选中的面板会加一圈高亮,下方给出选择它的完整理由。注意:最终成品其实同时包含了 A 和 B —— 窄屏是 A,宽屏是 B(再加一栏),这正是响应式布局的常态。

5.3

完整实现:三份文件,一个成品

成品只有 HTML、CSS、一小段 JS。下面是可以直接复制运行的代码,以及它在四种宽度下的真实表现。

讲解

文件结构

case/
  ├─ index.html   结构与文案(语义化标签:header / main / aside / footer)
  ├─ frame.css    全部样式:令牌 → 骨架 → 组件 → 断点
  └─ lang.js      一小段脚本:读取 ?lang 参数做双语,与本站主站的配置对象同源

样式表内部严格按四层顺序排列,这个顺序本身就是一份可读的文档:

  1. 令牌层:颜色、间距刻度、字体、圆角 —— 所有值只在这里出现一次;
  2. 骨架层:页面级的 Grid(.f-shell / .f-grid),决定区域的相对位置;
  3. 组件层:卡片、页头、侧栏模块;内部用 Flex,尺寸用令牌;
  4. 断点层:三个 min-width 媒体查询,全部集中在文件末尾。

读这份样式表的顺序:只想改版式,先看断点层(三档结构);只想换配色或间距,只看令牌层。中间两层通常不用动 —— 这正是四层顺序带来的好处。

几处值得单独说明的决定

  • 缩略图用 CSS 渐变而不是图片:aspect-ratio 固定比例 → 加载零等待、不产生布局跳动(CLS),而且不用维护素材。缺点是同质化,但博客列表页的缩略图本来就是「节奏点」而非内容。
  • 用边框分组,而不是阴影:列表里十几张卡片,如果每张都带阴影,页面会变得很吵;1px 边框在密集列表里安静得多。
  • grid-auto-rows1fr 保证同一行的卡片等高,按钮靠 margin-top: auto 吸到卡片底部 —— 一行里所有按钮因此落在同一条线上(回到 3.4 的第三条判据)。
  • 主题只提供了浅色,但所有颜色都走令牌,因此加一套 [data-theme=dark] 覆盖即可 —— 主站正是这么做的。

关键代码

结构:语义化标签 + 内容优先级顺序 html
<div class="f-shell">
    <header class="f-head">
      <div class="f-brand">远山<span>手记</span></div>
      <nav class="f-nav">
        <a href="#" aria-current="page">文章</a>
        <a href="#">笔记</a>
        <a href="#">关于</a>
      </nav>
      <button class="f-search" type="button">搜索</button>
    </header>

    <div class="f-grid">
      <!-- 归档栏:DOM 里在正文之后,靠 grid-area 放到左侧 -->
      <aside class="f-rail">
        <h4>归档</h4>
        <a href="#" aria-current="page">2026 · 秋</a>
        <a href="#">2026 · 夏</a>
      </aside>

      <!-- 正文:DOM 顺序上的第一块内容 -->
      <main class="f-stream">
        <section class="f-hero">
          <p class="f-kicker">精选</p>
          <h1>用栅格与留白,把首页安顿好</h1>
          <p class="f-lead">先确定信息优先级,再用栅格落位……</p>
        </section>
        <h2 class="f-sec-title">最新文章</h2>
        <div class="f-posts">
          <a class="f-post" href="#">
            <span class="f-thumb" aria-hidden="true"></span>
            <span>
              <h3>圣杯布局为什么还在被使用</h3>
              <p>三栏 + 通栏头尾 + 正文优先加载……</p>
            </span>
          </a>
        </div>
      </main>

      <aside class="f-side">…订阅 / 关于 / 标签…</aside>
    </div>

    <footer class="f-foot">© 2026 远山手记</footer>
  </div>
  • 用语义化标签(header / main / aside / nav / footer)而不是一堆 div:屏幕阅读器据此提供地标导航,键盘用户可以一键跳到 main
  • 页面里只有一个 h1,标题层级不跳级(h1 → h2 → h3)—— 这是无障碍的基本要求,也顺手保证了视觉层级。
  • aria-hidden 用在纯装饰的缩略图上,避免屏幕阅读器读出无意义的空元素。
样式:令牌 → 骨架 → 组件 → 断点 css
/* ① 令牌层 */
  :root {
    --space-4: 16px;
    --space-5: 24px;
    --space-6: 32px;
    --space-7: 48px;
    --accent: #1f6b64;
    --line: #e6e2da;
  }

  /* ② 骨架层:Grid 决定区域位置 */
  .f-shell {
    display: grid;
    gap: var(--space-5);
    max-width: 1180px;
    margin-inline: auto;
    padding: var(--space-4);
  }

  .f-grid {
    display: grid;
    gap: var(--space-5);
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "stream" "side";
  }
  .f-rail { display: none; }
  .f-stream { grid-area: stream; min-width: 0; }
  .f-side { grid-area: side; min-width: 0; }

  /* ③ 组件层:Flex 处理组件内部 */
  .f-post {
    display: grid;
    grid-template-columns: 64px minmax(0, 1fr);
    gap: var(--space-3);
    padding: var(--space-4);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
  }

  .f-thumb {
    width: 64px;
    aspect-ratio: 1;                       /* 固定比例:加载不跳动 */
    border-radius: 8px;
    background: linear-gradient(135deg, var(--accent), #7fb8ae);
  }

  .f-hero h1 {
    font-size: clamp(22px, 3.4vw + 8px, 40px);   /* 流体字号:少一个断点 */
  }

  /* ④ 断点层:集中在文件末尾,只做增强 */
  @media (min-width: 600px) {
    .f-grid {
      grid-template-columns: minmax(0, 1fr) 220px;
      grid-template-areas: "stream side";
      gap: var(--space-6);
    }
  }

  @media (min-width: 900px) {
    .f-posts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .f-post:first-child { grid-column: 1 / -1; }   /* 首篇通栏,形成节奏 */
  }

  @media (min-width: 1200px) {
    .f-grid {
      grid-template-columns: 180px minmax(0, 1fr) 240px;
      grid-template-areas: "rail stream side";
    }
    .f-rail { display: grid; align-content: start; gap: 4px; }
  }
  
  • 四层的顺序不能乱:令牌在最上(所有值来自这里),断点在最下(只覆盖位置与尺寸)。中间两层是「结构」与「零件」的分工。
  • 注意组件层里 .f-post 用的是 Grid(二维:图与文),而不是 Flex —— 图文并排且需要对齐时 Grid 更省事;.f-thumbaspect-ratio: 1 保证永远是正方形。
  • 断点层里没有任何颜色或字体声明 —— 那是令牌层的职责。这条纪律让改版变得很安全。
脚本:读参数做双语(与主站同源的做法) js
// 文案集中在一个配置对象里,切换语言只做一件事:遍历带 data-k 的元素
  const DICT = {
    en: {
      nav1: "Writing", nav2: "Notes", nav3: "About",
      h1: "Settling a homepage with grids and whitespace",
      streamTitle: "Latest writing"
    }
  };

  const params = new URLSearchParams(location.search);
  const lang = params.get("lang") === "en" ? "en" : "zh";
  const state = params.get("state") === "before" ? "before" : "after";

  // state=before 用于「改造前 / 改造后」对比,只切一个 class
  if (state === "before") document.body.classList.add("is-before");

  if (lang === "en") {
    document.querySelectorAll("[data-k]").forEach(function (el) {
      const value = DICT.en[el.dataset.k];
      if (value != null) el.textContent = value;   // 中文原文写在 HTML 里,缺译文时自然退回中文
    });
  }
  
  • 中文直接写在 HTML 里:没有 JS 也能完整阅读;英文放在配置对象里,只需维护一份对照,不会出现两份 HTML 逐渐「漂移」。
  • 缺译文时优雅退回原文,而不是显示空白或 undefined —— 双语站点最容易出的问题就是漏译,这一点必须兜住。
  • 本站主站的 i18n 用的是同一套思路,只是规模更大(区分「页面文案」与「JS 动态文案」两张表)。

动态演示

在新标签页打开成品 ↗

实时输出:上面每一步操作对应的 CSS css

这就是成品本身:可以滚动、可以点链接、可以在四种设备宽度之间来回切。注意 900px 那一档 —— 文章流变成两列且首篇通栏,这是一处「为了节奏而加」的断点,不是需求要求的结构变化。

5.4

逐块精修:把「能用」变成「好读」

最后一轮改造不动结构,只修正七处具体的版面问题 —— 而效果差别相当大。

讲解

改造前到底哪里不对

最初的实现可以运行、内容也对,但每一处都是「随手写的」:没有版心(max-width: none)、三栏写死(任何宽度都是三栏)、标题和正文同一字号、间距来自 3px / 4px 这类随手值、缩略图写死 96px。

这一节的要点:坏版面往往不是「设计能力不足」,而是每一个决定都缺少依据。逐条对照前面四章的原则,问题会自己浮出来 —— 这也是为什么验收清单比审美直觉更可靠。

七处修正,以及它们各自对应的原则

改动 从 → 到 对应原则
加版心 max-width: none1180px + margin-inline: auto 2.1 行宽
三栏写死 → 四档断点 固定三栏 → 单栏 / 两栏 / 三栏 4.1 移动优先
建立字号层级 标题 15px → clamp(22px, 3.4vw + 8px, 40px) 1.3 层级 + 4.2 流体
间距归入刻度 3px / 4px 随手值 → --space-1 … --space-7 1.2 留白
缩略图改为弹性 width: 96pxwidth: 64px; aspect-ratio: 1 4.3 响应式陷阱
卡片内部对齐 文字随内容浮动 → 图与文分列,按钮吸底 1.1 对齐 + 3.4 跨卡片对齐
分组关系可见 组内 = 组间 → 组间 = 组内 × 1.9 1.2 接近性原则

关键代码

同一份代码的「改造前」覆盖层 css
/* 演示用的「改造前」:全部是常见错误,逐条对应上面的修正表 */
  body.is-before .f-shell {
    max-width: none;          /* ✗ 没有版心 → 行宽随窗口无限拉长 */
    padding: 4px;             /* ✗ 间距随手给 */
    gap: 4px;
  }

  body.is-before .f-grid {
    grid-template-columns: 70px minmax(0, 1fr) 70px;
    grid-template-areas: "rail stream side";
    /* ✗ 不管多窄都硬撑三栏,窄屏挤成一团 */
  }

  body.is-before .f-hero h1 {
    font-size: 15px;          /* ✗ 标题与正文同级 → 层次消失 */
    font-weight: 400;
  }

  body.is-before .f-thumb {
    width: 96px;              /* ✗ 写死宽度,容器再窄也不缩 */
    height: 96px;
  }

  body.is-before .f-posts { gap: 3px; }   /* ✗ 组内组间一样大 → 分组关系消失 */
  
  • 把旧样式写成一层覆盖(而不是维护两份 HTML),使 A/B 对比只需切换一个 class —— 演示、回归测试、向同事解释改了什么,都用同一份代码。
  • 注意每一行都标了「✗ + 原因」。代码注释写清楚「为什么这样是错的」,比只写「这是旧版本」有用得多。
  • 这个 is-before 类在真实项目里通常只存在于演示环境;它的价值是让「改版收益」可见、可测。

动态演示

版本

这一轮做掉的七处修正
  • 加版心:max-width 1180px + 居中
  • 三栏写死 → 四档断点(移动优先)
  • 建立字号层级(标题用流体 clamp)
  • 间距全部归入统一刻度
  • 缩略图改用比例而非固定宽高
  • 卡片内部对齐,按钮吸底
  • 组间间距放大到组内的 1.9 倍

先看「改造前」并在下方把视口拖窄 —— 你会看到行宽失控、三栏挤成一团、标题与正文一样大。再切回「改造后」:同样的内容、同样的 HTML 结构,只是每一个决定都有了依据。

验收清单

把下面这份清单用在你自己的页面上。八条全部打勾,版面质量就有了基本保障(勾选状态会记在这台设备上)。

  • 把浏览器从最窄拖到最宽,没有任何一处横向滚动条、没有内容重叠或溢出。
  • 正文行宽在 45–75 西文字符(或 25–40 汉字)之间,超宽屏上也不会继续拉长。
  • 眯眼看页面(或缩到 25%),还能看出几条清晰的竖直对齐线。
  • 页面里的层级不超过三级,且用比例尺而不是随手取的字号。
  • 所有间距都来自同一套刻度;组内间距小于组间间距。
  • 关闭 CSS 的高亮,只用键盘 Tab 走一遍:焦点顺序与视觉顺序一致,且没有卡在不可见元素上。
  • 缩放到 200% 或把默认字号调大,版面仍然成立(说明用了 rem / 相对单位)。
  • 支持「减少动态效果」的系统设置;图片有宽高或比例,加载时不产生跳动。

案例到此完成。回看这五章:需求决定了骨架,骨架决定了实现方式,实现方式决定了断点怎么加 —— 每一步都可以追溯到上一步。页面布局的「好」,最终来自这种可追溯,而不是来自某一个漂亮的像素值。