[譯] 從原型圖到成品:步步深入 CSS 佈局

?Badd發表於2019-05-27

用 CSS 將原型實現

對很多人來說,建立佈局是前端開發領域中最難啃的骨頭之一。

你肯定經歷過耗費數個小時,換著花樣地嘗試所有可能起作用的 CSS 屬性、一遍遍地從 Stack Overflow 上覆制貼上程式碼,寄希望於誤打誤撞地賭中那個能實現預期效果的魔幻組合

如果你的慣用策略就是按部就班地組合佈局 —— 先把 A 元素放在這兒,好了,A 元素就位了,我再看怎麼把 B 放在那兒 …… 那你沒有挫敗感才怪呢。CSS 的玩法可與 SKetch 或者 Photoshop 的玩法不一樣。

在本文中,我將向你展示如何以統籌全域性的思維實現 CSS 佈局,根治佈局難產的頑疾。

我們將用一個小案例貫穿全文,我會把所有的 CSS 程式碼都解釋給你聽,因此即使你不知道或者忘記了 positiondisplay 的用法,即使你分不清 align-itemsjustify-content 的區別,你仍會有所斬獲。

而且我們會用純 HTML 和 CSS 程式碼來演示,因此你不需要 React、Vue、Angular、CSS-in-JS 甚至是 JavaScript 方面的知識儲備。

聽起來很棒吧?那就開始吧。

佈局小例子

在本文中,我們要比照 Twitter 的推文元件自己仿寫一個:

推文元件的草圖

不論是一個像這樣的草圖,還是一個細節精美的原型圖,“有章可循” 總是個好主意。

要避免一邊在腦海裡設計,一邊在瀏覽器中七拼八湊地攢佈局,這樣的開發過程才會更順暢。你當然可以達到那種手腦合一的境界!但鑑於你還在乖乖地讀這篇文章,我可以假設你還沒有那麼神通廣大。:)

第一步:分而治之

在動手敲程式碼之前,我們先把佈局的各個單元區分開來:

劃分推文元件的各個單元

在用 CSS 鋪排佈局時,用行和列的形式去構思大有裨益。因此,要麼你把元素從上到下排列,要麼從左到右排列。這種行和列的思路完美對應了 CSS 中兩種佈局技術:Flexbox 和 Grid。

當然了,我們的示例佈局並不是中規中矩的行列。它有一張圖片鑲嵌在左側,其他元素排列在右側。

第二步:沿著各個單元畫方框

畫一些方框把這些元素框起來,看看行和列是否初具規模。我們把方向一致的單元歸到同一個方框中。

將推文元件的不同單元框起來

在頁面中的 HTML 元素基本上都可視為矩形。當然,有些元素有圓角,有些元素是圓形,或者是複雜的 SVG 形狀等。通常你看不到頁面上有一堆矩形。但你可以用矩形邊框的模式去分析它們。這樣的想象能幫你理解佈局。

之所以提到矩形,是因為你要把一系列元素對齊 —— 如第一行的使用者名稱、@handle(譯者注:handle 屬於專有名詞,指 Twitter 中的使用者 ID,所以在本文中保留不譯。詳見 www.urbandictionary.com/define.php?…)和時間以及最後一行的圖示 —— 把它們用方框包起來便於規劃。

按目前的規劃,把佈局用 HTML 程式碼實現出來大概如下所示:

<article>
  <img
    src="http://www.gravatar.com/avatar"
    alt="Name"
  />
  <div>
    <span>@handle</span>
    <span>Name</span>
    <span>3h ago</span>
  </div>
  <p>
    Some insightful message.
  </p>
  <ul>
    <li><button>Reply</button></li>
    <li><button>Retweet</button></li>
    <li><button>Like</button></li>
    <li><button>...</button></li>
  </ul>
</article>
複製程式碼

展示出的效果是這樣的(可以點選這裡除錯程式碼):

推文元件的預設樣式

這離我們想要的效果還遠呢。但是!所有所需的內容都齊全了。有些元素還以從左到右的順序排列。

我們可以認為,即使不用進一步設定樣式,目前的佈局效果也能達到網頁想表達的要點,這也是一個優秀的 HTML 應該達到檢查標準。

