深入淺出grid佈局

異次元的廢D發表於2018-08-14

原文釋出於 github.com/ta7sudan/no…, 如需轉載請保留原作者 @ta7sudan.

注意, 因為 grid 標準還在修訂中, 以下內容可能會隨著標準的改變而過時(比如據說之後的標準可能會將 grid-row-gapgrid-column-gap 改為 row-gapcolumn-gap, 以便與 column 佈局統一), 當然我也會盡量及時修正. 以下內容寫於 2018/2/27.

本文中提到的 content-box 均是在未改變 box-sizing 的情況下適用.

關於 grid 的應用場景, 或者說比其他佈局有什麼優勢? 在有既要按水平方向進行對齊, 同時又要保證垂直方向的對齊時, 可以使用 grid 佈局.

關於 grid 的建議是, 如果面向的使用者是最新的瀏覽器, 那基本可以用於生產環境, 否則不建議用於生產環境.

在介紹 gird 佈局之前我們先來看幾幅圖

img101

整個最大的整塊矩形塊我們稱作網格容器(grid container), 也是我們的佈局容器, 即容器裡面的元素都按照網格進行佈局. 圖中的虛線我們稱為網格線(grid line), 網格線是一個佈局時的參考線, 並不會被實際渲染出來. 網格線帶有一個屬性, 那就是編號, 即每個網格線都是有自己的序號的.

img102

像這樣, 無論水平方向還是垂直方向的網格線都有自己的編號. 編號是網格線固有的屬性, 無需開發者自己定義, 預設從 1 開始, 從上往下依次遞增, 從左往右依次遞增(但也不一定, 還與書寫模式有關). 網格線可以有自己的名字, 名字由開發者定義, 具體參考後文.

網格容器被網格線分隔成了 3x3 個單元格, 每個單元格我們稱為網格單元(grid item). 網格線也把網格容器分成了三行三列, 這裡行和列我們都稱為網格軌道(grid track). 其中深色的矩形塊 One, Two, Three, Four 我們稱為網格專案(grid item).

那網格單元和網格專案有什麼區別? 答案是網格單元不是一個元素, 只是元素定位的參考系, 是一個邏輯概念, 不會被渲染到頁面. 網格專案是我們實際渲染的元素, 網格專案根據網格單元進行定位, 網格專案可以佔據一個網格單元, 也可以佔據多個網格單元, 像下面這樣.

img104

可以看到, 這裡的 One, Two 等矩形塊都是網格專案, 這才是實際渲染出來的樣子, 它們根據網格單元進行定位, 有些佔據一個網格單元, 有些佔據多個網格單元, 多個網格專案也可能重疊佔據同一個網格單元.

img103

但是網格專案不僅僅可以根據網格單元進行定位, 還可以根據網格區域進行定位. 我們可以從一片連續的網格單元中取出一個矩形, 如紅框所示. 我們可以把這四個網格單元定義成一個網格區域(grid area), 然後讓一個網格專案佔據一塊網格區域(目前只支援矩形的網格區域, 還不支援 L 型的網格區域).

有人可能已經注意到, 第三幅圖中的網格專案之間有些空白間隔, 我們把這些間隔叫做網格間距(gutter). 那網格間距是否佔用網格軌道的寬度呢? 下圖可以更清楚的表明這點.

img105

可以看到, 藍色的是網格單元, 網格單元所處的地方是網格軌道, 而網格間距並沒有佔用網格軌道的空間.

OK, 通過以上幾幅圖, 我們大概可以知道, 網格容器, 網格軌道, 網格線, 網格單元, 網格區域, 網格間距都是邏輯概念, 是網格容器內部元素定位的參考系, 它們(除了網格容器和網格間距)不會被實際渲染出來, 而網格專案則是網格容器內部實際存在的元素, 會被渲染出來.

整個 grid 佈局的過程就像是在一個事先定義的網格里面鋪地磚, 我們把網格專案鋪在網格單元上, 一個網格專案可以佔據一個或多個網格單元(為了便於理解, 這裡我們先用佔據這個詞, 但準確說並不是網格專案佔據網格單元, 而是指定網格專案的定位區域一個或多個網格單元. 之後的內容中會更詳細地解釋這點), 網格專案之間也可以重疊. 所以使用 grid 佈局的基本套路就是: 先定義網格容器, 再定義網格單元, 然後指定網格專案怎麼個"鋪"法.

需要注意的是, 我們說的定位是指網格專案的 margin-box 擺放在網格單元/網格區域之中, 網格單元/網格區域就像一個 BFC 那樣, 使得網格專案的外邊距不和其他網格專案的外邊距發生摺疊.

深入淺出grid佈局

這個圖中清晰地表明瞭一個網格專案的 margin-box 位於一個網格區域(三個網格單元)之中.

幾個術語

