[譯] 當你建立 Flexbox 佈局時,都發生了什麼?

下小朋友發表於2019-03-04

快速簡介: 在我的理想世界裡,CSS Grid 和 Flexbox 應當作為一個整體一起出現,才能組成完整的網頁佈局系統。然而,我們先有了 Flexbox 屬性,因為它比浮動(floats)更適合用來做網格佈局,所以出現了很多以 Flexbox 為基礎的網格佈局系統。事實上,很多人覺得 Flexbox 彈性佈局令人困擾或者難以理解,都是因為嘗試把它作為網格佈局的方法。

在接下來的一系列文章裡面,我會花一點時間來詳細講解 Flexbox 彈性佈局 —— 以之前我用來理解 grid 屬性的方式。我們一起看看 Flexbox 設計是為了什麼,它擅長處理什麼,以及我們為什麼不選擇它作為佈局的方法。在這篇文章中,我們會詳細看一下,當你在樣式表新增 display: flex 的時候,究竟會發生什麼。

一個 Flex 容器,拜託了!

為了使用 Flexbox 佈局,你需要一個元素作為 flex 容器,在 CSS 中,使用 display: flex

HTML:

<div class="container">
  <div class="item">1</div>
  <div class="item">2</div>
  <div class="item">3</div>
</div>
複製程式碼

CSS:

body {
  padding: 20px;
  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;
}

* {box-sizing: border-box;}

p {
  margin: 0 0 1em 0;
}

.container {  
  border: 5px solid rgb(111,41,97);
  border-radius: .5em;
  padding: 10px;
  display: flex;
  flex-direction: row-reverse;
}

.item {
  width: 100px;
  height: 100px;
  padding: 10px;
  background-color: rgba(111,41,97,.3);
  border: 2px solid rgba(111,41,97,.5);
}
複製程式碼

請看 Rachel Andrew(@rachelandrew)在 CodePen 上的這個例子:Smashing Flexbox Series 1: display: flex;

讓我們花一點點時間來思考一下到底 display: flex 意味著什麼。在 Display Module Level 3 中,display 的每一個值都由兩個部分構成:內部顯示模式和外部顯示模式。當我們使用 display: flex 時,我們其實定義的是 display: block flex。flex 容器的外部顯示模式是 block,它在文件流中顯示為正常的塊級元素。內部顯示模式是 flex,所以在容器內部的直接子元素按照彈性佈局來排列。

可能你之前沒仔細想過,但其實已經知道了。flex 容器在頁面中和其他塊級元素的表現一樣。如果你在 flex 容器後面緊跟一個段落,它們兩個都會表現為正常的塊級元素。

我們也可以把容器的屬性設定為 inline-flex,就和設定成 display:inline flex 一樣。比如說有一個行內級別的 flex 容器,容器裡還有一些參與 flex 佈局的子元素。這個 flex 容器內的子元素的表現就和塊級 flex 容器內的子元素表現一樣,不同之處就在於容器本身在整體佈局中的表現。

HTML:

<div class="container">
  <div class="item">1</div>
  <div class="item">2</div>
  <div class="item">3</div>
</div>

<em>這個 flex 容器是行內元素,所以另一個行內元素會緊跟它後面顯示。</em>
複製程式碼

CSS:

body {
  padding: 20px;
  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;
}

* {box-sizing: border-box;}

p {
  margin: 0 0 1em 0;
}

.container {  
  border: 5px solid rgb(111,41,97);
  border-radius: .5em;
  padding: 10px;
  display: inline-flex;
}

.item {
  width: 100px;
  height: 100px;
  padding: 10px;
  background-color: rgba(111,41,97,.3);
  border: 2px solid rgba(111,41,97,.5);
}
複製程式碼

請看 Rachel Andrew(@rachelandrew)在 CodePen 上的這個例子:Smashing Flexbox Series 1: display: inline-flex;

元素的外部顯示模式決定了它作為盒模型在頁面中怎樣顯示,同時與內部顯示模式一起,決定了其子元素的行為。這是一個很有用的思想。你可以把這種思想應用於任何 CSS 定義的盒模型。這個元素將會如何表現?它的子元素又會怎樣表現?這些問題的答案就與它們的內部顯示模式和外部顯示模式有關。

行或者列?

一旦我們定義了 flex 容器之後,一些預設值就開始發揮作用了。在我們沒有新增任何其他屬性的情況下,flex 子專案(flex item)會按照行來排列。這是因為 flex-direction 屬性的預設值就是 row。如果你不對它進行設定,它就會按照行的方向來顯示。flex-direction 屬性是用來設定主軸(main axis)的排列方向,這個屬性還有其他的值:

  • column
  • row-reverse
  • column-reverse