關於語義化 HTML 的說明

你可能會好奇,為何我選的是那些元素 —— articlep 等等。為何不都用 div 呢?

為何要這樣寫:

<article>
  <img ... />
  <div>
    <span/>
    <span/>
    <span/>
  </div>
  <p> ... </p>
  <ul>
    <li>
      <button> ... </button>
    </li>
  </ul>
</article>
複製程式碼

而不這樣寫?

<div>
  <img ... />
  <div>
    <div/>
    <div/>
    <div/>
  </div>
  <div> ... </div>
  <div>
    <button> ... </button>
  </div>
</div>
複製程式碼

其實,每個 HTML 元素的名稱都有其特定含義,在不同場景中恰如其分地使用語義上與它們所表示的內容匹配的元素,是很好的語義化實踐。

這種寫法,首先,有助於開發者理解程式碼;其次,對使用螢幕閱讀器等輔助裝置的使用者比較友好。同時這樣用標籤也有利於 SEO —— 搜尋引擎會試著理解這個頁面的含義,以便於顯示相關廣告來盈利、幫助搜尋者找到滿意結果。

article 標籤代表文章類內容,而你可以認為推文這種東西有點類似於一篇文章。

p 標籤代表段落,而推文的內容文字有點類似於一個段落。

ul 標籤代表無序列表(與有序列表或數字序號列表相對應),在本示例中,你可以用它來存放列表資訊。

我們無法用隻言片語就說清楚 HTML 元素的語義,以及何種情況用何種標籤。但大多數情況下,一個語義化元素即使其語義再不貼切,也比用 div 強,div 標籤只代表 “一塊區域”。

元素的預設樣式

是什麼決定了元素的樣式?為什麼有的元素獨佔一行,而有的元素能共處一行?

預設樣式下的推文元件

這要歸因於元素的預設樣式,這其中就有我們要探討的第一個 CSS 知識點:行內元素塊級元素

行內元素們肩並肩擠在一行裡(就像句子中的詞一樣,必要時會折行)。根據再瀏覽器中的預設樣式劃分,spanbutton 以及 img 都是行內元素。

塊級元素,總是踽踽獨行。以控制檯輸出的方式去理解,你可以認為塊級元素前後各有一個換行符 \n。就好像console.log("\ndiv\n")articledivliul 以及 p 標籤都是塊級元素。

注意,在上面的例子中,為什麼即使 img 標籤是行內元素,頭像圖片依然獨佔一行?因為它下方的 div 是塊級元素。

然後要注意,為什麼 @handle、使用者名稱和時間都在同一行?原因是它們都在 span 標籤中,而 span 是行內元素。

這三個 span 和 文字 “insightful message” 處於不同行,因為(a)它們被包在一個 div 中,div 後面自然要另起一行;(b)p 標籤同樣是塊級元素,它自然從新行開始排列。(之所有沒有出現兩個空行,是因為 HTML 合併了相鄰的空行,與相鄰空格同理。)

如果你再看得仔細點,你會發現 “insightful message” 的上下方空間,要比頭像圖片以及 handle、使用者名稱、時間的上下方空間要大。此空間的大小也由預設樣式控制:p 標籤的頂部和底部都有 margin

你也會注意到按鈕列表的圓點,以及列表的縮排行為。這些也都是預設樣式。我們馬上就要修改這些預設樣式了。

第三步:再畫一些方框

我們想把頭像圖片放在左側,其餘元素放在右側。你可能會根據剛剛探討的行內和塊級知識來推斷,認為只要把右側的元素都包裹到一個如 span 標籤般的行內元素中,就完事大吉了。

但這是行不通的。行內元素並不能阻止其內部的塊級元素另起一行。

為了把這些元素收拾得服服帖帖,我們需要用一些更強大的技術,比如 Flexbox 或者 Grid 佈局。這次我們選用 Flexbox 來解決。

Flexbox 的原理

CSS 的 Flex 佈局能夠把元素以行或者列的形式排布。這是一種單向的佈局系統。為了實現交叉的行和列(正如推文元件的設計那樣),我們需要新增一些容器元素來扭轉方向。

