前端知库

Grid 的 grid-template-areas 怎样画页面布局?怎样配合 auto-fill 做响应式?

grid-template-areas 用一组字符串把网格画出来,每个名字占的格子组成一个区域,子元素用 grid-area 放进去,"." 表示空格子。每行格子数必须相同,同名格子必须拼成矩形,否则整条声明无效。每个区域还会自动生成 名字-start 和 名字-end 网格线。换布局只要在媒体查询里改字符串;卡片列表用 repeat(auto-fill, minmax()) 让列数随宽度变化。

参考答案

简答

比如后台页面要有顶栏、侧边栏、主内容和底栏。用 grid-template-areas 可以把布局直接“画”出来:每个字符串是一行,里面每个名字是一个格子,"head head" "nav main" "foot foot" 就是顶栏和底栏横跨两列。子元素写 grid-area: main,就放进名为 main 的区域。

它有两条硬规则:每一行的格子数必须一样;同一个名字占的格子必须拼成一个实心矩形,不能是 L 形或者分成两块。违反任何一条,整条声明都会失效。不想放东西的格子用 . 占位。

好处是布局一眼就能看懂,改布局也只改这一处。手机上要把侧边栏挪到主内容下面,只要在媒体查询里换一组字符串,HTML 和子元素的样式都不用动。另外每个区域会自动生成 名字-start 和 名字-end 网格线,其他元素也能按这些线定位。

区域适合固定结构的页面骨架。商品卡片这种数量不定的列表,用 repeat(auto-fill, minmax(200px, 1fr)) 让浏览器按容器宽度自动决定放几列,两者经常一起用:外层用区域分骨架,主内容里再用 auto-fill 排卡片。

先把布局画出来

下面是一个三行两列的后台布局。每个字符串是一行,空格分开的每个名字是一列:

CSS
.layout {
  display: grid;
  grid-template-columns: 200px 1fr;
  grid-template-rows: 56px 1fr 40px;
  grid-template-areas:
    'head head'
    'nav  main'
    'foot foot';
}

.header { grid-area: head; }
.sidebar { grid-area: nav; }
.content { grid-area: main; }
.footer { grid-area: foot; }

MDN 的说明是,每写一个字符串就创建一行,字符串里的每个格子创建一列。同一个名字在一行内或者相邻行里重复出现,这些格子就合成一个跨多格的区域。所以 head 在第一行写了两次,顶栏就横跨两列。

为什么 nav main 中间有两个空格也没关系?规范规定格子之间只按空白分隔,多余的空白会被忽略,所以可以用空格把列对齐,方便阅读。

哪些写法会让整个布局失效

CSS Grid 规范对字符串有两条要求:所有字符串的格子数(包括用 . 表示的空格子)必须相同,并且至少有一个格子;一个名字如果跨多个格子,这些格子必须组成一个实心矩形。任何一条不满足,整条 grid-template-areas 声明就是无效的,浏览器会当作没写,子元素上的 grid-area 也就找不到区域了。

下面用一个函数模拟这两条检查,并算出每个区域占的网格线。网格线从 1 开始编号,第一列左边是第 1 条线:

JavaScript
function parseAreas(rows) {
  const grid = rows.map((row) => row.trim().split(/\s+/));
  if (grid.some((cells) => cells.length !== grid[0].length)) return 'invalid: column count';
  const areas = {};
  grid.forEach((cells, r) => {
    cells.forEach((name, c) => {
      if (/^\.+$/.test(name)) return;
      const box = areas[name] || (areas[name] = { r1: r, r2: r, c1: c, c2: c, count: 0 });
      box.r1 = Math.min(box.r1, r);
      box.r2 = Math.max(box.r2, r);
      box.c1 = Math.min(box.c1, c);
      box.c2 = Math.max(box.c2, c);
      box.count += 1;
    });
  });
  for (const [name, b] of Object.entries(areas)) {
    if ((b.r2 - b.r1 + 1) * (b.c2 - b.c1 + 1) !== b.count) return 'invalid: ' + name;
  }
  const result = {};
  for (const [name, b] of Object.entries(areas)) {
    result[name] = `${b.r1 + 1} / ${b.c1 + 1} / ${b.r2 + 2} / ${b.c2 + 2}`;
  }
  return result;
}