當子專案排成一行的時候,子專案會按照在文件中的順序,從行內維度的起始邊緣依次排列。在規範中,這個邊緣就被叫做 main-start

main-start 是一行的開始

main-start 是行內維度的起始位置(Large preview)。

如果我們使用 column,子專案從塊級維度的起始邊緣開始排列,因此構成一列。

子專案按照列來排列,main-start 位於頂部

main-start 是塊級維度的起始位置(Large preview)。

當我們使用 row-reverse 時,main-startmain-end 的位置互換了,因此,子專案也會相應的按照相反的順序來排列。

子專案從行的末尾開始排列

main-start 在內聯維度的末尾(Large preview)。

column-reverse 也具有一樣的效果。另外還有一點很重要,那就是這些值並不會“改變子專案的順序”,儘管它們看起來是這樣的效果,它們改變的是這些子專案開始排列的位置:通過改變 main-start 來達到目的。所以我們的子專案會按照相反的方向來排列,這僅僅是因為它是從容器的結束位置開始排列的。

另外還有一件很重要的事情要記住,當排列順序發生改變時,這僅僅是視覺上的,因為我們要求子專案從結束位置開始排列。它們在文件中仍然是原來的順序,當你使用螢幕閱讀器的時候仍然是按照源文件的順序進行索引。如果你真的想要改變子元素的順序,不應該使用 row-reverse,而是直接在文件源中去改變子專案的順序。

Flexbox 的兩條軸線

我們已經講解了 flexbox 的一個重要特性:能夠將主軸(main axis)的方向從行切換為列。這種軸的方向切換,就是為什麼我常常認為網格佈局中的對齊更容易理解的原因。因為在網格佈局中,在兩個方向上你都可以採用幾乎相同的方式來實現對齊。而對於彈性佈局來說會更麻煩點,因為在主軸(main axis)和交叉軸(cross axis)上,子專案的表現是不太相同的。

我們已經瞭解了主軸(main axis),即你用 flex-direction 屬性的值來定義的那根軸線。交叉軸(cross axis)則在另一個方向上。如果你設定 flex-direction: row,那麼你的主軸(main axis)是沿著行的方向,你的交叉軸(cross axis)是沿著列的方向。如果設定 flex-direction: column,主軸(main axis)是沿著列的方向,交叉軸(cross axis)是沿著行的方向。在這裡我們就需要討論 flexbox 的另外一個重要特點,那就是它與螢幕的物理方向無關。我們不討論從左到右方向的行,或從上到下方向的列,因為情況並非總是如此。

書寫模式

當我在上文中描述行和列的時候,我提到了塊級和行內的維度 dimensions。這篇文章是用英文寫的,它是水平的書寫模式。這就意味著當你要 Flexbox 顯示一行時,子專案會水平的展示。在這個例子中,main-start 位於左邊 —— 也就是英文書寫模式中中句子開始的位置。

如果我使用的是從右到左書寫的語言,比如阿拉伯語的話,起始位置就會位於右邊:

HTML:

<div class="container">
  <div class="item">1</div>
  <div class="item">2</div>
  <div class="item">3</div>
</div>
複製程式碼

CSS:

body {
  padding: 20px;
  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;
}

* {box-sizing: border-box;}

p {
  margin: 0 0 1em 0;
}

.container {  
  border: 5px solid rgb(111,41,97);
  border-radius: .5em;
  padding: 10px;
  display: flex;
  flex-direction: row;
  direction: rtl;
}

.item {
  width: 100px;
  height: 100px;
  padding: 10px;
  background-color: rgba(111,41,97,.3);
  border: 2px solid rgba(111,41,97,.5);
}
複製程式碼

請看 Rachel Andrew(@rachelandrew)在 CodePen 上的這個例子:Smashing Flexbox Series 1: row with rtl text

flexbox 的初始值意味著,如果我所做的只是建立一個 flex 容器,我的子專案將會從右側開始顯示,並且向左排列。內聯方向的起始位置是你正在使用的書寫模式中句子開始的位置

如果你使用垂直書寫模式並且使用的預設排列方向(這裡指 flex-direction: row),此時的行就會是垂直方向的,因為這就是垂直書寫方式語言排列行的方式。你可以嘗試為 flex 容器設定 writing-mode 屬性,把值設定為 vertical-lr。現在,你再把 flex-direction 設定為 row,子專案就會排成垂直的一列了。