接著我們來了解一些術語, 其中有些前面我們已經提到過, 不過這裡給出一些更具體的定義.

  • 網格容器(grid container), display: grid;display: inline-grid; 的元素, 整個網格系統都在網格容器的 content-box 中, 而網格容器對外(其他元素)依然是塊級元素或者行內元素. 這個元素的所有正常流中的直系子元素(包括匿名盒子)都將成為網格專案(grid item) (W3C: Each in-flow child of a grid container becomes a grid item). display: grid; 會建立一個GFC(grid formatting context), 類似 BFC 的東西.
  • 網格線(grid line) 是網格容器的水平或垂直分界線. 網格線存在於列或行的任意一側. 它們可以通過數字索引(從1開始)來引用或者通過開發者自定義的名字來引用. 作為元素定位時的參考系, 是一個邏輯意義上的實體, 不會被渲染.
  • 網格軌道(grid track) 是任意兩條相鄰網格線之間的空間, 即一行或者一列, 之後簡稱軌道/行/列. 作為元素定位時的參考系, 是一個邏輯意義上的實體, 不會被渲染.
  • 網格單元(grid cell) 是行和列相交的空間, 是 grid 佈局的最小單元. 作為元素定位時的參考系, 是一個邏輯意義上的實體, 不會被渲染.
  • 網格區域(grid area) 是一塊邏輯意義上的空間, 它包含了一個或多個相鄰的網格單元, 由某四條網格線圍成. 網格區域可以通過名字定義, 也可以通過網格線定義. 作為元素定位時的參考系, 是一個邏輯意義上的實體, 不會被渲染.
  • 顯式網格(explicit grid)和隱式網格(implicit grid), 通過 grid-template-columnsgrid-template-rows 建立出來的軌道, 我們稱它們屬於顯示網格. 但是這些軌道不一定能容納所有的網格專案, 瀏覽器根據網格專案的數量計算出來需要更多的軌道, 就會自動生成新的軌道, 這些自動生成的軌道我們稱它們屬於隱式網格.
  • 網格間距(gutter) 通過 grid-row-gapgrid-column-gap (但是現在 W3C 標準中已經修改為通過 row-gap, column-gap 來定義了. 沒錯, 就是那個列布局中也會用到的 column-gap)定義相鄰軌道之間的空白空間. 是一個額外的不佔用軌道空間的空白. 對於跨越多個網格單元的網格專案, 不僅僅佔據單元格的空間, 也佔據其中的網格間距的空間. 網格間距只會在顯示網格中出現.
  • 剩餘空間是我自己發明的詞, 即網格容器某方向上的大小(content-box 的 width)減去該方向上所有的網格間距之和剩下的空間, 也即所有行/列的寬度之和.

接下來也會通過具體例子進一步說明這些名詞.

新單位 fr 和 repeat(), minmax() 函式

在看具體例子之前先了解下隨著 grid 佈局一同出現的新單位 fr 以及兩個新函式 repeat(), minmax(). 這裡只簡單地介紹這些內容, 之後會有更具體的說明.

fr

fr 其實就是分配剩餘空間的權重, 熟悉 flex-grow 的話可以很容易 get 到這一點.

img107

可以看到, 我們有一個網格容器, 容器有網格間距, 其中有三個網格專案, 它們的寬度都是 1fr, 所以它們均分了剩餘空間.

img108

而 2fr 的話則是這樣, 它的工作機制就像 flex-grow 那樣.

repeat()

repeat() 函式可以簡單地理解為巨集展開, 就像 Sass 中的 mixin 那樣, 舉個例子

grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(3, 1fr 2fr);
複製程式碼

相當於

grid-template-columns: 1fr 1fr 1fr;
grid-template-columns: 1fr 2fr 1fr 2fr 1fr 2fr;
複製程式碼

repeat(3, 1fr) / repeat(3, 1fr 2fr) 展開成了 1fr 1fr 1fr / 1fr 2fr 1fr 2fr 1fr 2fr, . 當然, 這裡只是簡單地這麼理解, 之後還有更具體的說明.

minmax()

minmax() 用來指定一個軌道的寬度值, 比如你可以指定一個軌道寬固定為 200px, 你也可以指定一個軌道寬度為 minmax(200px, 1fr), 這樣的話, 軌道寬度是彈性的, 最小為 200px, 最大為 1fr.

使用 grid 佈局

使用 grid 佈局的一般套路可以總結為:

定義網格容器 -- 劃分網格單元 -- 根據網格單元定義網格專案

來看一個例子

<div class="main m0">
	<div class="item">1</div>
	<div class="item">2</div>
	<div class="item">3</div>
	<div class="item">4</div>
</div>
複製程式碼
.main {
	width: 300px;
	background: #fff4e6;
	border: 2px solid #f76707;
	border-radius: 5px;
	margin-bottom: 50px;
	display: grid;
}

.item {
	color: #d9480f;
	background: #ffd8a8;
	border: 2px solid #ffa94d;
	border-radius: 5px;
}
.m0 {
	grid-template-columns: 1fr 1fr 1fr;
	grid-template-rows: 50px 100px;
}
複製程式碼

img109

我們先定義了一個網格容器, 然後 grid-template-columns: 1fr 1fr 1fr; 定義了三列, 每列寬為 1fr, grid-template-rows: 50px 100px; 定義了兩行, 其中第一行為 50px 高, 第二行為 100px 高. 網格專案預設被放入一個網格單元中, 按照從左往右從上往下排列, 於是得到如圖效果. 其中 grid-template-columns: 1fr 1fr 1fr; 也可以寫成 grid-template-columns: repeat(3, 1fr);.

顯式網格和隱式網格

在上面的例子中, 我們通過 grid-template-columnsgrid-template-rows 把網格容器劃分成了 2x3 個網格單元, 假如我們只指定一行的話又是怎樣?

.m1 {
	grid-template-columns: 1fr 1fr 1fr;
	grid-template-rows: 50px;
}
複製程式碼

img110

可以看到, 儘管我們沒有指定兩行, 但是網格容器依然被劃分成了 2x3 個網格單元, 其中第一行的所有網格單元是我們通過 grid-template-columnsgrid-template-rows 手動建立的, 我們把這種手動建立(即寬高都是由開發者定義的)的行和列產生的網格單元稱為顯式網格(explicit grid), 第二行是瀏覽器根據網格專案的個數計算自動生成的, 我們把這種由瀏覽器自動生成的網格單元為隱式網格(implicit grid).

我們指定了第一行為 50px 高度, 但是第二行因為是自動生成的, 所以高度預設由瀏覽器根據內容決定. 那是不是我們就沒辦法指定隱式網格的大小了? 也不是, 我們可以通過 grid-auto-columnsgrid-auto-rows 來指定隱式網格的大小.