const layout = parseAreas(['head head', 'nav  main', 'foot foot']);
console.log(layout.head); // 1 / 1 / 2 / 3
console.log(layout.main); // 2 / 2 / 3 / 3
console.log(parseAreas(['a a', 'a b', 'c c'])); // invalid: a
console.log(parseAreas(['a b c', 'a b'])); // invalid: column count
console.log(Object.keys(parseAreas(['. head', 'nav main'])).join(',')); // head,nav,main

head 的结果 1 / 1 / 2 / 3 和 grid-area 的写法一样,依次是行开始线、列开始线、行结束线、列结束线,也就是从第 1 条行线到第 2 条,横跨第 1 到第 3 条列线。

第三行里 a 占了左上、右上、左中三个格子,拼起来是 L 形,不是矩形,所以无效。第四行第二个字符串少了一个格子,也无效。最后一行的 . 是空格子,不会生成区域,所以只有三个名字。

规范还说明,以后的版本可能会允许非矩形或者不连续的区域,但目前的规范不允许。

区域名还能当作网格线名用

规范规定,每个命名区域 foo 会自动生成四条隐式的线名:行方向和列方向的开始线都叫 foo-start,结束线都叫 foo-end。它们和手写的线名一样可以用来定位,只是不会出现在 grid-template-columns 的值里。

所以一个浮在主内容右上角的提示条,不用写数字,也能对齐到 main 区域:

CSS
.notice {
  grid-column: main-start / main-end;
  grid-row: main-start;
}

反过来也成立:子元素写 grid-area: main 能生效,是因为浏览器按名字找到了 main-start 和 main-end 这几条线。

怎样在手机上换布局

区域的好处在响应式时最明显。窄屏上去掉侧边栏那一列,把导航放到主内容上面,只改容器上的三条属性:

CSS
@media (max-width: 640px) {
  .layout {
    grid-template-columns: 1fr;
    grid-template-rows: 56px auto 1fr 40px;
    grid-template-areas:
      'head'
      'nav'
      'main'
      'foot';
  }
}

子元素上的 grid-area 一个都没改,HTML 也没动,它们按名字自动落到新的位置上。

要注意,这样改的只是视觉位置,读屏软件和 Tab 键还是按 HTML 源码的顺序走。如果视觉顺序和源码顺序差得太多,键盘用户会觉得焦点在乱跳,所以源码顺序本身应该是合理的阅读顺序。

卡片列表怎样让列数自动变化

区域适合结构固定的骨架。主内容里的商品卡片数量不定,屏幕宽就多放几列,窄就少放几列,这时用 repeat() 配合 auto-fill 和 minmax():

CSS
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 16px;
}

意思是每列至少 200px、最多平分剩余空间,在不溢出的前提下尽量多放列。容器从 1000px 缩到 500px,列数会自动从 4 列变成 2 列,不需要写媒体查询。

auto-fit 规则一样,区别只在项目少、放不满一行的时候:auto-fill 把空列留着,auto-fit 把空列收成 0 宽,让已有的项目拉宽铺满。两者的细节和 1fr 导致横向滚动条的问题,见《Grid 的 fr、minmax、auto-fit 和 auto-fill 如何使用?》。

常见追问

grid-template-areas 和直接写 grid-row、grid-column 数字有什么区别?

效果一样,因为区域名最终也是转换成网格线来定位的。区别在维护:数字分散写在每个子元素上,改布局要改很多处;区域把整个布局集中写在容器上,看字符串就知道长什么样,响应式时也只改容器。

两个子元素写了同一个 grid-area 会怎样?

它们会被放进同一块区域,互相重叠,后面的盖在前面的上面,可以用 z-index 调整。区域名只描述网格里的位置,并不限制只能放一个元素。

什么时候用 Grid,什么时候用 Flex?

要同时控制行和列、让不同行的同一列对齐时用 Grid;一行里的元素按内容宽度依次排开时用 Flex。详细的比较见《Flexbox 和 Grid 应该如何选择?》。