HTML:

<div class="container">
  <div class="item">1</div>
  <div class="item">2</div>
  <div class="item">3</div>
</div>
複製程式碼

CSS:

body {
  padding: 20px;
  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;
}

* {box-sizing: border-box;}

p {
  margin: 0 0 1em 0;
}

.container {  
  border: 5px solid rgb(111,41,97);
  border-radius: .5em;
  padding: 10px;
  display: flex;
  flex-direction: row;
  writing-mode: vertical-lr;
}

.item {
  width: 100px;
  height: 100px;
  padding: 10px;
  background-color: rgba(111,41,97,.3);
  border: 2px solid rgba(111,41,97,.5);
}
複製程式碼

請看 Rachel Andrew(@rachelandrew)在 CodePen 上的這個例子:Smashing Flexbox Series 1: row with a vertical writing mode

所以,一行可以水平的排列,main-start 位於左側或者右側,也可以垂直排列,main-start 位於頂部。即使我們的思維習慣了橫向排列的文字,很難想象一行垂直排列的文字,但它的 flex-direction 屬性的值仍然是 row

為了讓子專案按照塊級維度進行排列,我們可以把 flex-direction 的值設定成 column 或者 column-reverse。在英語(或者阿拉伯語)這樣的水平書寫模式裡,子專案會從容器頂部開始按照垂直方向排列。

在垂直書寫模式中,塊級方向橫跨整個頁面,這也是這種書寫模式下塊級元素的排列方向。如果你將一列設定為為 vertical-lr,那麼這些塊級元素會從左到右進行排列(內部的文字方向仍為垂直排列)。

HTML:

<div class="container">
  <div class="item">1</div>
  <div class="item">2</div>
  <div class="item">3</div>
</div>
複製程式碼

CSS:

body {
  padding: 20px;
  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;
}

* {box-sizing: border-box;}

p {
  margin: 0 0 1em 0;
}

.container {  
  border: 5px solid rgb(111,41,97);
  border-radius: .5em;
  padding: 10px;
  display: flex;
  flex-direction: column;
  writing-mode: vertical-lr;
}

.item {
  width: 100px;
  height: 100px;
  padding: 10px;
  background-color: rgba(111,41,97,.3);
  border: 2px solid rgba(111,41,97,.5);
}
複製程式碼

請看 Rachel Andrew(@rachelandrew)在 CodePen 上的這個例子:Smashing Flexbox Series 1: column in vertical-lr writing mode

但是,無論塊級元素怎麼顯示,只要你使用的是 column 方向,那麼元素始終處在塊級維度之中。

瞭解一行或者一列能夠在不同的物理方向上執行,有助於我們理解網格佈局和彈性佈局中的一些術語。在網格佈局和彈性佈局中,並不會使用『左和右』、『上和下』這樣的方向,因為我們並不會指定文件的書寫模式。現在所有的 CSS 都變的更注重書寫模式了。如果你對其他已經支援這種方向差異的 CSS 屬性和值有興趣的話,可以讀一下我的這篇文章 Logical Properties and Values

總結一下,記住:

  • flex-direction: row

    • 主軸 = 行內維度
    • main-start 位於當前書寫模式下句子開頭的位置
    • 交叉軸 = 塊級維度
  • flex-direction: column

    • 主軸 = 塊級維度
    • main-start 位於當前書寫模式下塊級元素的開頭位置
    • 交叉軸 = 行內維度

預設對齊方式

當我們設定 display: flex 時,還會發生一些事情,預設的對齊方式會發揮作用。在該系列的其他文章中,我們會好好地瞭解一下對齊方式。但是,我們現在在探索 display: flex 的時候,也應該看一下這些發揮作用的預設值。

注意值得注意的是,儘管這些對齊屬性始於 Flexbox 規範,但 Box Alignment(盒模型對齊)會最終覆蓋 Flexbox 規範的相關內容,如 flexbox 規範中所述。

主軸對齊方式

justify-content 屬性的預設值是 flex-start,就像我們的 CSS 寫的那樣:

.container {
    display: flex;
    justify-content: flex-start;
}
複製程式碼

這就是我們的 flex 子專案(flex item)從 flex 容器的起始邊緣開始排列的原因。同樣也是當我們設定 row-reverse 時,它們變為從結束邊緣開始排列的原因,因為那個邊緣變成了主軸(main axis)的起點。

當你看見對齊屬性以 justify- 開頭時,這個屬性就是作用於主軸(main axis)的。也就是說 justify-content 屬性規定主軸(main axis)的對齊方式,並將子專案從起始邊緣對齊。