.m2 {
	grid-template-columns: 1fr 1fr 1fr;
	grid-template-rows: 50px;
    grid-auto-rows: 100px;
}
複製程式碼

img112

可以看到, 第二行又變回了 100px 高.

既然可以自動生成行, 那什麼時候自動生成列? 我們可以通過 grid-auto-flow 來改變網格專案的排列方向.

.m3 {
	grid-template-columns: 1fr;
	grid-template-rows: repeat(3, 50px);
	grid-auto-flow: column;
}
複製程式碼

img113

這裡我們指定了三行, 但是隻指定了一列, 但是在 grid-auto-flow 的作用下, 原本先從左往右, 再從上往下排列的網格專案變成了先從上往下, 再從左往右排列, 於是第二列屬於隱式網格.

到這裡, 最基本的 grid 佈局方式就比較清楚了, 不過如果 grid 佈局僅僅是這些內容, 那前面我也就不用廢話這麼多了.

以上程式碼見 demo.

基於網格線的定位

我們之前說了, grid 佈局的一般套路是定義網格容器 -- 劃分網格單元 -- 根據網格單元定義網格專案, display: grid; 完成了定義網格容器這步. 通過 grid-template-columns grid-template-rows grid-auto-columns grid-auto-rows 定義了 mxn 個網格單元, 完成了劃分網格單元這步. 但是我們並沒有根據網格單元定義網格專案, 網格專案是瀏覽器按照特定順序自動填充到每個網格單元中的, 那要怎麼完成這一步呢?

最簡單的是通過網格線, 還記得之前提到過, 網格線是有自己的編號的, 也可以有自己的名字, 接下來我們通過編號和名字來完成這最後一步.

<div class="main m0">
	<div class="item">1</div>
	<div class="item">2</div>
	<div class="item">3</div>
	<div class="item">4</div>
</div>
複製程式碼
.m0 {
	grid-template-columns: repeat(5, 1fr);
	grid-template-rows: repeat(4, 50px);
}
.m0 > .item:nth-child(2) {
	grid-row-start: 2;
	grid-row-end: 4;
	grid-column-start: 2;
	grid-column-end: 4;
}
複製程式碼

img114

我們定義了 4x5 個網格單元, 從除錯工具可以很清楚地看到, 第二個網格專案佔據了 2x2 個網格單元, 所以看起來比其他幾個網格專案大了不少. 而這裡, 其實就是我們通過網格線指定了第二個網格專案的範圍, grid-row-start 指定了網格專案的水平方向的起始邊是水平方向編號為 2 的網格線, grid-row-end 指定了水平方向結束邊是水平方向編號為 4 的網格線, grid-column-start 指定了垂直方向起始邊是垂直方向編號為 2 的網格線, grid-column-end 指定了垂直方向結束邊是垂直方向編號為 4 的網格線, 於是第二個網格專案的範圍就是 row2, row4, col2, col4 這四條網格線圍成範圍, 這樣一個網格專案就可以佔據多個網格單元.

可能已經有人注意到 3 和 4 被放到了 2 的上面, 不禁會問 3 和 4 是按照什麼規則放的? 這個問題留給之後解釋.

對於這個例子, 我們還可以以縮寫的形式, 通過 grid-row grid-column 或者 grid-area 來完成.

.m1 {
	grid-template-columns: repeat(5, 1fr);
	grid-template-rows: repeat(4, 50px);
}
.m1 > .item:nth-child(2) {
	grid-row: 2/4;
	grid-column: 2/4;
}
複製程式碼

或者

.m2 {
	grid-template-columns: repeat(5, 1fr);
	grid-template-rows: repeat(4, 50px);
}
.m2 > .item:nth-child(2) {
	grid-area: 2/2/4/4;
}
複製程式碼

其中 grid-area 的順序是 grid-row-start grid-column-start grid-row-end grid-column-end.

我們還可以省略 grid-column-endgrid-row-end, 這樣的話瀏覽器預設將它們設定成比 start 編號大 1 的網格線, 即只佔一個軌道.

.m1 {
	grid-template-columns: repeat(5, 1fr);
	grid-template-rows: repeat(4, 50px);
}
.m1 > .item:nth-child(2) {
	grid-row-start: 2;
	grid-column-start: 2;
}
複製程式碼

img115

可以看到, 第二個網格專案只佔了一個網格單元.

以上這些網格線相關的屬性還可以是負值, 表示倒數第 n 條網格線. eg.

.m4 {
	grid-template-columns: repeat(5, 1fr);
	grid-template-rows: repeat(4, 50px);
}
.m4 > .item:nth-child(2) {
	grid-area: -1/2/4/-1;
}
複製程式碼

img116

這裡 -1 表示倒數第一條網格線, 所以第二個網格專案是 row5, col2, row4, col6 這四條網格線圍成的區域.

命名的網格線

前面我們還說了, 網格線除了有自己的編號, 還可以有自己的名字, 不過編號是瀏覽器定義的, 而名字是開發者自己定義的. 那怎麼定義名字呢? 也很簡單, 在定義網格單元的時候可以順便定義網格線的名字.

<div class="main m5">
	<div class="item">1</div>
</div>
複製程式碼
.m5 {
	grid-template-columns: [first-start] 1fr [first-end second-start] 1fr [second-end third-start] 1fr [third-end];
	grid-template-rows: [first-start] 50px [first-end second-start] 50px [second-end third-start] 50px [third-end];
}
.m5 > .item {
	grid-area: second-start/second-start/second-end/second-end;
}
複製程式碼

img117