每一層佈局都用方框包圍

你可以在容器上設定 display: flex; 來啟用 Flex 佈局。容器本身是塊級元素(得以獨佔一行),其內部元素會成為 “Flex 子項” —— 即它們不再是行內或塊級元素了;它們都受 Flex 容器控制。

在本例中,我們會設定一些巢狀的 Flex 容器,讓該成行的成行,該成列的成列。

我們把外層容器(綠色方框)設定為列,藍色方框設定為行,而紅色方框中的元素排布在列中。

箭頭方向即為 Flex 佈局方向

為何選 Flexbox 佈局,不選 Grid 佈局?

由於一些原因,我決定用 Flexbox 佈局而不用 Grid 佈局。我覺得 Flexbox 佈局更易於學習,也更適用於輕量級的佈局。當佈局中主要是行或者主要是列時,Flexbox 佈局的表現更出色。

另一個重點就是,即使 Grid 佈局比 Flexbox 佈局年輕,前者也撼動不了後者的地位。它們各自適用於不同的場景,對於二者,我們都要學習,技不壓身。有些情況你甚至會同時使用二者 —— 例如 Grid 佈局排布整體頁面,而 Flexbox 佈局調控頁面中的一個表單。

沒錯沒錯,在 Web 開發的世界,普遍的更替法則是後浪推前浪,但 CSS 並不如此。Flexbox 和 Grid 能夠和諧共存。

用 CSS 解決問題,條條大路通羅馬!

第四步:應用 Flexbox

好了,既然我們已經打定主意,那就開動吧。我把左側元素包進一個 div,並給元素們設定類名,便於應用 CSS 選擇器。

<article class="tweet">
  <img
    class="avatar"
    src="http://www.gravatar.com/avatar"
    alt="Name"
  />
  <div class="content">
    <div class="author-meta">
      <span class="handle">@handle</span>
      <span class="name">Name</span>
      <span class="time">3h ago</span>
    </div>
    <p>
      Some insightful message.
    </p>
    <ul class="actions">
      <li><button>Reply</button></li>
      <li><button>Retweet</button></li>
      <li><button>Like</button></li>
      <li><button>...</button></li>
    </ul>
  </div>
</article>
複製程式碼

程式碼在這裡

看著好像沒有變化。

預設樣式下的推文元件

這是因為 div 作為塊級元素(如果沒有空行就引入一個)是看不見的。當你需要一個包裹其他元素的容器,除了 div 之外沒有更貼合語義的選擇了。

下面我們們的第一段 CSS 程式碼,我們會把它放在 HTML 文件中 head 標籤的 style 裡:

.tweet {
  display: flex;
}
複製程式碼

幹得漂亮!我們用類選擇器鎖定了所有類名為 tweet 的元素。當然目前只有一個這樣的元素,但如果有十個,那它們將都會是 Flex 容器了。

CSS 中以 . 開頭的選擇器代表類選擇器。為什麼是 .?我可不知道。你只要記住這條規則就行了。

設定了 display:flex

現在文字內容都到頭像右側去了。問題是頭像圖片都扭曲變形了。

因為 Flex 容器會預設:

  • 把子項排成一行;
  • 讓子項與其內容等寬,並 ——
  • 把所有子項的高度拉平為最高子項的高度。

我們可以用 align-items 屬性來控制垂直方向的對齊方式。

.tweet {
  display: flex;
  align-items: flex-start;
}
複製程式碼

align-items 的預設值是 stretch,而將其設為 flex-start 後,會讓子項沿著容器頂部對齊,並且讓子項保持各自的高度。

方向的辯證:行還是列?

另外,Flex 容器的預設排列方向是 flex-direction: row;。是的,這個方向是 “行”,即使我們可能感覺那更像是兩列。要把它想成是子項們排成一,這樣理解就舒服多了。

有點像這張花瓶的圖片,或者說兩張臉的圖片。橫看成嶺側成峰。

Rubin 的花瓶

Wikipedia

給文字內容更多的空間

Flex 佈局的子項僅取其所需寬度,但我們需要 content 區域儘量寬敞一些。

