前言
Flex想必大家都很熟悉,也是大家平時在進行頁面布局的首選方案。(反正我是!)。不知道大家平時在遇到Flex布局屬性問題時,是如何查閱并解決的。反正,我每次記不住哪些屬性或者對哪些屬性的用法忘記時。我總是求助于阮一峰老師寫的Flex 布局教程:語法篇[1]。
其實,對于css來講,大家都抱著一種「死記硬背」的東西來對待它。久而久之,就會出現上述我說的問題,一個屬性或者一個使用案例,需要去指定的網站去查詢。這算是好的呢,有些同學沒有自己的知識體系或者收藏資料。 每次遇到問題,都是bAIdu/google一下,然后CV大發一通。
其實,我們應該把將 CSS 視為一組布局模式。每種布局模式都是一個可以實現或重新定義每個 CSS 屬性的「算法」。我們使用 CSS 聲明(鍵/值對)提供算法,算法決定如何使用它們。
換句話說,我們編寫的 CSS 是這些算法的輸入,就像傳遞給函數的參數一樣。如果我們想真正熟悉 CSS,僅僅學習屬性是不夠的;我們必須學習算法如何使用這些屬性。
只有,我們在對一些布局模式有了一定的掌握之后,我們才會在遇到類似的問題,游刃有余的處理問題。或者說像調用函數一樣,輸入特定的參數,得到特定的結果。
所以,今天我們來換一種對Flex的思考角度,對它來一次深度解析。
還有一點,需要說明,下文中不會設計到特有屬性的介紹,并且還需要大家對Flex布局有一點的知識儲備。
比方說,下圖中標注的一些概念下文中就不會過多介紹了。推薦大家先把阮老師的那個文章通讀幾遍,對Flex有一個大體的了解在閱讀下文。
好了,天不早了,干點正事哇。
我們能所學到的知識點
前置知識點
Flexbox 是個啥?
Flex Direction
對齊(Alignment)
假設大小(Hypothetical size)
增長(Grow)和萎縮(Shrink)
最小尺寸的陷阱
間距
包裹
1. 前置知識點
「前置知識點」,只是做一個概念的介紹,不會做深度解釋。因為,這些概念在下面文章中會有出現,為了讓行文更加的順暢,所以將本該在文內的概念解釋放到前面來。「如果大家對這些概念熟悉,可以直接忽略」同時,由于閱讀我文章的群體有很多,所以有些知識點可能「我視之若珍寶,爾視只如草芥,棄之如敝履」。以下知識點,請「酌情使用」。
CSS 布局算法
CSS 有不同的模式,確定它如何在頁面上布局元素。這些模式通常被稱為布局算法或布局模式。
在 CSS 中有七種布局模式,下圖是MDN_CSS_Layout_Mode[2]的描述
其中Multi-column layout估計大家沒咋接觸過,剩余的或多或少在我們平時開發中都有接觸過。
其中四種被使用最多。流動、定位、flex和grid。
流動布局(Flow Layout)
默認情況下,CSS 使用所謂的流動布局算法(也稱Normal flow)。流動將頁面上的每個元素都視為屬于文本文檔。
塊級元素以垂直方式在頁面上重疊顯示。它們會盡量占用盡可能多的水平空間,同時盡量減少垂直空間的占用。
內聯元素在水平方向上像段落中的文本一樣顯示在一起。它們通常具有固定的寬度和高度,這就是為什么許多其他我們可能想要使用的屬性在這些元素上不起作用的原因。我們可以通過將它們的顯示屬性更改為inline-block來更改此行為。
定位布局
如果在元素上使用 position 屬性,我們現在正在要求 CSS 根據定位布局算法顯示該元素。在此布局模式中,我們可以請求幾種不同類型的行為:
- 靜態(Static)
- 相對(Relative)
- 絕對(Absolute)
- 固定(Fixed)
- 粘性(Sticky)
絕對定位元素往往因為在其他地方無法正常工作而被認為是一種hacky的解決方案。
還有一點需要注意,根據我們使用的值的不同,我們可能需要「考慮元素的父級」。例如,在絕對定位元素中,該元素相對于其最近的定位布局祖先定位。這意味著 CSS 將查找 html 樹并找到最近的一個祖先,「該祖先也使用了這些值之一」。如果找不到,則絕對定位元素將相對于視口定位。
彈性盒布局
當 display 屬性設置為 flex 時,元素將根據彈性盒布局算法布置其子元素。
而它就是我們今天要講的重點,下文中有更多的介紹。
如果想了解更多的Flex的細節,可以參考w3c_flexbox[3]。
網格布局
網格與彈性盒類似,只要在元素上使用了 display: grid,就會開始使用網格布局算法。此布局算法將根據網格布局算法顯示所有子元素。
Grid 和 Flexbox 的區別在于,Grid 適用于布局具有列和行的二維內容,而 Flexbox 適用于布局具有「一維內容」,即單個列或行。
我們后面也會有針對Grid的文章,預估在 12 月份或者明年 1 月份。
替換元素
在 CSS 中,替換元素(Replaced Element)是指一個由瀏覽器根據元素的標簽和屬性創建的、在渲染時展示的元素,而「不是由文檔中的內容決定其顯示的元素」。這些元素通常是具有外部資源(如圖像或嵌入式框架)的元素,其內容由瀏覽器根據其屬性和上下文動態生成。
以下是一些常見的替換元素:
「<img> 元素:」通過src屬性引用外部圖像。
<img src="image.jpg" alt="Description" />
「<audio> 和 <video> 元素:」通過src屬性引用外部音頻或視頻文件。
<audio controls>
<source src="audio.mp3" type="audio/mp3" />
</audio>
<video width="320" height="240" controls>
<source src="movie.mp4" type="video/mp4" />
</video>
「<iframe> 元素:」通過src屬性引用外部網頁或嵌入式內容。
<iframe src="https://example.com"></iframe>
「<object> 元素:」用于嵌入外部資源,如 Flash 動畫。
<object data="flash.swf" type="Application/x-shockwave-flash">
<!-- fallback content or alternate content -->
</object>
「<canvas> 元素:」通過 JAVAScript 繪制圖形。
<canvas width="200" height="200"></canvas>
替換元素與非替換元素的主要區別在于,替換元素的渲染不依賴于文檔的其他部分。它們的外觀和尺寸通常由其屬性和外部資源決定。替換元素具有一定的固有尺寸,不受文本或子元素的影響。
在 CSS 中,替換元素還可以通過 object-fit 和 object-position 這樣的屬性進行進一步控制,以指定元素的替換內容的顯示方式。例如:
img {
object-fit: cover; /* 圖片按比例縮放并覆蓋整個容器 */
object-position: center; /* 圖片在容器中居中顯示 */
}
2. Flexbox 是個啥?
CSS 由許多不同的布局算法組成,官方稱之為布局模式?!该糠N布局模式都是 CSS 中的一種小型子語言」。默認布局模式是流式布局,但我們可以通過更改父容器上的display屬性來選擇使用Flexbox:
display:block
display:flex
當我們將 display 設置為 flex 時,我們創建了一個flex格式化上下文。這意味著,默認情況下,「所有子元素將根據 Flexbox 布局算法定位」。
每種布局算法都是為解決特定問題而設計的。默認的Flow布局旨在創建數字文檔;它本質上是Microsoft word的布局算法?!笜祟}和段落以塊的形式垂直堆疊,而文本、鏈接和圖像等元素則不顯眼地位于這些塊內部」。
Flexbox專注于在行或列中排列一組項目,并提供對這些項目的分布和對齊具有極大控制權。正如其名稱所示,Flexbox關注的是靈活性。我們可以控制項目是增長還是收縮,額外空間如何分配等。
3. Flex Direction
如前所述,Flexbox的關鍵在于「控制在行或列中元素的分布」。默認情況下,項目將在「一行中側邊堆疊」,但我們可以通過使用flex-direction屬性切換到列:
flex-direction:row
flex-direction:column
使用flex-direction: row時,「主軸水平運行,從左到右」。當我們切換到flex-direction: column時,「主軸垂直運行,從上到下」。
在Flexbox中,一切都「基于主軸」。算法不關心垂直/水平,甚至不關心行/列。所有規則都圍繞這個主軸以及垂直運行的交叉軸結構。
我們可以輕松切換水平布局到垂直布局。所有規則都會「自動適應」。這個特性是 Flexbox 布局模式獨有的。
子元素將「默認」根據以下兩個規則定位:
- 主軸(Primary Axis):子元素將「緊密」排列在容器的「起始位置」。
- 交叉軸(Cross Axis):子元素將「伸展」以「填充整個容器」。
在Flexbox中,我們決定主軸是水平運行還是垂直運行。這是「所有 Flexbox 計算的基準」。
4. 對齊(Alignment)
我們可以使用justify-content屬性來改變「子元素沿主軸」的分布方式:
由于主軸是row和column的情況很類似,下文中我們都按主軸為row來講解
當涉及到主軸時,我們通常不考慮對齊單個子元素。相反,重點是關于整個組的分布。
我們可以將所有項目緊密堆疊在特定位置(使用flex-start、center和flex-end),或者我們可以將它們分開(使用space-between、space-around和space-evenly)。
對于交叉軸,情況有些不同。我們使用align-items屬性:
在align-items中,有一些與justify-content相同的選項,但并「沒有完全的重疊」。
為什么它們不共享相同的選項呢?我們將很快揭開這個謎團,但首先,我需要分享另一個對齊屬性:align-self。
與justify-content和align-items不同,align-self應用于子元素,而不是容器。它允許我們沿著交叉軸改變特定子元素的對齊方式:
align-self具有與align-items完全相同的值。實際上,它們改變的是完全相同的內容。
align-items是一種語法糖,是一種方便的簡寫,可以「一次性自動設置所有子元素的對齊方式」。
Content VS items
在 Flexbox 中,項目沿著主軸分布?!改J情況下,它們很好地排列在一起,側邊相鄰」。我可以畫一條直線,將所有子元素串起來,就像烤肉一樣:
然而,交叉軸是不同的。「一條垂直的直線只會與其中一個子元素相交」。
這更像是垂直方向用牙簽串的烤腸,而不是烤肉串:
這里有一個顯著的區別。對于烤腸而言,「每個項目都可以沿著它的棍子移動,而不會干擾其他項目」:
相比之下,通過我們的主軸串聯每個兄弟元素,一個單獨的項目如果要移動位置,那勢必會影響周圍兄弟元素的。
這是主軸和交叉軸之間的基本區別。當我們討論交叉軸上的對齊時,每個項目都可以隨心所欲。然而,在主軸上,我們「只能考慮如何分配整個組」。
針對上面的內容,我們可以給出一個正確的定義:
- justify — 沿「主軸定位」某物。
- align — 沿「交叉軸定位」某物。
- content — 「一組」可以被分配的“東西”。
- items — 可以「單獨定位」的單個項目。
因此:我們有justify-content來控制沿主軸分配整個組,我們有align-items來沿交叉軸單獨定位每個項目。這是我們用來管理 Flexbox 布局的兩個主要屬性。
當涉及到主軸時,我們必須將項目視為一個組,作為可以分配的內容。
5. 假設大小(Hypothetical size)
假設我有以下的 CSS:
.item {
width: 2000px;
}
我們第一直覺就是「我們將得到一個寬度為 2000 像素的項目」。其實這句話是不對的!
讓我們用一個例子來說明。
<style>
.flex-wrapper {
display: flex;
}
.item {
width: 2000px;
}
</style>
<div class="item"></div>
<div class="flex-wrapper">
<div class="item"></div>
</div>
結果缺不一樣。
兩個項目都應用了完全相同的 CSS。它們都有width: 2000px。然而,第一個項目比第二個項目寬得多!
差異在于「布局模式」。第一個項目是使用流式布局(flow)渲染的,在流式布局中,width是一個「硬性約束」。當我們設置width: 2000px時,我們肯定能到一個寬度為 2000 像素的元素,即使它已經超過當前視口的寬度。
然而,在 Flexbox 中,width屬性的實現方式不同。這「更像是一個建議而不是硬性約束」。
規范對此有一個名字:「假設大小」(Hypothetical size)。
在這種情況下,限制因素是父元素沒有足夠的空間容納一個寬度為 2000px 的子元素。因此,子元素的大小被縮小,以「適應空間」。
這是 Flexbox 哲學的核心部分?!甘挛锸橇鲃雍挽`活的,可以根據世界的限制進行調整」。
6. 增長(Grow)和萎縮(Shrink)
要真正了解 Flexbox 的流動性,我們需要討論三個屬性:flex-grow、flex-shrink和flex-basis。
flex-basis
在 Flex行中,flex-basis的作用與width相同。在 Flex 列中,flex-basis的作用與height相同。
「Flexbox 中的一切都與主/交叉軸有關」。例如,justify-content將沿主軸分布子元素,無論主軸是水平還是垂直,它的工作方式都完全相同。
然而,width和height不遵循此規則!width「始終會影響水平尺寸」。當我們將flex-direction從row切換到column時,它不會突然變成height。
因此,Flexbox 創建了一個通用的“大小”屬性,稱為flex-basis。它就像width或height,但與其他所有屬性一樣,「與主軸相關聯」。它允許我們設置元素在主軸方向上的假設大小,無論這是水平還是垂直。
下圖集中,每個子元素都被賦予了flex-basis: 50px,但可以調整第一個子元素的flex-basis。
就像我們在width中看到的那樣,flex-basis更像「是一個建議而不是一個硬性約束」。在某個時候,所有元素都沒有足夠的空間來保持它們被分配的大小,因此「它們必須妥協,以避免溢出」。
一般來說,在 Flex 行中,我們可以互換使用width和flex-basis,但也有一些例外情況。例如,width屬性對替換元素(如圖像)的影響與flex-basis不同。此外,width可以將項目減小到其最小尺寸以下,而flex-basis則不能。
flex-grow
默認情況下,Flex 上下文中的元素將縮小到它們在主軸上的「最小舒適尺寸」。這通常「會創建額外的空間」。
我們可以使用flex-grow屬性指定如何使用該空間:
flex-grow的「默認值是 0」,這意味著增長是可選的。如果我們希望「子元素吞并容器中的任何額外空間」,我們需要明確告訴它。
如果多個子元素設置了flex-grow怎么辦?在這種情況下,「額外的空間將根據它們的flex-grow值成比例地分配給子元素」。
當單個子元素被賦予正的flex-grow值時,它將「吞并所有額外的空間」。在這種情況下,數字是無關緊要的:1 和 1000 具有相同的效果。
flex-shrink
在我們迄今為止看到的大多數示例中,我們有額外的空間可以使用。如果我們的子元素太大而父容器無法容納怎么辦?
<<< 左右滑動見更多 >>>
兩個項目都會收縮,但它們會「按比例收縮」。第一個子元素始終是第二個子元素寬度的 2 倍。
flex-basis和width設置了元素的假設大小。Flexbox算法可能會「將元素收縮到低于這個期望大小」,但「默認情況下,它們將始終按比例縮放,保持兩個元素之間的比例」。
如果我們不希望元素按比例縮小,可以使用flex-shrink屬性。
現在我們有兩個子元素,每個都有一個假設大小為 250px。容器至少需要 500px 寬度,以便將這些子元素以其假設大小容納其中。
假設我們將容器縮小到 400px。嗯,我們不能把 500px 的內容塞進一個 400px 的袋子里!我們有 100px 的虧空。為了使它們適應,我們的元素將需要放棄總共 100px。
flex-shrink屬性讓我們決定如何處理這個虧空。
與flex-grow類似,它是一個比例?!改J情況下,兩個子元素的flex-shrink都是 1,因此每個子元素消化虧空的一半」。它們各自放棄 50px,它們的實際大小從 250px 縮小到 200px。
現在,假設我們將第一個子元素提高到flex-shrink: 3:
我們總的虧空是 100px。通常,每個子元素將支付 1/2,但由于我們已經調整了flex-shrink,第一個元素最終支付了 3/4(75px),第二個元素支付了 1/4(25px)。
「絕對值并不重要,一切都取決于比例」。如果兩個子元素都具有flex-shrink: 1,每個子元素將支付總虧空的 1/2。如果兩個子元素都增加到flex-shrink: 1000,每個子元素將支付總虧空的 1000/2000。無論如何,最終效果都是相同的。
對flex-shrink:我們可以將其視為flex-grow的“反面”。它們是同一硬幣的兩面:
- flex-grow 控制當項目小于其容器時額外空間的「分配方式」。
- flex-shrink 控制項目大于其容器時空間的「移除方式」。
這意味著這兩個屬性中只能有一個生效。如果有額外的空間,flex-shrink沒有影響,因為項目不需要縮小。如果子元素太大而無法容納,flex-grow沒有影響,因為沒有額外的空間可分配。
防止縮小
有時,我們不希望 Flex 子元素縮小。
讓我們看一個例子:
當容器變窄時,我們的兩個圓形被擠變形了。如果我們希望它們保持圓形怎么辦?
我們可以通過設置flex-shrink: 0來實現:
當我們將flex-shrink設置為 0 時,實質上我們「完全退出了縮小過程」。Flexbox 算法將flex-basis(或width)視為硬最小限制。
7. 最小尺寸的陷阱
假設我們正在構建一個搜索表單:
當容器縮小到一定程度以下時,內容溢出!
「根本原因是flex-shrink 的默認值是 1」,我們在示例中設置了該屬性,按道理輸入框應該能夠縮小到它需要的程度!但是卻事與愿違。
原因是:除了假設大小之外,Flexbox 算法還關心另一個重要的大?。骸缸钚〈笮 ?。
Flexbox算法拒絕將子元素縮小到其最小大小以下。無論我們如何增加flex-shrink,內容將溢出而不是繼續縮小!
文本輸入框的默認最小大小為 170px-200px(在不同的瀏覽器之間有所變化)。
在其他情況下,限制因素可能是元素的內容。
對于包含文本的元素,最小寬度是最長不可斷開的字符串的長度。
好消息是:我們可以「使用min-width屬性重新定義最小大小」。
通過直接在 Flex 子元素上設置min-width: 0px,我們告訴 Flexbox 算法覆蓋內置的最小寬度。因為我們將其設置為 0px,所以元素可以縮小到必要的程度。
8. 間距
gap允許我們在每個 Flex 子元素之間創建空間。
這對于諸如導航標題之類的東西非常有用:
自動邊距
margin屬性用于在特定元素周圍添加空間。在某些布局模式中,如 Flow 和Positioned(前面都有過介紹),它甚至可以用于通過margin: auto將元素居中。
在 Flexbox 中,自動邊距變得更加有趣:
「自動邊距將吞噬額外的空間,并將其應用于元素的邊距」。它使我們能夠精確控制在哪里分配額外的空間。
一個常見的頁眉布局特點是在一側放置標志,而在另一側放置一些導航鏈接。
<style>
ul {
display: flex;
gap: 12px;
}
li.logo {
margin-right: auto;
}
</style>
<nav>
<ul>
<li class="logo">
<a href="/"> 首頁 </a>
</li>
<li>
<a href=""> 語言 </a>
</li>
<li>
<a href=""> 個人中心 </a>
</li>
</ul>
</nav>
ul {
list-style-type: none;
}
ul a {
text-decoration: none;
}
圖片
列表中的第一項通過給它設置margin-right: auto,我們「聚集了所有額外的空間,并強制將其放在第一項和第二項之間」。
使用瀏覽器devtool來查看元素信息。
9. 包裹
到目前為止,我們的所有項目都是并排或縱列的。flex-wrap屬性允許我們改變這一點。
如果容器寬度不能包含子元素的話,子元素會被隱藏。
我們可以通過設置flex-wrap:wrap來讓子元素自動換行。
當我們設置flex-wrap: wrap時,項目不會收縮到其假設大小以下。
使用flex-wrap: wrap,我們「不再有一個可以穿過每個項目的單一主軸線」。實際上,「每一行都充當其自己的小型 Flex 容器」。
當我們有多行時,交叉軸現在可能與多個項目相交!
每一行都是其自己的小型 Flexbox 環境。align-items將在包圍每一行的無形框內上下移動每個項目。
但如果我們想對齊行本身怎么辦?我們可以使用align-content屬性:
總結一下這里發生的情況:
- flex-wrap: wrap給我們兩行東西。
- 在每一行內,align-items允許我們將每個單獨的子項上下滑動。
- 然而,在整體上,我們有兩行在一個單一的 Flex 上下文內!現在,交叉軸將與兩行相交,而不是一行。因此,我們不能單獨移動行,我們需要將它們作為一個組進行分配。
- 使用我們上面的定義,我們正在處理內容,而不是項目。但我們仍然在談論交叉軸!因此,我們想要的屬性是align-content。
Reference
[1]Flex 布局教程:語法篇:https://www.ruanyifeng.com/blog/2015/07/flex-grammar.html
[2]MDN_CSS_Layout_Mode:https://developer.mozilla.org/en-US/docs/Web/CSS/Layout_mode
[3]w3c_flexbox:https://www.w3.org/TR/css-flexbox-1/