這裡我們定義了 3x3 個網格單元, 有 3 列, 每列 1fr 的寬度, 1fr 定義了列寬, 兩邊的 [] 的內容則是軌道兩邊網格線的名字, 並且一個網格線可以有不止一個名字, 多個名字用空格分隔, 並且不同方向的網格線可以使用相同的名字, 如垂直方向和水平方向都有名為 first-start 的網格線. 之後我們通過名字而不是編號, 指定了網格專案佔據的範圍.

相同名字的網格線

不僅不同方向的網格線可以使用相同的名字, 其實相同方向的網格線也可以使用相同的名字.

.m6 {
	grid-template-columns: [col-start] 1fr [col-start] 1fr [col-start] 1fr [col-start];
	grid-template-rows: [row-start] 50px [row-start] 50px [row-start] 50px [row-start];
}
.m6 > .item {
	grid-area: 1/col-start/span 2/col-start 2;
}
複製程式碼

img118

這裡我們的列網格線都叫做 col-start, 行網格線都叫做 row-start, 那瀏覽器要怎麼區分它們? 答案就是 col-start 2 這樣的值, 表示第二條名為 col-start 的網格線, 也就是編號 2 的網格線. 而如果只寫 col-start 不加編號的話則預設是第一條 col-start 的網格線.

這裡還有個 span 2, 意思是 start 編號加 2 的網格線, 也就是 1+2=3, 編號 3 的網格線. 所以這裡 grid-area: 1/col-start/span 2/col-start 2; 等價於 grid-area: 1/1/3/2;. 對於只指定了結束網格線, 而起始網格線用 span 的話.

grid-column-end: 5;
grid-column-start: span 2;
複製程式碼

這裡的 grid-column-start 應該是 5-2=3, 而不是 5+2=7 了.

以上程式碼見 demo.

基於網格區域的定位

之前我們已經知道什麼是網格區域, 也接觸到了 grid-area, 你會說, 很簡單嘛, grid-area 就是一個縮寫而已, 指定了四條網格線圍成的區域. 不過如果僅僅認為 grid-area 只有這樣的功能, 那實在是有點 naive 了.

我們知道網格線可以有通過名字來引用, 其實網格區域也可以通過名字引用.

<div class="main m0">
	<div class="item header">1</div>
	<div class="item sidebar">2</div>
	<div class="item content">3</div>
	<div class="item footer">4</div>
</div>
複製程式碼
.m0 {
	grid-template-columns: repeat(9, 1fr);
	grid-auto-rows: minmax(100px, auto);
	grid-template-areas: 
	"hd hd hd hd hd hd hd hd hd"
	"sd sd sd main main main main main main"
	"ft ft ft ft ft ft ft ft ft";
}
.m0 > .header {
	grid-area: hd;
}
.m0 > .footer {
	grid-area: ft;
}
.m0 > .content {
	grid-area: main;
}
.m0 > .sidebar {
	grid-area: sd;
}
複製程式碼

我們先定義了 9 列, 然後沒有固定行數, 而是指定了隱式網格的行高最小為 100px, 最大由瀏覽器自動處理. 接著我們使用了一個 grid-template-areas 的屬性, 屬性有三個字串, 對映到三行, 每個字串中有 hd 這樣的內容, 空格分隔, hd 表示的是一個網格區域的名字, 每個 hd 對應一個網格單元, 即指定了這個網格單元屬於 hd 這個區域, grid-template-areas 其實就定義了一個 3x9 的網格系統.

接著我們定義了四個類 .header .footer .content .sidebar, 並且通過 grid-area 指定了每個類佔據的區域.

img119

於是我們通過區域的名字指定了網格專案的範圍.

注意到其實這裡 grid-template-areas 已經完成了劃分網格單元這一操作, 但是它劃分的網格單元都是屬於隱式網格, 因為它沒有指定網格單元的寬高(一個網格單元的寬高都由開發者指定的話才屬於顯式網格), 所以為了好看點我們還是需要 grid-template-columnsgrid-auto-rows.

另外我們發現, 我們需要在 grid-template-areas 手寫一個矩陣, 並且每個分量都必須是一個網格區域的名字, 這會導致網格專案剛好佔滿所有網格單元. 那假如我們希望留出一些網格單元作為空白的話怎麼辦? 就像這樣.

img120

我們可以這麼寫

.m1 {
	grid-template-columns: repeat(9, 1fr);
	grid-auto-rows: minmax(100px, auto);
	grid-template-areas: 
	"hd hd hd hd hd hd hd hd hd"
	"sd sd sd main main main main main main"
	". . . ft ft ft ft ft ft";
}
.m1 > .header {
	grid-area: hd;
}
.m1 > .footer {
	grid-area: ft;
}
.m1 > .content {
	grid-area: main;
}
.m1 > .sidebar {
	grid-area: sd;
}
複製程式碼

我們用 . 替代了原本是網格區域名字的地方, 表示這裡的網格單元不屬於任何網格區域, 留出空白.

需要注意的是命名的網格區域還是有它的侷限, 那就是無法指定兩個網格專案重疊, 如果需要網格專案重疊, 還是需要通過網格線來實現.

命名網格線定義的命名網格區域

之前我們的 demo 中, 命名一個網格線都是用 xxx-start, xxx-end 這樣的名字, 是不是一定要用 - 連線一個 start 或者 end 呢? 其實也不是, 名字是可以隨便起的, 不過按照這種方式起名的話有一個好處, 就是瀏覽器會自動幫你定義一個 xxx 的網格區域.

舉個例子.

<div class="main m2">
	<div class="item content">content</div>
</div>
複製程式碼
.m2 {
	grid-template-columns: [main-start] 1fr [content-start] 1fr [content-end] 1fr [main-end];
	grid-template-rows: [main-start] 50px [content-start] 50px [content-end] 50px [main-end];
}
.content {
	grid-area: content;
}
複製程式碼