因此,我們要給 content 這個 div 設定 flex: 1; 屬性。(該 div 有類名,那我們就又可以用類選擇器啦!)

.content {
  flex: 1;
}
複製程式碼

我們也要給頭像設定 margin,好在頭像和文字之間加點空隙:

.avatar {
  margin-right: 10px;
}
複製程式碼

設定了 display:flex

看起來順眼一些了吧!

margin 和 padding

那…… 為什麼用 margin 而不用 padding?為什麼要設定在頭像右側,而不是文字內容左側呢?

這是一條約定俗成的規則:在元素右側和下方設定 margin,不去碰左側和上方的 margin。

至少是在英文介面的佈局中,文件流的方向是從左到右、從上到下的,因此,每個元素都 “依賴” 其左側和上方的元素。

在 CSS 中,每個元素的定位都受到其左側和上方的元素的影響。(至少在你遇見 position: absolute 那幫傢伙之前是這樣的。)

SoC 原則(Separation of Concerns)

從技術實現的角度來說,怎樣設定 avatarcontent 之間的空隙都一樣。該是多寬就是多寬,沒有 border 的干擾(paddingborder 的內側;而 margin 在外側)。

但當事關可維護性、對元素的全域性觀時,這就有區別了。

我曾嘗試把元素理解為一個個獨立個體,就像每個 JavaScript 函式只實現單一功能一樣:如果它們都僅僅扮演單一的角色,那麼寫起程式碼來就很容易,報錯時除錯也很容易。

如果我們把 margin 設定到 content 的左側,後來有一天我們去掉了 avatar,可是以前的縫隙還留在那。我們還得排查導致額外空間的原因(是來自 tweet 容器嗎? 還是來自 content 呢?)並把它處理掉。

或者,如果 content 設定了左側的 margin,而我們想要把 content 替換成別的元素,我們還要記著再把之前那個空隙補上

好了好了,為了 10 畫素的事,沒必要費這麼多口舌,乾脆就把 margin 設在頭像的右側和下方。讓我們繼續埋頭敲程式碼吧。

移除列表的樣式

無序列表 ul 和其中的列表項 li 在左側窩藏了很大空間,還有一些圓點。這都不是我們想要的效果。

我們可以把無序列表左側的空隙都清除掉。我們還要把它變成一個 Flex 容器,這樣裡面的按鈕就能排成一行了(用 flex-direction: row)。

列表項有個屬性是 list-style-type,預設值為 disc,使得每個列表項以圓點開頭,我們用 list-style: none;list-style 是一個縮寫屬性,整合了幾個其他屬性,其中就包括 list-style-type)將該效果關閉。

.actions {
  display: flex;
  padding: 0;
}
.actions li {
  list-style: none;
}
複製程式碼

按鈕排成一排

.actions 又是一個類選擇器。原汁原味。

.actions li 選擇器,意即 “actions 類元素中所有的 li 元素”。它是類選擇器和元素選擇器的結合。

複合選擇器中用以分隔的空格代表著選擇範圍的縮小。事實上,CSS 是以倒序讀取選擇器的。其過程是 “先找到頁面中所有的 li,然後在這些 li 中找到類名是 actions 的那些”。但無論你用正序還是倒序的方式去理解,結果都是一樣的。(在 StackOverflow 檢視更多詳解)

橫排按鈕

要橫排按鈕有好幾種方式。

一種就是設定 Flex 子項的對齊方式。你應該對設定對齊方式很熟悉,每個富文字編輯器頂部都有這種功能的按鈕:

對齊按鈕:左對齊/居中對齊/右對齊/兩端對齊

它們把文字進行左對齊、居中對齊、右對齊以及 “兩端對齊”,也就是鋪滿整行。

在 Flexbox 佈局中,你可以用 justify-content 屬性來實現對齊。設定了 flex-direction: row(預設值,也是本文中一直在用的設定)後,可以通過 justify-content 把子項進行或左或右地對齊。justify-content 的預設值為 flex-start(因此所有元素都向左看齊)。如果我們給 .actions 元素設定 justify-content: space-between,它們就會均勻地鋪滿整行,就像這樣:

按鈕對齊:justify-content:space-between