justify-content 還有其他的值:

  • flex-end
  • center
  • space-around
  • space-between
  • space-evenly(在塊級對齊方式中新增)

這些值用來處理 flex 容器中剩餘空間的分佈。這就是子專案間會發生移動或者說相互分隔的原因了。如果你新增屬性 justify-content: space-between,剩餘空間被平均分配給子專案。當然,這隻有在容器有剩餘空間的情況下才會發生。如果你的 flex 容器被全部充滿了(子專案排列完後,沒有任何剩餘空間),那麼設定 justify-content 屬性不會產生任何效果。

你可以把 flex-direction 設定成 column。因為 flex 容器在沒有高度的情況下不會有剩餘空間,所以設定 justify-content: space-between 不會發生變化。如果你把容器設定成比展示所需要的高度更高的話,那麼這個屬性就會發揮作用了:

HTML:

<div class="container">
  <div class="item">1</div>
  <div class="item">2</div>
  <div class="item">3</div>
</div>
複製程式碼

CSS:

body {
  padding: 20px;
  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;
}

* {box-sizing: border-box;}

p {
  margin: 0 0 1em 0;
}

.container {  
  border: 5px solid rgb(111,41,97);
  border-radius: .5em;
  padding: 10px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  height: 500px;
}

.item {
  width: 100px;
  height: 100px;
  padding: 10px;
  background-color: rgba(111,41,97,.3);
  border: 2px solid rgba(111,41,97,.5);
}
複製程式碼

請看 Rachel Andrew(@rachelandrew)在 CodePen 上的這個例子:Smashing Flexbox Series 1: column with a height

交叉軸的對齊方式

子專案在單一交叉軸的 flex 容器中也會沿著這根交叉軸對齊。這裡執行的對齊是子專案沿著交叉軸相互對齊。在接下來的這個例子中,有一個子專案比其他項佔據更高的空間,然後其他的子專案會按照某種規範來拉伸到與它相同的高度,這個規範就是 align-items 屬性,因為它的初始值就是 stretch

HTML:

<div class="container">
  <div class="item">One</div>
  <div class="item">Two</div>
  <div class="item">
    <ul>
    <li>Three: a</li>
    <li>Three: b</li>
    <li>Three: c</li>
    </ul>
    </div>
</div>
複製程式碼

CSS:

body {
  padding: 20px;
  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;
}

* {box-sizing: border-box;}

p {
  margin: 0 0 1em 0;
}

.container {  
  border: 5px solid rgb(111,41,97);
  border-radius: .5em;
  padding: 10px;
  display: flex;
}

.item {
  padding: 10px;
  background-color: rgba(111,41,97,.3);
  border: 2px solid rgba(111,41,97,.5);
}

.item ul {
  margin: 0;
  padding: 0;
  list-style: none;
}
複製程式碼

請看 Rachel Andrew(@rachelandrew)在 CodePen 上的這個例子:Smashing Guide to Layout: clearfix

當你在 flex 佈局中看到有一個屬性是以 align- 開頭的,那個就是交叉軸的對齊方式,align-items 屬性規定子專案在沿著交叉軸方向上的對齊方式,這個屬性的其他的值包括:

  • flex-start
  • flex-end
  • center
  • baseline

如果你不想其他的子專案跟拉伸到跟最高的那一項一樣高的話,設定 align-items: flex-start,它會把子專案都沿著交叉軸的起始位置對齊。

HTML:

<div class="container">
  <div class="item">One</div>
  <div class="item">Two</div>
  <div class="item">
    <ul>
    <li>Three: a</li>
    <li>Three: b</li>
    <li>Three: c</li>
    </ul>
    </div>
</div>
複製程式碼

CSS:

body {
  padding: 20px;
  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;
}

* {box-sizing: border-box;}

p {
  margin: 0 0 1em 0;
}

.container {  
  border: 5px solid rgb(111,41,97);
  border-radius: .5em;
  padding: 10px;
  display: flex;
  align-items: flex-start;
}

.item {
  padding: 10px;
  background-color: rgba(111,41,97,.3);
  border: 2px solid rgba(111,41,97,.5);
}

.item ul {
  margin: 0;
  padding: 0;
  list-style: none;
}
複製程式碼

請看 Rachel Andrew(@rachelandrew)在 CodePen 上的這個例子:Smashing Flexbox Series 1: align-items: flex-start

flex 子專案的初始值

終於說到這裡了,flex 子專案(flex item)也是有初始值的,它們包括:

  • flex-grow: 0
  • flex-shrink: 1
  • flex-basis: auto