img121

可以看到, 儘管這裡我們並沒有定義一個名為 content 的網格區域, 但是當我們指定 .content 的網格區域為 content 時它被放在了中間, 表明確實存在這麼個網格區域, 這個命名的網格區域就是瀏覽器根據命名的網格線自動建立的. 當以 xxx-start 和 xxx-end 命名網格線時, 如果剛好圍成了一個矩形, 則瀏覽器為這個矩形建立一個名為 xxx 的網格區域.

命名網格區域定義的命名網格線

同樣的, 瀏覽器也會為一個命名的網格區域自動建立命名的網格線.

.m3 {
	grid-template-columns: repeat(9, 1fr);
	grid-auto-rows: minmax(100px, auto);
	grid-template-areas: 
	"hd hd hd hd hd hd hd hd hd"
	"sd sd sd main main main main main main"
	"ft ft ft ft ft ft ft ft ft";
}
.m3 > .header {
	grid-row: hd-start/hd-end;
	grid-column: hd-start/hd-end;
}
.m3 > .sidebar {
	grid-row: sd-start/sd-end;
	grid-column: sd-start/sd-end;
}
.m3 > .content {
	grid-row: main-start/main-end;
	grid-column: main-start/main-end;
}
.m3 > .footer {
	grid-row: ft-start/ft-end;
	grid-column: ft-start/ft-end;
}
複製程式碼

img122

儘管這裡我們沒有定義命名網格線, 但是定義了命名的網格區域, 瀏覽器自動為這些網格區域生成了對應的命名網格線, 也是以 xxx-start, xxx-end 這樣的形式, 於是我們可以直接通過這些名字引用這些網格線.

以上程式碼見 demo.

網格間距

網格間距比較簡單, 只需要通過 grid-row-gap grid-column-gap 來指定即可.

<div class="main m0">
	<div class="item">1</div>
	<div class="item">2</div>
	<div class="item">3</div>
	<div class="item">4</div>
</div>
複製程式碼
.m0 {
	grid-template-columns: repeat(4, 1fr);
	grid-template-rows: repeat(4, 50px);
	grid-row-gap: 10px;
	grid-column-gap: 20px;
}
.m0 > .item:nth-child(1) {
	grid-area: 1/1/3/3;
}
複製程式碼

img123

這裡我們通過 grid-row-gap grid-column-gap 指定了行之間的間距為 10px, 列之間的間距為 20px. 需要注意的是, 網格間距並不會導致網格線數量增加.

以上程式碼見 demo.

Grid 中的自動定位

之前我們已經注意到, 有些網格專案儘管我們沒指定它們佔據的區域, 但是它們還是會自動按照某種規則進行排列. 現在我們就來討論這個規則是怎樣運作的.

<div class="main m0">
	<div class="item">1</div>
	<div class="item">2</div>
	<div class="item">3</div>
	<div class="item">4</div>
	<div class="item">5</div>
	<div class="item">6</div>
	<div class="item">7</div>
	<div class="item">8</div>
	<div class="item">9</div>
	<div class="item">10</div>
	<div class="item">11</div>
	<div class="item">12</div>
</div>
複製程式碼
.m0 {
	grid-template-columns: repeat(4, 1fr);
	grid-auto-rows: 50px;
}
.m0 > .item:nth-child(2) {
	grid-area: 2/3/4;
}
.m0 > .item:nth-child(5) {
	grid-area: 1/1/3/3;
}
複製程式碼

我們定義了一個 4 列的網格容器, 其中第二個網格專案佔據 row2, row4, col3, col4 圍成的區域, 第五個網格專案佔據 row1, row3, col1, col3 圍成的區域. 之後其他元素按照先從左往右, 再從上往下的順序進行排列. 如圖所示.

img125

再考慮這種情況.

.m1 {
	grid-template-columns: repeat(4, 1fr);
	grid-auto-rows: 50px;
}
.m1 > .item:nth-child(2) {
	grid-area: 2/3/4;
}
.m1 > .item:nth-child(5) {
	grid-area: 1/1/3/3;
}
.m1 > .item:nth-child(1) {
	grid-column-end: span 2;
	grid-row-end: span 2;
}
.m1 > .item:nth-child(9) {
	grid-column-end: span 2;
	grid-row-end: span 2;
}
複製程式碼

依然是 4 列, 12 個網格專案, 其中 2 和 5 的定位沒變, 1 和 9 沒有指定起始網格線, 只指定了結束網格線, 意味著 1 和 9 要佔據 2x2 個網格單元, 但是具體位置由瀏覽器自動定位. 如圖.

img126

我們把通過網格線或網格區域指定了位置的網格專案稱為定位的網格專案, 把沒有指定位置而是依靠瀏覽器自動定位的網格專案稱為無定位的網格專案. 設定位的網格專案的集合為 A, 無定位的網格專案的集合為 B, B[i] 的大小為 B[i].x, B[i].y, 總行數為 m, 總列數為 n, 所有網格單元的集合 C 為 mxn 的矩陣, 自動定位的演算法可以用虛擬碼表述為這樣

for(i = 0; A不為空; ++i) {
    將A[i]放到對應位置
}
for(j = 0; j < m; ++j) {
    for(k = 0; k < n; ++k) {
        if(B不為空) {
            for(p = 0; p < B[0].x; ++p) {
                for(q = 0; q < B[0].y; ++q) {
                    if(c[j+p][k+q]被佔據) {
                        break 2;
                    }
                }
            }
            if(p == B[0].x && q == B[0].y) {
            	cur = B.unshift();
            	把cur放到C[j][k]到C[j+p][k+q]的矩形中
            }
        } else {
            break 2;
        }
    }
}
複製程式碼