可我們想要的不是這樣的效果。如果這幾個按鈕可以不佔滿整行會更好。所以得換一種方式。

這次,我們給每個列表項設定一個右側的 margin,把它們分隔開來。還要給整個推文元件設定一個邊框,以便我們能夠直觀地衡量效果。用 1px solid #ccc 設定一個 1 畫素寬的灰色實線邊框。

.tweet {
  display: flex;
  align-items: flex-start;
  border: 1px solid #ccc;
}
.actions li {
  list-style: none;
  margin-right: 30px;
}
複製程式碼

現在效果如下:

元件帶邊框,按鈕分隔排列

按鈕的排列看起來優雅多了,但灰色邊框告訴我們,所有元素都過於靠左了。還是用 padding 分配點空間吧。

.tweet {
  display: flex;
  align-items: flex-start;
  border: 1px solid #ccc;
  padding: 10px;
}
複製程式碼

現在推文元件有內邊距了,但有些地方還是很空。如果我們用瀏覽器除錯工具將元素高亮顯示,就會發現 pul 元素有預設的上下 margin(在 Chrome 的除錯工具中,margin 以橙色顯示,而 padding 以綠色顯示):

p 和 ul 周圍的間隔

還有一處有意思的細節;行與行之間的上下 margin 是等距的 —— 並沒有疊加出雙倍間距!因為 CSS 在豎直方向上有 margin 坍塌現象。當上下兩個 margin 短兵相接時,數值大的 margin 會 “吃掉” 小的。詳情參見 CSS 技巧:margin 坍塌

對於本例的佈局,我會手動調整 .author-metapul 的右側 margin。如果要真刀真槍地開發網站,建議你考慮用 CSS reset 作為開發基礎,有利於跨瀏覽器相容。

p, ul {
  margin: 0;
}
.author-meta, p {
  margin-bottom: 1em;
}
複製程式碼

, 將選擇器隔開,可以一次性把樣式應用到多個選擇器上。因此 p , ul 的含義就是 “所有的 p 元素,以及所有的 ul 元素”。亦即二者的合集。

在這裡我們使用了新的尺寸單位,1em 中的 em。一個單位的 em 等於 body 標籤上的以畫素為單位的字號大小。body 標籤的預設字號為 16px(16 畫素高),所以本例中的 1em 相當於 16pxem 隨字號改變而改變,因此可以用 1em 來表達 “我想讓文字下方的 margin 和文字的高度一樣,不論文字高度是多少”。

現在的效果如下:

設定 margin

現在讓我們把圖片縮小一些,並將其設定為圓形。我們將其寬高設定為 48 畫素,正和 Twitter 的頭像寬高一樣。

.avatar {
  margin-right: 10px;
  width: 48px;
  border-radius: 50%;
}
複製程式碼

我們用 border-radius 屬性來設定圓角,有好幾種方式來定義該屬性的值。如果你想要小圓角效果,可以用帶 pxem 或其他單位名稱的數字賦值。例如 border-radius: 5px 的效果:

圓角半徑為 5 畫素的頭像

如果將 border-radius 設為寬和高的一半(在本例中即為 24 畫素),其效果就是一個圓形。但更方便的寫法是 border-radius:50%,這樣我們就不必知道具體尺寸,CSS 會計算出確切結果。甚至,如果以後寬高值變了,也無需重新修改屬性值了!

圓形頭像

再接再厲

眼下還有一些需要潤色之處。

我們要把字型設為 Helvetica(Twitter 用的那一款)、把字號縮小一些、把使用者名稱加粗,還有,翻轉 “@handle 使用者名稱 的順序(在 HTML 程式碼中),使之與 Twitter 一模一樣。:D

.tweet {
  display: flex;
  align-items: flex-start;
  border: 1px solid #ccc;
  padding: 10px;
  /* 
    更改字型和字號。
    在 .tweet 選擇器上設定的 CSS 效果,其所有子元素都會繼承。
    (除了按鈕。按鈕不太合群)
  */
  font-family: Helvetica, Arial, sans-serif;
  font-size: 14px;
}

.name {
  font-weight: 600;
}

