CSS 佈局教程:從經典方法到最新技術
已發表: 2022-03-11在不掌握 CSS 的情況下掌握網頁佈局與學習在旱地游泳一樣可行。 但與游泳不同——一旦掌握了它,你就會終生掌握這項技能——掌握 CSS 是一個永遠不會真正結束的過程,因為 CSS 本身在不斷發展。
不同瀏覽器(甚至同一瀏覽器的不同版本)在 CSS 實現和支持方面的差異以及 CSS 建議採用率的不同加劇了挑戰。 十多年來,網頁設計師和開發人員一直在努力應對每個新瀏覽器版本支持的零星且不一致的附加 CSS3 功能。
但無論如何,掌握 CSS 對於任何可靠的網頁設計師或開發人員來說都是絕對必要的。 本文將引導您了解一些基本的 CSS 佈局原則,從經典的 CSS2 技術到最新的 CSS3 佈局方法。
注意:本文中的所有代碼示例都使用 HTML5 元素和 Sass 語法。 完整的工作代碼可以從 https://github.com/laureanoarcanio/css-layout-examples 克隆。
用例
學習技術的最佳方法之一是擁有一個你試圖支持的特定用例或一個你試圖解決的特定問題。 為此,我們將專注於具有一組特定要求的用例。
我們的用例由具有一些動態行為的 Web 應用程序佈局組成。 它將在頁面上具有固定元素,例如頁眉、頁腳、導航菜單和子導航,以及可滾動的內容部分。 具體佈局要求如下:
- 基本佈局
- 頁眉、頁腳、導航菜單和子導航都保持在滾動狀態
- 導航和子導航元素佔據所有垂直的空閒空間
- 內容部分使用頁面上所有剩餘的可用空間並具有可滾動區域
- 動態行為
- 導航菜單默認僅顯示圖標,但也可以展開以顯示文本(然後可以折疊以再次僅顯示圖標)
- 佈局變化
- 有些頁面在導航菜單旁邊有子導航,有些則沒有
使用經典 CSS2 技術的 CSS 教程
對於初學者,這是我們將在使用經典 CSS 的示例實現中使用的 HTML5 標記:
<body class="layout-classic"> <header></header> <nav></nav> <aside></aside> <main></main> <footer></footer> </body>
固定定位
在 CSS2 中,我們可以通過採用使用固定定位的定位佈局模型來實現頁面上的固定元素(頁眉、頁腳等)。
此外,我們將使用z-index
CSS 屬性來確保我們的固定元素保持在頁面上其他內容的“頂部”。 z-index
屬性指定元素的堆棧順序,其中具有較大堆棧順序的元素始終位於具有較低堆棧順序的元素的“頂部”。 請注意, z-index
屬性僅適用於定位元素。 對於我們的示例,我們將任意使用 20 的z-index
值(高於默認值)以確保我們的固定元素在視覺上保持在最前面。
此外,我們將width
屬性設置為 100%,這指示瀏覽器將所有可用空間水平用於元素。
#header, #footer { position: fixed; width: 100%; z-index: 20; } #header { top: 0; height: 5em; } #footer { bottom: 0; height: 3em; }
好的,這就是頁眉和頁腳。 但是#nav
和#subnav
呢?
CSS 擴展
對於#nav
和#subnav
,我們將使用一種稱為CSS 擴展的稍微複雜一點的技術,可以在將元素定位為固定(即,在頁面上的固定位置)或絕對(即,在相對於其最近定位的祖先或包含塊的指定位置)。
垂直擴展是通過將元素的top
和bottom
屬性都設置為固定值來實現的,因此元素將垂直擴展以相應地使用剩餘的垂直空間。 基本上你正在做的是將元素的頂部與頁面頂部的特定距離和元素底部與頁面底部的特定距離綁定,因此元素會擴展以填充整個垂直空間在這兩點之間。
類似地,水平擴展是通過將元素的left
和right
屬性都設置為固定值來實現的,因此元素會水平擴展以相應地使用剩餘的水平空間。
對於我們的用例,我們需要使用垂直擴展。
#nav, #subnav { position: fixed; top: 6em; /* leave 1em margin below header */ bottom: 4em; /* leave 1em margin above footer */ z-index: 20; } #nav { left: 0; width: 5em; } #subnav { left: 6em; /* leave 1em margin to right of nav */ width: 13em; }
默認(靜態)定位
主要的可滾動內容區域可以簡單地依賴默認(靜態)定位,從而元素按照它們在文檔流中出現的順序呈現。 由於我們頁面上的其他所有內容都處於固定位置,因此該元素是文檔流中唯一的元素。 因此,要正確定位它,我們需要做的就是指定它的margin
屬性,以避免與固定的頁眉、頁腳和導航/子導航重疊:
#main { margin: 6em 0 4em 20em; }
至此,我們已經滿足了使用 CSS2 的用例的基本佈局要求,但我們仍然需要滿足動態功能的附加要求。
使用經典 CSS2 技術的動態行為
要求規定我們的導航菜單默認只顯示圖標,但也可以展開以顯示文本(然後可以折疊以再次只顯示圖標)。
讓我們首先在導航菜單展開時將其寬度添加5em
。 我們將通過創建一個“擴展的” CSS 類來做到這一點,我們可以從導航菜單元素中動態添加或刪除它:
#nav { left: 0; width: 5em; &.expanded { /* Sass notation */ width: 10em; } }
下面是一個 JavaScript 代碼示例(在此示例中,我們使用 jQuery),可用於根據用戶單擊導航切換圖標在展開和折疊模式之間動態切換導航菜單:
$('.layout-classic #nav').on('click', 'li.nav-toggle', function() { $('#nav'').toggleClass('expanded'); });
有了這個,我們的導航菜單現在可以動態展開或折疊。 偉大的。
嗯,有點好,但不完全。 儘管導航菜單現在可以擴展和收縮,但它不能很好地與頁面的其餘部分配合使用。 擴展的導航菜單現在與子導航重疊,這絕對不是所需的行為。
這揭示了 CSS2 的主要限制之一; 也就是說,需要用固定位置值硬編碼的東西太多了。 因此,對於頁面上需要重新定位以適應擴展導航菜單的其他元素,我們需要使用更多固定位置值定義額外的“擴展”CSS 類。
#subnav { left: 6em; width: 13em; &.expanded { left: 11em; /* move it on over */ } } #main { margin: 6em 0 4em 20; z-index: 10; &.expanded { margin-left: 25em; /* move it on over */ } }
然後,我們需要擴展我們的 JavaScript 代碼,以便在用戶單擊導航切換時添加對這些其他元素的動態調整:
$('.layout-classic #nav').on('click', 'li.nav-toggle', function() { $('#nav, #subnav, #main').toggleClass('expanded'); });
好的,這樣效果更好。
使用經典 CSS2 技術的佈局變化
現在讓我們解決一些頁面隱藏子導航菜單的要求。 具體來說,我們希望在用戶單擊主導航區域中的“用戶”圖標時隱藏子導航菜單。
所以首先,我們將創建一個應用display: none
的新類“hidden”:
.hidden { display: none; }
同樣,當用戶單擊用戶圖標時,我們將使用 JavaScript (jQuery) 將“隱藏”CSS 類應用於#subnav
元素:
$('#nav.fa-user').on('click', function() { $('#subnav').toggleClass('hidden'); });
通過此添加,當用戶單擊“用戶”圖標時, #subnav
元素會正確隱藏,但它佔用的空間仍然未使用,而不是其他元素擴展以使用由#subnav
元素騰出的空間。
為了在隱藏#subnav
元素時獲得所需的行為,我們將使用一種鮮為人知但非常有用的 CSS 選擇器,稱為相鄰兄弟選擇器。
相鄰兄弟 CSS 選擇器
相鄰兄弟選擇器允許您指定兩個元素,僅選擇緊跟在指定第一個元素之後的第二個元素的那些實例。
例如,以下將僅選擇 ID 為main
且緊跟ID 為subnav
的元素的那些元素:
#subnav + #main { margin-left: 20em; }
上面的 CSS 片段將20em
#main
且僅當它緊跟在顯示的#subnav
。
然而,如果#nav
被擴展(這導致expanded
的類也被添加到#main
,基於我們之前的代碼),我們將#main
的左邊距移動到25em。
#subnav + #main.expanded { margin-left: 25em; }
並且,如果#subnav
被隱藏,我們將#main
的左邊距一直移動到 6em 以緊鄰#nav
:
#subnav.hidden + #main { margin-left: 6em; }
(注意:使用相鄰兄弟選擇器的一個缺點是它迫使我們始終在 DOM 中存在#subnav
,無論它是否被顯示。)
最後,如果#subnav
被隱藏而#nav
被展開,我們將11em
#main
#subnav.hidden + #main.expanded { margin-left: 11em; }
這使我們能夠在沒有任何繁重的 JavaScript 代碼的情況下將它們連接在一起,但我們也可以看到如果我們向頁面添加更多元素,這段代碼會變得多麼複雜。 我們再次看到,使用 CSS2,需要對位置值進行大量硬編碼才能使事情正常工作。
利用 CSS3
CSS3 提供了顯著增強的功能和佈局技術,使其更易於使用並且更少依賴硬編碼值。 CSS3 本質上是為支持更多動態行為而設計的,從這個意義上說,它更“可編程”。 讓我們檢查其中一些與我們的用例相關的新功能。
CSS3 calc()
函數
新的 CSS3 calc()
函數可用於動態計算 CSS 屬性值(但請注意,支持因瀏覽器而異)。 提供給calc()
函數的表達式可以是使用標準運算符優先級規則結合基本算術運算符( +
、 -
、 *
、 /
)的任何簡單表達式。
使用calc()
函數可以幫助避免 CSS2 所需的大量硬編碼值。 在我們的例子中,這使我們能夠更動態地實現 CSS 擴展。 例如:
#nav, #subnav { position: fixed; height: calc(100% - 10em); /* replaces */ z-index: 20; }
通過使用calc()
函數的上述height
規範,我們實現了與 CSS2 中使用top:6em
和bottom:4em
相同的結果,但是以更加靈活和自適應的方式,並且不需要硬編碼top
和bottom
位置價值觀。
CSS3 彈性盒佈局
Flexbox 是 CSS3 中引入的一種新佈局(支持因瀏覽器而異)。 flexbox 佈局使在頁面上以可預測的方式排列不同屏幕尺寸、分辨率和設備的元素變得更加簡單。 因此,它在響應式網頁設計的上下文中特別有用。
主要特點包括:
- 定位子元素要容易得多,複雜的佈局可以更簡單地實現,代碼更簡潔。
- 子元素可以在任何方向佈置,並且可以具有靈活的尺寸以適應顯示空間。
- 子元素會自動擴展契約以適應可用的空閒空間。
Flexbox 引入了自己獨特的一組術語和概念。 其中一些包括:
- 彈性容器。 一個
display
屬性設置為flex
或inline-flex
的元素,用作 flex 項的容器元素。 - 彈性項目。 彈性容器中的任何元素。 (注意:直接包含在 flex 容器中的文本被包裹在一個匿名的 flex 項中。)
- 軸。 每個 flexbox 佈局都有一個
flex-directio
,它指定了 flex 項目沿其佈局的主軸。 則橫軸是垂直於主軸的軸。 - 線條。 根據
flex-wrap
屬性,Flex 項目可以佈置在單行或多行上。 - 方面。 flexbox 對應的 height 和 width 是
main size
和cross size
,它們分別指定了 flex 容器的主軸和交叉軸的大小。
好的,有了這個簡短的介紹,如果我們使用 flexbox 佈局,我們可以使用以下替代標記:

<body class="layout-flexbox"> <header></header> <div class="content-area"> <nav></nav> <aside></aside> <main></main> </div> <footer></footer> </body>
對於我們的示例用例,我們的主要佈局(頁眉、內容、頁腳)是垂直的,因此我們將設置我們的 flexbox 以使用列佈局:
.layout-flexbox { display: flex; flex-direction: column; }
雖然我們的主佈局是垂直的,但我們的內容區域(nav、subnav、main)中的元素是水平佈局的。 每個 flex 容器只能定義一個方向(即,它的佈局必須是水平的或垂直的)。 因此,當佈局需要的不僅僅是這個(應用程序佈局的常見情況),我們可以將多個容器嵌套在另一個容器中,每個容器都有不同的方向佈局。
這就是為什麼我們添加了一個額外的容器(我稱之為content-area
)包裝#nav
、 #subnav
和#main
。 這樣,整體佈局可以是垂直的,而內容區域的內容可以是水平的。
現在為了定位我們的 flex 項目,我們將使用屬性flex
,它是 flex 的簡寫flex: <flex-grow> <flex-shrink> <flex-basis>;
. 這三個 flex 屬性決定了我們的 flex 項目如何在流動方向上分配它們之間剩餘的任何可用空間,如下所示:
- flex-grow:指定一個項目相對於同一容器內的其他靈活項目可以增長多少
- flex-shrink:指定一個項目相對於同一容器內的其他靈活項目如何收縮
- flex-basis:指定項目的初始大小(即在收縮或增長之前)
將flex-grow
和flex-shrink
都設置為零意味著項目的大小是固定的,它不會增長或縮小以適應更多或更少的可用空間。 這是我們為頁眉和頁腳所做的,因為它們具有固定的大小:
#header { flex: 0 0 5em; } #footer { flex: 0 0 3em; }
要讓項目佔用所有可用空間,請將其flex-grow
和flex-shrink
值都設置為 1 並將其flex-basis
值設置為auto
。 這就是我們為內容區域所做的,因為我們希望它佔用所有可用的空閒空間。
正如我們之前所說,我們希望content-area
中的項目按行方向佈局,因此我們將添加display: flex
; 和flex-direction: row;
. 這使得 content-area 成為#nav
、 #subnav
和 `#main 的新彈性容器。
所以這就是我們最終為content-area
的 CSS 得到的結果:
.content-area { display: flex; flex-direction: row; flex: 1 1 auto; /* take up all available space */ margin: 1em 0; min-height: 0; /* fixes FF issue with minimum height */ }
在內容區域中, #subnav
#nav
固定的大小,因此我們只需相應地設置flex
屬性:
#nav { flex: 0 0 5em; margin-right: 1em; overflow-y: auto; } #subnav { flex: 0 0 13em; overflow-y: auto; margin-right: 1em; }
(請注意,我在這些 CSS 規範中添加了overflow-y: hidden
以克服內容超出和溢出容器高度。Chrome 實際上不需要這個,但 FireFox 需要。)
#main
將佔用剩餘的可用空間:
#main { flex: 1 1 auto; overflow-y: auto; }
這一切看起來都不錯,所以現在讓我們將動態行為添加到其中,看看效果如何。
JavaScript 與我們之前的相同(除了這裡,我們指定的 CSS 元素容器類是layout-flexbox
而之前是layout-classic
):
$('.layout-flexbox #nav').on('click', 'li.nav-toggle', function() { $('#nav').toggleClass('expanded'); });
我們將expanded
類添加到 CSS 中,如下所示:
#nav { flex: 0 0 5em; /* collapsed size */ margin-right: 1em; overflow-y: auto; &.expanded { flex: 0 0 10em; /* expanded size */ } }
瞧!
請注意,這一次我們不需要讓其他項目知道寬度的變化,因為 flexbox 佈局會為我們處理所有這些。
剩下的唯一事情就是隱藏子導航。 你猜怎麼著? 使用與以前相同的 JavaScript 代碼,這也“正常工作”,無需任何額外更改。 Flexbox 知道可用空間,它會自動使我們的佈局工作而無需額外的代碼。 很酷。
Flexbox 還提供了一些有趣的方式來使垂直和水平元素居中。 我們在這裡意識到,對於一種表示語言來說,包含自由空間的概念是多麼重要,以及我們的代碼使用這些技術可以變得多麼可擴展。 另一方面,與經典 CSS 相比,這裡的概念和符號可能需要更多的時間來掌握。
CSS3 網格佈局
如果 Flexbox Layout 處於 CSS3 的前沿,那麼 Grid Layout 可以說是處於最前沿。 W3C 規範仍處於草稿狀態,並且對瀏覽器的支持仍然相當有限。 (它通過 chrome://flags 中的“experimental Web Platform features”標誌在 Chrome 中啟用)。
也就是說,我個人不認為這個草案是革命性的。 相反,正如 HTML5 設計原則所說: “當一種做法在作者中已經很普遍時,考慮採用它而不是禁止它或發明新的東西。”
因此,基於標記的網格已經使用了很長時間,所以現在 CSS 網格佈局實際上只是遵循相同的範式,在沒有標記要求的情況下在表示層提供所有優點和更多。
一般的想法是有一個預定義的、固定的或靈活的網格佈局,我們可以在其中放置我們的元素。 與 flexbox 一樣,它也適用於自由空間原則,允許我們在同一元素中定義垂直和水平“方向”,這在代碼大小和靈活性方面帶來了優勢。
網格佈局引入了 2 種類型的網格; 即顯式和隱式。 為簡單起見,我們將重點關注顯式網格。
與 flexbox 一樣,網格佈局引入了自己獨特的一組術語和概念。 其中一些包括:
- 網格容器。
display
屬性設置為“grid”或“inline-grid”的元素,其中包含的元素通過定位和對齊到預定義的網格(顯式模式)來佈局。 網格是一組相交的水平和垂直網格線,將網格容器的空間劃分為網格單元。 有兩組網格線; 一個用於定義列,另一個用於定義行。 - 網格軌道。 兩條相鄰網格線之間的空間。 每個網格軌道都分配了一個大小調整函數,該函數控制列或行可以增長的寬度或高度,從而控制其邊界網格線的距離。
- 網格單元格。 兩個相鄰行和兩個相鄰列網格線之間的空間。 它是定位網格項時可以引用的網格的最小單位。
- 靈活的長度。 用
fr
單位指定的維度,它表示網格容器中可用空間的一小部分。
如果我們使用網格佈局,這是我們可以使用的替代標記:
<body class="layout-grid"> <header></header> <nav></nav> <aside></aside> <main></main> <footer></footer> </body>
請注意,使用這種佈局,我們不需要像對 flexbox 那樣對內容區域進行額外的包裝,因為這種類型的佈局允許我們在同一個網格容器中定義兩個方向的元素空間指定。
現在讓我們深入研究 CSS:
.layout-grid { display: grid; grid-template-columns: auto 0 auto 1em 1fr; grid-template-rows: 5em 1em 1fr 1em 3em; }
我們已經定義了display: grid;
在我們的容器上。 grid-template-columns
和grid-template-rows
屬性分別指定為網格軌道之間的空間列表。 換句話說,這些值不是網格線的位置; 相反,它們代表兩個軌道之間的空間量。
請注意,測量單位可以指定為:
- 一個長度
- 網格容器大小的百分比
- 對占據列或行的內容的測量,或
- 網格中可用空間的一小部分
所以使用grid-template-columns: auto 0 auto 1em 1fr;
我們將有:
- 1 條軌道定義 2 列
auto
寬度(#nav
空間) - 0 的 1 個裝訂線(
#subnav
的邊距在元素級別,因為它可以存在或不存在,這樣我們就避免了雙裝訂線) - 1 條軌道定義 2 列
auto
寬度(#subnav
空間) - 1 個
1em
的排水溝 #main
使用1fr
的 1 首曲目(將佔用所有剩餘空間)
在這裡,我們大量使用軌道的auto
值,這允許我們擁有動態列,其中行的位置和大小由它們的最大內容定義。 (因此,我們需要指定#nav
和#subnav
元素的大小,我們很快就會這樣做。)
同樣,對於行線,我們有grid-template-rows: 5em 1em 1fr 1em 3em;
這將我們的#header
和#footer
設置為固定,並且在使用1em
排水溝時使用剩餘的可用空間之間的所有元素。
現在讓我們看看我們如何將要定位的實際元素放置到我們定義的網格中:
#header { grid-column: 1 / 6; grid-row: 1 / 2; } #footer { grid-column: 1 / 6; grid-row: 5 / 6; } #main { grid-column: 5 / 6; grid-row: 3 / 4; overflow-y: auto; }
這指定我們希望我們的標題位於網格線 1 和 6(全寬)之間,以及我們的行的網格線 1 和 2 之間。 頁腳相同,但在最後兩行之間(而不是前兩行)。 並且主要區域被適當地設置為它應該佔據的空間。
請注意, grid-column
和grid-row
屬性是分別指定grid-column-start
/ grid-column-end
和grid-row-start
/ grid-row-end
的簡寫。
好的,回到#nav
和#subnav
。 由於我們之前使用自動值將#nav
和#subnav
放入軌道中,因此我們需要指定這些元素的寬度(對於擴展模式也是如此,我們只需更改其寬度,其餘的由網格佈局處理)。
#nav { width: 5em; grid-column: 1 / 2; grid-row: 3 / 4; &.expanded { width: 10em; } } #subnav { grid-column: 3 / 4; grid-row: 3 / 4; width: 13em; /* track has gutter of 0, so add margin here */ margin-left: 1em; }
所以現在我們可以切換我們的#nav
並隱藏/刪除#subnav
並且一切正常! 網格佈局還允許我們為線使用別名,因此最終更改網格不會破壞代碼,因為它映射到名稱而不是網格線。 絕對期待更多瀏覽器更廣泛地支持這一點。
結論
即使使用經典的 CSS 技術,也有很多 Web 開發人員沒有意識到或利用的東西。 也就是說,其中大部分可能非常乏味,並且可能涉及在整個樣式表中重複地對值進行硬編碼。
CSS3 已經開始提供更加複雜和靈活的佈局技術,這些技術非常容易編程,並且避免了以前 CSS 規範的大部分乏味。
掌握 CSS2 和 CSS3 的這些技術和範式對於利用 CSS 所提供的一切來優化用戶體驗和代碼質量至關重要。 這篇文章實際上只是代表了所有需要學習的東西以及使用 CSS 的強大和靈活性可以完成的所有事情的冰山一角。 有它!