即先逐行掃描, 找到一個空餘的網格單元, 再從集合 B 中取一個無定位的網格專案, 看是否能夠放下, 不能的話就找下一個空餘網格單元, 重複此操作.

所以在這種情況下, 3 沒有在 2 的上面, 而是和 2 相鄰, 2 和 3 上面留出了空白.

當然我們也可以把這些空白填上, 只需要通過 grid-auto-flow: dense; 即可, 沒錯, 就是之前我們見到的那個改變自動定位流向的 grid-auto-flow.

.m2 {
	grid-template-columns: repeat(4, 1fr);
	grid-auto-rows: 50px;
	grid-auto-flow: dense;
}
.m2 > .item:nth-child(2) {
	grid-area: 2/3/4;
}
.m2 > .item:nth-child(5) {
	grid-area: 1/1/3/3;
}
.m2 > .item:nth-child(1) {
	grid-column-end: span 2;
	grid-row-end: span 2;
}
.m2 > .item:nth-child(9) {
	grid-column-end: span 2;
	grid-row-end: span 2;
}
複製程式碼

img127

可以看到, 現在 3, 4, 6 把之前 2 上面和邊上的空餘空間給補上了. 這種情況下的自動定位演算法可以描述為

for(j = 0; j < m; ++j) {
    for(k = 0; k < n; ++k) {
        for(i = 0; B不為空;) {
            for(p = 0; p < B[i].x; ++p) {
                for(q = 0; q < B[i].y; ++q) {
                    if(c[j+p][k+q]被佔據) {
                        break 2;
                    }
                }
            }
            if(p == B[i].x && q == B[i].y) {
                cur = B.splice(i, 1);
                把cur放到C[j][k]到C[j+p][k+q]的矩形中
                i = 0;
            } else {
                ++i;
            }
        }
    }
}
複製程式碼

即先逐行掃描, 找到一個空餘的網格單元, 再從集合 B 中取一個非定位的網格專案, 看是否能夠放下, 如果不能, 則從 B 中取下一個非定位的網格專案, 重複此操作. 這種情況下, 會盡量保證從上往下沒有空白.

注意這裡都沒有改變自動定位的流向, 即預設是行優先, 先從左往右, 再從上往下, 逐行掃描. 如果改變了自動定位的流向, 即 grid-auto-flow: column; 或者 grid-auto-flow: column dense;, 則演算法變為列優先, 即先從上往下, 再從左往右, 逐列掃描.

其實上面隱含了一個小技巧, 就是大部分時候如果我們希望指定一個網格專案的大小, 那就得通過網格線或者網格區域, 不可避免地也指定了網格專案的位置, 假如我們希望指定一個網格專案的大小, 但又不想指定它的具體位置, 我們可以省略起始網格線, 通過 span 關鍵字, 如 grid-template-columns: span 2; 這樣, 就指定了一個兩列寬的網格專案, 但又沒有指定它的具體位置, 而是由瀏覽器自動定位.

以上程式碼見 demo.

匿名網格專案

如同匿名塊級元素和匿名行內元素一樣, 當出現這種情況的時候

<div class="grid">
    test test
    <div class="item">item</div>
    <div class="item">item</div>
</div>
複製程式碼

未被標籤包裹的 test test 內容也會被作為網格專案, 瀏覽器會為它生成一個匿名網格專案, 匿名網格專案只能被自動定位, 因為我們無法通過選擇器為它們指定位置.

absolute 和 grid

預設情況下, 如果一個網格專案是絕對定位的話, 同其他時候一樣, 網格專案脫離文件流, 瀏覽器不會為它生成隱式網格, 它也不會影響其他網格的定位, 就像它不是網格容器的子元素那樣.

<div class="main m0">
	<div class="item">1</div>
	<div class="item">2</div>
	<div class="item">3</div>
	<div class="item">4</div>
</div>
複製程式碼
.m0 {
	grid-template-columns: 1fr 1fr 1fr;
	grid-template-rows: 50px 100px;
}
.m0 > .item:nth-child(4) {
	width: 50px;
	height: 50px;
	position: absolute;
	top: 0;
	left: 0;
}
複製程式碼

img128

可以看到, 4 是絕對定位的, 因為網格容器沒有設定任何定位方式, 所以這裡它的包含塊是根元素而不是網格容器.

而假如網格容器設定了定位, 且絕對定位的元素原本是自動定位的. 如下面例子.

.m1 {
	grid-template-columns: 1fr 1fr 1fr;
	grid-template-rows: 50px 100px;
	position: relative;
}
.m1 > .item:nth-child(4) {
	width: 50px;
	height: 50px;
	position: absolute;
	top: 0;
	left: 0;
}
複製程式碼

img129

對於設定了絕對定位的自動定位的網格專案, 如果網格容器設定了定位, 則包含塊是網格容器.

假如我們給絕對定位的網格專案設定了網格區域

.m2 {
	grid-template-columns: repeat(4, 1fr);
	grid-template-rows: repeat(4, 50px);
	position: relative;
}
.m2 > .item:nth-child(4) {
	width: 50px;
	height: 50px;
	grid-area: 2/2/4/4;
	position: absolute;
	bottom: 0;
	left: 0;
}
複製程式碼

img130

則網格專案的包含塊是網格區域而不是網格容器. 可以看到, 4 位於網格區域的左下角而不是網格容器的左下角.

img131

這幅圖能夠很清楚地說明這點.

以上程式碼見 demo.

grid 中的對齊

同 flexbox 一樣, grid 中也有對齊, 依舊是熟悉的屬性, 熟悉的味道.

  • align-items
  • align-self
  • align-content
  • justify-self
  • justify-content