.handle,
.time {
  color: #657786;
}
複製程式碼

font-weight: 600; 的效果等同於 font-weight: bold;。字型有很多不同程度的字重,範圍是從 100 到 900(最淡到最濃)。normal(預設值)等價於 400。

另外,CSS 中的註釋寫法與 JavaScript 或其他語言不用,不允許以 // 開頭。某些瀏覽器支援 // 風格的 CSS 註釋,但並非所有瀏覽器都如此。用 C 語言風格的 /* */ 包圍註釋內容即可高枕無憂。

還有一個小竅門:可以用 偽元素在 “handle” 與 “時間” 之間新增一個凸點。這個凸點符號單純為了裝飾,不具有具體語義,所以用 CSS 實現不會汙染 HTML 語義結構。

.handle::after {
  content: " \00b7";
}
複製程式碼

::after 建立了一個偽元素,它位於 .handle 元素內部的最後方(“落後” 於元素的內容)。你還可以用 ::before 建立偽元素。可以給 content 屬性賦值任何文字內容,包括 Unicode 字元。你可以恣意發揮,像給任何其他元素設定樣式一樣。偽元素用來實現標記(badge)、訊息提醒或其他小花樣最合適不過了。

圖示按鈕

還有一項工作要做,那就是用圖示替換按鈕。我們要在 head 標籤裡新增 Font Awesome 圖示字型:

<link
  rel="stylesheet"
  href="https://use.fontawesome.com/releases/v5.8.1/css/all.css"
  integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf"
  crossorigin="anonymous"
/>
複製程式碼

然後用下列程式碼替換原來的 ul,新列表中的每個按鈕裡有圖示和隱藏文字:

<ul class="actions">
  <li>
    <button>
      <i
        class="fas fa-reply"
        aria-hidden="true"
      ></i>
      <span class="sr-only">Reply</span>
    </button>
  </li>
  <li>
    <button>
      <i
        class="fas fa-retweet"
        aria-hidden="true"
      ></i>
      <span class="sr-only">Retweet</span>
    </button>
  </li>
  <li>
    <button>
      <i
        class="fas fa-heart"
        aria-hidden="true"
      ></i>
      <span class="sr-only">Like</span>
    </button>
  </li>
  <li>
    <button>
      <span aria-hidden="true">...</span>
      <span class="sr-only">More Actions</span>
    </button>
  </li>
</ul>
複製程式碼

Font Awesome 是一款圖示字型,它配合斜體標籤 i 可以展示圖示。正因為它是字型,那些可以用於文字的 CSS 屬性(例如 colorfont-size)都適用於圖示字型。

我們在這兒做了些微調,來提升按鈕的可訪問性:

  • 特性 aria-hidden="true" 使螢幕閱讀器忽略此圖示。
  • sr-only 類是 Font Awesome 內建的類。它讓元素在你眼前隱身,但螢幕閱讀器能讀取到它。

這裡有一門由 Marcy Sutton 講授的關於圖示按鈕可訪問性的免費 Egghead 課程

現在我們將要給按鈕新增一些樣式 —— 移除邊框、上色以及加大字號。還要設定 cursor: pointer,把滑鼠游標變成 “手” 型,就像超連結的效果那樣。最後,用 .actions button:hover 選擇處於 hover 狀態的按鈕,把它們變成藍色。

.actions button {
  border: none;
  color: #657786;
  font-size: 16px;
  cursor: pointer;
}
.actions button:hover {
  color: #1da1f2;
}
複製程式碼

下面就是推文元件光芒四射的最終效果:

最終效果

如果你想自己除錯程式碼,到沙箱裡來。

如何精進 CSS 水平

最能提高 CSS 水平的就是實踐。

仿寫你喜歡的網站。設計者和藝術家稱其為 “臨摹”。我寫過一篇用臨摹的方法學 React,其中的原則也適用於 CSS。

選一些有意思的、你覺得難度大的樣式效果。用 HTML 和 CSS 臨摹該效果。如果卡殼了,用瀏覽器的除錯工具看看原網站的效果是如何實現的。“栽秧苗、腿跟上、抬頭看看直不直。” :)

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


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

相關文章