這意味我們的子專案(flex item)在預設情況下不會自動充滿主軸上的剩餘空間。如果 flex-grow 被設定成一個正數,才會導致子專案拉伸並佔據剩餘空間。

這些子專案(flex item)同樣可以收縮。預設情況下,flex-shrink 的值被設定成了 1。這就意味著,如果我們的 flex 容器非常小,那麼其中的子元素在溢位容器之前就會自動的縮小以適應容器大小。這是一個非常靈活的屬性。總的來說就是,子專案在容器沒有足夠空間去排列的情況下依然能保持在容器之內,並且不會溢位。

為了在預設情況下獲得最好的展示效果,flex-basis 屬性的預設值被設定成 auto,我們會在這個系列的其他文章中好好了解這代表什麼。現在,你只需要將 auto 理解為『大到足夠適應容器』就行了。在這種情況下,當 flex 容器中有一些子專案,其中的一個子專案相較於其他包含更多的內容,那麼它會被分配更多的空間。

HTML:

<div class="container">
  <div class="item">Two words</div>
  <div class="item">Now three words</div>
  <div class="item">
    This flex item has a lot of content and so it is going to need more space in the flex container.
    </div>
</div>
複製程式碼

CSS:

body {
  padding: 20px;
  font: 1em Helvetica Neue, Helvetica, Arial, sans-serif;
}

* {box-sizing: border-box;}

p {
  margin: 0 0 1em 0;
}

.container {  
  border: 5px solid rgb(111,41,97);
  border-radius: .5em;
  padding: 10px;
  display: flex;
  width: 400px;
}

.item {
  padding: 10px;
  background-color: rgba(111,41,97,.3);
  border: 2px solid rgba(111,41,97,.5);
}
複製程式碼

請看 Rachel Andrew(@rachelandrew)在 CodePen 上的這個例子:Smashing Flexbox Series 1: initial values of flex items

上面這個例子就是彈性佈局靈活性的體現。在 flex-basis 屬性的預設值是 auto,並且子專案(flex item)沒有設定尺寸的情況下,它就會有一個 max-content 的基礎尺寸。這就是它根據內容自動延伸之後沒有經過任何其他包裝的尺寸。然後,子專案按照比例來佔據剩餘空間,詳見以下 flexbox 規範中的說明。

『注意:分配收縮空間時,是根據基本尺寸乘以彈性收縮係數(flex shrink factor)。收縮空間的大小與子專案設定的 flex-shrink 的大小成比例。比如說有一個較小的子專案,在其他較大的子專案明顯收縮之前,是不會被收縮到沒有空間的。』

也就是說較大的子專案收縮的空間更多,那麼現在我們得到了最終的佈局結果。你可以比較一下下面兩個截圖,都是使用上面的例子,不同的是在第一個截圖中,第三個盒子內容較少佔據的空間較小,因此相對的每一列的佔據的空間更均勻一些。

這是較大的子專案佔據更多空間的例子

其他項為了給較大的一項提供空間而自動收縮(Large preview)。

彈性佈局會試圖幫助我們獲得一個合理的最終顯示結果,而不需要寫 CSS 的人來定義。它不會平均的減少每行的寬度,從而形成一個每行只有幾個單詞的很高的子專案,而是會給該子專案分配更多的空間用以展示其內容。這種表現正是如何正確彈性佈局的關鍵。它最適用於用於沿著一條軸線排列的元素,以一種靈活和感知內容的方式。這裡我簡單介紹了一點細節,但在接下來的系列文章中我們會更加深入的瞭解這些演算法。

總結

在這篇文章中,我用彈性佈局的屬性的一些預設值來介紹當你設定 display: flex 的時候,究竟發生了什麼。令人驚訝的是,當你逐步分解之後,發現它原來有這麼多內容,並且這些內容就包含了彈性佈局的核心特點。

彈性佈局是非常靈活的:它會根據你的內容自動地做出不錯的選擇 —— 通過收縮和拉伸達到最好的展示效果。彈性佈局還能感知書寫模式:佈局中行和列的方向跟書寫模式有關。彈性佈局通過分配空間,允許子專案在主軸(main axis)上以整體的的方式來對齊。它還允許子專案按照交叉軸來對齊,使得交叉軸上的專案相互關聯。更重要的是,彈性佈局知道你的內容有多大,並且儘量採用更好的方式來展示你的內容。在接下來的文章中,我們會更加深入的探索,思考我們什麼時候以及為什麼要用彈性佈局。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章