在這裡, 我們先糾正兩點比較容易引起誤會的地方.

  • grid 的主要作用是定位而不是控制網格專案的大小, 儘管它可以用來控制網格專案的大小. 我們通過給網格專案指定網格線/網格區域, 其實是指定網格專案在這塊區域內定位, 而不是指定網格專案佔滿這塊區域
  • 並不是整個網格容器都會被網格軌道佔滿

實際上, 網格專案的大小是由以上屬性來決定的.

這部分內容對於熟悉 flexbox 的人來說比較簡單, 就只貼幾個簡單的示例好了, 如果不熟悉的話建議先了解 flexbox.

需要注意的是, grid 和 flexbox 不一樣的是, grid-auto-flow 不會像 flex-direction 那樣改變 start 和 end. 比如即使 grid-auto-flow: column; 的情況, align-items 還是對每行的行內進行上下對齊, 而不會變成對每列列內進行左右對齊. 同理其他屬性.

align-items

控制每行中的所有網格專案在行軌道內的對齊方式

align-items: start

img132

align-items: end

img133

align-items: center

img134

align-items: baseline

img135

以上程式碼見 demo.

align-self

控制單個元素在行軌道內的對齊方式

align-self: end

img136

align-self: center

img137

align-self: baseline

img138

以上程式碼見 demo.

align-content

控制所有行軌道在網格容器內的對齊方式

.m0 {
	width: 500px;
	height: 400px;
	grid-template-columns: repeat(3, 100px);
	grid-template-rows: repeat(3, 100px);
	align-content: start;
}
.m0 > .item:nth-child(2) {
	font-size: 28px;
}
複製程式碼

注意到, 所有的列寬之和小於網格容器的寬度, 所有的行高之和小於網格容器的高度.

img139

於是整個網格系統沒有佔滿網格容器, 而是位於網格容器左上角, 這是 align-content: start; 的作用.

align-content: end

img140

align-content: center

img141

align-content: space-around

img142

align-content: space-between

img143

注意, align-content 可能會使得行間距變寬.

以上程式碼見 demo.

justify-self

類似 align-self, 控制一個網格專案在列軌道上的對齊方式.

justify-self: start

img144

justify-self: end

img145

justify-self: center

img146

以上程式碼見 demo.

justify-content

類似 align-content, 控制所有列軌道在網格容器內的對齊方式

justify-content: end

img147

justify-content: center

img148

justify-content: space-around

img149

justify-content: space-between

img150

注意, justify-content 可能導致列間距變寬.

以上程式碼見 demo.

Grid 相關屬性

  • grid-template-columns
  • grid-template-rows
  • grid-template-areas
  • grid-template
  • grid-auto-columns
  • grid-auto-rows
  • grid-auto-flow
  • grid-row-start
  • grid-column-start
  • grid-row-end
  • grid-column-end
  • grid-row
  • grid-column
  • grid-area
  • grid-row-gap
  • grid-column-gap
  • grid-gap
  • grid

看上去很多, 不過現在我們可以把它們劃分成幾類:

  • 劃分網格單元的屬性
  • 定位網格專案的屬性

grid-template-columns/grid-template-rows

劃分網格單元的屬性, 作用於網格容器, 用來指定網格的列寬列數以及命名網格線, 預設值 none. eg.

grid-template-columns: 50px 100px;
grid-template-columns: [col-start] 100px;
grid-template-columns: [col-start] 100px [col-end];
grid-template-columns: [col-start] 100px [col-end col-start] 100px [col-end];
grid-template-columns: 50px minmax(30px, 1fr);
複製程式碼

百分比的值相對於網格容器的 content-box, 如果網格容器的 content-box 大小是由軌道的大小決定的, 則百分比單位被視為 auto.

grid-template-areas

劃分網格單元的屬性, 作用於網格容器, 用來命名網格區域, 預設值 none. eg.

grid-template-areas: "a a a b" "a a a b";
grid-template-areas: "a a a b" ". . . b";
複製程式碼

grid-template

劃分網格單元的屬性, 作用於網格容器, 是 grid-template-columns grid-template-rows grid-template-areas 的縮寫.

grid-template: 100px 1fr/50px 1fr; /* rows / columns */
grid-template: [row-line] 100px [row-line] 50px [row-line]/[col-line] 50px [col-line] 50px [col-line];
grid-template: "a a a" "b b b";
grid-template: "a a a" 20px "b b b" 30px;
grid-template: "a a a" 20px "b b b" 30px / 1fr 1fr 1fr;
grid-template: [header-top] "a a a" [header-bottom] [main-top] "b b b" 1fr [main-bottom] / auto 1fr auto;
複製程式碼

grid-auto-columns/grid-auto-rows

劃分網格單元的屬性, 作用於網格容器, 用來指定隱式網格的行列寬度.

grid-auto-columns: 1fr;
grid-auto-columns: 1fr 100px; /* 1fr 100px 1fr 100px... */
grid-auto-columns: 1fr 100px 50px; /* 1fr 100px 50px 1fr 100px 50px... */
複製程式碼

百分比的值相對於網格容器的 content-box, 如果網格容器的 content-box 大小是由軌道的大小決定的, 則百分比單位被視為 auto.

grid-auto-rows / grid-auto-rows 可以不止一個值, 多個值表示交替.

grid-auto-flow

劃分網格單元的屬性, 作用於網格容器, 用來指定自動定位演算法是行優先(先從左往右再從上往下)還是列優先(先從上往下再從左往右)以及是否填充空餘空間. 預設值 row.

grid-auto-flow: row;
grid-auto-flow: column;
grid-auto-flow: row dense;
grid-auto-flow: column dense;
複製程式碼

grid-column-gap/grid-row-gap

劃分網格單元的屬性, 作用於網格容器, 用來指定垂直/水平方向的網格間距大小. 預設值 0.

grid-column-gap: 20px;
grid-column-gap: 20%;
複製程式碼

百分比相對於網格容器的 content-box.

grid-gap

劃分網格單元的屬性, 作用於網格容器, grid-column-gap grid-row-gap 的縮寫.

grid-gap: 10px 20px; /* row col */
複製程式碼

grid

grid-template-rows grid-template-columns grid-template-areas grid-auto-rows grid-auto-columns grid-auto-flow 的縮寫.

grid-row-start/grid-row-end/grid-column-start/grid-column-end

定位網格專案的屬性, 作用於網格專案, 指定起始/結束的網格線.

grid-row-start: 3;
grid-row-start: row-start;
grid-row-start: row-start 2;
grid-row-start: span 3;
複製程式碼

grid-row/grid-column

grid-row-start grid-row-end / grid-column-start grid-column-end 的縮寫.

grid-row: 1/3; /* start/end */
grid-column: 2/4;
複製程式碼

grid-area

定位網格專案的屬性, 作用於網格專案, 指定網格專案所屬的網格區域.

grid-area: 1/2/3/4; /* row-start/column-start/row-end/column-end */
grid-area: row-start/column-start/row-end/column-end;
grid-area: areaname;
複製程式碼

幾個函式

  • repeat()
  • minmax()
  • fit-content()

fit-content()

適用於任何可以用來定義軌道寬度的屬性. 用來指定軌道的寬度, 效果類似於 max-width, 即內容不夠給定寬度的話, 按內容寬度, 內容大於給定寬度, 按給定寬度. eg.

grid-template-columns: fit-content(20px);
grid-template-columns: fit-content(20%);
grid-template-columns: fit-content(20vw);
複製程式碼

其中百分比單位是, 在空間足夠時, 相對於網格容器 content-box 對應軸方向的大小, 在空間不夠時, 相對於網格容器 content-box 對應軸方向的剩餘空間的大小(即 content-box 大小減去所有網格間距的大小).

以上程式碼見 demo.

minmax()

適用於任何可以用來定義軌道寬度的屬性. 用來指定軌道的最小和最大寬度, 效果類似於同時指定了 min-widthmax-width. 帶兩個引數 min 和 max. eg.

grid-template-columns: minmax(100px, 200px);
grid-template-columns: minmax(100px, 1fr);
grid-template-columns: minmax(100px, 20%);
grid-template-columns: minmax(100px, min-content);
grid-template-columns: minmax(100px, max-content);
grid-template-columns: minmax(100px, auto);
複製程式碼

幾個注意點

  • 如果 min > max, 則忽略 max 的值, 整個 minmax(min, max) 的值視為 min 的值
  • fr 單位可以作為 max, 但是不能作為 min
  • 百分比單位相對於網格容器的 content-box 大小
  • max-content 表示儘可能寬, 儘可能不換行
  • min-content 表示儘可能窄, 達到最大的 inline-box 寬度
  • auto 作為 max 時等同於 max-content, 作為 min 時, 等同於 min-content. 實際測下來, auto 在 max 下的表現和 1fr 一樣, 不知道為什麼

示例程式碼見 demo

repeat()

適用於任何可以用來定義軌道寬度的屬性. 用於重複某一模式生成軌道列表. 效果類似於巨集展開或 Sass 的 @mixin. eg.

grid-template-columns: repeat(2, 20%);
grid-template-columns: repeat(2, 1fr); /* 1fr 1fr */
grid-template-columns: repeat(2, [col-start] 1fr); /* [col-start] 1fr [col-start] 1fr */
grid-template-columns: repeat(2, [col-start] 1fr [col-end]); /* [col-start] 1fr [col-end col-start] 1fr [col-end]*/
grid-template-columns: repeat(2, [col1-start] 1fr [col2-start] 3fr); /* [col1-start] 1fr [col2-start] 3fr [col1-start] 1fr [col2-start] 3fr */
grid-template-columns: repeat(2, [col-start] minmax(100px, 1fr) [col-end]);
grid-template-columns: repeat(auto-fill, 100px);
grid-template-columns: repeat(auto-fit, 100px);
複製程式碼

重點

  • 百分比單位相對於網格容器的 content-box 大小
  • auto-fill 會盡可能地讓每個軌道更寬並且儘可能在一行(列)中放下更多列(行), 不保證所有軌道佔滿網格容器. 在每個軌道寬度確定的情況下優先確保軌道盡可能寬(示例 m1), 在每個軌道寬度不確定的情況下, 優先確保放下更多列(示例 m2, m3, m4). (不要看 MDN 的描述!!! 巨他媽難翻譯! 看 W3C 的描述 好懂得一比, 其中的 gap 不是指 grid-gap, 而是網格系統沒佔滿網格容器的空餘空間都是 gap)
  • auto-fit 會盡可能讓所有軌道佔滿網格容器, 並且儘可能地讓每個軌道更寬並且儘可能在一行(列)中放下更多列(行).
  • 我覺得實際場景下, 只要用 auto-fit 就夠了...

示例程式碼見 demo

其他一些細節

  • floatclear 對網格專案是無效的, 但是 float 依然會改變網格專案 display 的計算值(參考 深入理解float), 網格專案的 display 計算值預設為 blockified(The display value of a grid item is blockified: if the specified display of an in-flow child of an element generating a grid container is an inline-level value, it computes to its block-level equivalent)
  • vertical-align 對網格專案是無效的
  • ::first-line ::first-letter 不能用於網格容器
  • z-index 用來控制網格專案之間的疊加層級
  • order 用來控制自動定位的網格專案的排列順序
  • 利用媒體查詢可以動態地改變列數和行數

參考資料