編寫高效CSS程式碼的一些建議

github發表於2014-08-28

在參與規模龐大、歷時漫長且參與人數眾多的專案時,所有開發者遵守如下規則極為重要:

  • 保持 CSS 易於維護
  • 保持程式碼清晰易懂
  • 保持 CSS 的可擴充性

為了實現這一目標,我們要採用諸多方法。

本文件第一部分將探討語法、格式以及分析 CSS 結構;第二部分將圍繞方法論、思維框架以及編寫與規劃 CSS 的看法。

CSS 文件分析

無論編寫什麼文件,我們都應當維持統一的風格,包括統一的註釋、統一的語法與統一的命名規範。

總則

將行寬控制在 80 位元組以下。漸變(gradient)相關的語法以及註釋中的 URL 等可以算作例外,畢竟這部分我們也無能為力。

我傾向於用 4 個空格而非 Tab 縮排,並且將宣告拆分成多行。

單一檔案與多檔案

有人喜歡在一份檔案檔案中編寫所有的內容,而我在遷移至 Sass 之後開始將樣式拆分成多個小檔案。這都是很好的做法。無論你選擇哪種,下文的規則都將適用,而且如果你遵守這些規則的話你也不會遇到什麼問題。這兩種寫法的區別僅僅在於目錄以及區塊標題:

目錄

在 CSS 的開頭,我會維護一份目錄,就像這樣:

/*------------------------------------*\
    $CONTENTS
\*------------------------------------*/
/**
 * CONTENTS............You’re reading it!
 * RESET...............Set our reset defaults
 * FONT-FACE...........Import brand font files
 */

這份目錄可以告訴其他開發者這個檔案中具體含有哪些內容。這份目錄中的每一項都與其對應的區塊標題相同。

如果你在維護一份單檔案 CSS,對應的區塊將也在同一檔案中。如果你是在編寫一組小檔案,那麼目錄中的每一項應當對應相應的 @include 語句。

區塊標題

目錄應當對應區塊的標題。如下:

/*------------------------------------*\
    $RESET
\*------------------------------------*/

區塊標題字首 $ 可以讓我們使用 [Cmd|Ctrl]+F 命令查詢標題名時將搜尋範圍限制在區塊標題中

如果你在維護一份大檔案,那麼在區塊之間空 5 行,如下:

/*------------------------------------*\
    $RESET
\*------------------------------------*/
[Our
reset
styles]



/*------------------------------------*\
    $FONT-FACE
\*------------------------------------*/

在大檔案中快速翻動時這些大塊的空檔有助於區分割槽塊。

如果你在維護多份以 include 連線的 CSS 的話,在每份檔案頭加上標題即可,不必這樣空行。

程式碼順序

儘量按照特定順序編寫規則,這將確保你充分發揮 CSS 中第一個 C 的意義:cascade,層疊。

一份規劃良好的 CSS 應當按照如下排列:

  1. Reset 萬物之根源
  2. 元素型別 沒有 class 的 h1ul 等
  3. 物件以及抽象內容 最一般、最基礎的設計模式
  4. 子元素 由物件延伸出來的所有擴充及其子元素
  5. 修補 針對異常狀態

如此一來,當你依次編寫 CSS 時,每個區塊都可以自動繼承在它之前區塊的屬性。這樣就可以減少程式碼相互抵消的部分,減少某些特殊的問題,組成更理想的 CSS 結構。

關於這方面的更多資訊,強烈推薦 Jonathan Snook 的 SMACSS

CSS 樣式集分析

[selector]{
    [property]:[value];
    [<- Declaration ->]
}

[選擇器]{
    [屬性]:[值];
    [<- 宣告 ->]
}

編寫 CSS 樣式時,我習慣遵守這些規則:

  • class 名稱以連字元(-)連線,除了下文提到的 BEM 命名法;
  • 縮排 4 空格;
  • 宣告拆分成多行;
  • 宣告以相關性順序排列,而非字母順序;
  • 有字首的宣告適當縮排,從而對齊其值;
  • 縮排樣式集從而反映 DOM;
  • 保留最後一條宣告結尾的分號。

例如:

.widget{
    padding:10px;
    border:1px solid #BADA55;
    background-color:#C0FFEE;
    -webkit-border-radius:4px;
       -moz-border-radius:4px;
            border-radius:4px;
}
    .widget-heading{
        font-size:1.5rem;
        line-height:1;
        font-weight:bold;
        color:#BADA55;
        margin-right:-10px;
        margin-left: -10px;
        padding:0.25em;
    }

我們可以發現,.widget-heading 是 .widget 的子元素,因為前者的樣式集比後者多縮排了一級。這樣通過縮排就可以讓開發者在閱讀程式碼時快速獲取這樣的重要資訊。

我們還可以發現 .widget-heading 的宣告是根據其相關性排列的:.widget-heading 是行間元素,所以我們先新增字型相關的樣式宣告,接下來是其它的。

以下是一個沒有拆分成多行的例子:

.t10    { width:10% }
.t20    { width:20% }
.t25    { width:25% }       /* 1/4 */
.t30    { width:30% }
.t33    { width:33.333% }   /* 1/3 */
.t40    { width:40% }
.t50    { width:50% }       /* 1/2 */
.t60    { width:60% }
.t66    { width:66.666% }   /* 2/3 */
.t70    { width:70% }
.t75    { width:75% }       /* 3/4*/
.t80    { width:80% }
.t90    { width:90% }

在這個例子(來自inuit.css’s table grid system)中,將 CSS 放在一行內可以使得程式碼更緊湊。

命名規範

一般情況下我都是以連字元(-)連線 class 的名字(例如 .foo-bar 而非 .foo_bar 或 .fooBar),不過在某些特定的時候我會用 BEM(Block, Element, Modifier)命名法。

BEM 命名法可以使得選擇器更規範,更清晰,更具語義。

該命名法按照如下格式:

.block{}
.block__element{}
.block--modifier{}

其中:

  • .block 代表某個基本的抽象元素;
  • .block__element 代表構成 .block 的一個子元素;
  • .block--modifier 代表 .block 的某個不同狀態或版本。

打個比方:

.person{}
.person--woman{}
    .person__hand{}
    .person__hand--left{}
    .person__hand--right{}

這個例子中我們描述的基本元素是一個人,然後這個人可能是一個女人。我們還知道人擁有手,這些是人體的一部分,而手也有不同的狀態,如同左手與右手。

這樣我們就可以根據親元素來劃定選擇器的名稱空間並傳達該選擇器的職能,例如根據這個選擇器是一個子元素(__)還是其親元素的不同狀態(--)。

由此,.page-wrapper 是一個獨立的選擇器。這是一個符合規範的命名,因為它不是其它元素的子元素或其它狀態;然而.widget-heading 則與其它物件有關聯,它應當是 .widget 的子元素,所以我們應當將其重新命名為.widget__heading

BEM 命名法雖然不太好看,而且相當冗長,但是它使得我們可以通過名稱快速獲知元素的功能和元素之間的關係。與此同時,BEM 語法中的重複部分非常有利於 gzip 的壓縮演算法。

無論你是否使用 BEM 命名法,你都應當確保 class 命名得當,力保一字不多、一字不少;將元素命名抽象化以提高複用性(例如 .ui-list.media)。子元素的命名則要儘量精準(例如 .user-avatar-link)。不用擔心 class 名的數量或長度,因為寫得好的程式碼 gzip 也能有效壓縮。

HTML 中的 class

為了確保易讀性,在 HTML 標記中用兩個空格隔開 class 名,例如:

<div class="foo--bar  bar__baz">

增加的空格應當可以使得在使用多個 class 時更易閱讀與定位。

JavaScript 鉤子

切勿將標記 CSS 樣式的 class 用作 JavaScript 鉤子。把 JS 行為與樣式混在一起將無法對其分別處理。

如果你要把 JS 和某些標記繫結起來的話,寫一個 JS 專用的 class。簡單地說就是劃定一個字首 .js- 的名稱空間,例如.js-toggle.js-drag-and-drop。這意味著我們可以通過 class 同時繫結 JS 和 CSS 而不會因為衝突而引發麻煩。

<th class="is-sortable  js-is-sortable">
</th>

上面的這個標記有兩個 class,你可以用其中一個來給這個可排序的表格欄新增樣式,用另一個新增排序功能。

I18n

雖然我(該 CSS Guideline 文件原作者 Harry Roberts)是個英國人,而且我一向拼寫 colour 而非 color,但是為了追求統一,我認為在 CSS 中用美式拼法更佳。CSS 以及其它多數語言都是以美式拼法編寫,所以如果在 .colour-picker{} 中寫 color:red 就缺乏統一性。我以前主張同時用兩種拼法,例如:

.color-picker,
.colour-picker{
}

但是我最近參與了一份規模龐大的 Sass 專案,這個專案中有許多的顏色變數(例如 $brand-color$highlight-color等等),每個變數要維護兩種拼法實在辛苦,要查詢並替換時也需要兩倍的工作量。

所以為了統一,把所有的 class 與變數都以你參與的專案的慣用拼法命名即可。

註釋

我使用行寬不超過 80 位元組的文件塊風格註釋:

/**
 * This is a docBlock style comment
 * 
 * This is a longer description of the comment, describing the code in more
 * detail. We limit these lines to a maximum of 80 characters in length.
 * 
 * We can have markup in the comments, and are encouraged to do so:
 * 
   <div class=foo>
       <p>Lorem</p>
   </div>
 * 
 * We do not prefix lines of code with an asterisk as to do so would inhibit
 * copy and paste.
 */


/**
 * 這是一個文件塊(DocBlock)風格的註釋。
 *
 * 這裡開始是描述更詳細、篇幅更長的註釋正文。當然,我們要把行寬控制在 80 位元組以內。
 *
 * 我們可以在註釋中嵌入 HTML 標記,而且這也是個不錯的辦法:
 *
    <div class=foo>
        <p>Lorem</p>
    </div>
 *
 * 如果是註釋內嵌的標記的話,在它前面不加星號,以免被複制進去。
 */

在註釋中應當儘量詳細描述程式碼,因為對你來說清晰易懂的內容對其他人可能並非如此。每寫一部分程式碼就要專門寫註釋以詳解。

註釋的擴充用法

註釋有許多很高階的用法,例如:

  • 準修飾選擇器(Quasi-qualified selectors)
  • 程式碼標籤
  • 繼承標記

準修飾選擇器(Quasi-qualified selectors)

你應當避免過分修飾選擇器,例如如果你能寫 .nav{} 就儘量不要寫 ul.nav{}。過分修飾選擇器將影響效能,影響 class 複用性,增加選擇器私有度。這些都是你應當竭力避免的。

不過有時你可能希望告訴其他開發者 class 的使用範圍。以 .product-page 為例,這個 class 看起來像是一個根容器,可能是 html 或者 body 元素,但是僅憑 .product-page 則無法判斷。

我們可以在選擇器前加上準修飾(即將前面的型別選擇器註釋掉)來描述我們規劃的 class 作用範圍:

/*html*/.product-page{}

這樣我們就能準確獲知該 class 的作用範圍而不會影響複用性。

其它例子如:

/*ol*/.breadcrumb{}
/*p*/.intro{}
/*ul*/.image-thumbs{}

這樣我們就能在不影響程式碼私有度的前提下獲知 class 作用範圍。

程式碼標籤

如果你寫了一組新樣式的話,可以在它上面加上標籤,例如:

/**
 * ^navigation ^lists
 */
.nav{}

/**
 * ^grids ^lists ^tables
 */
.matrix{}

這些標籤可以使得其他開發者快速找到相關程式碼。如果一個開發者需要查詢和列表相關的部分,他只要搜尋 ^lists 就能快速定位到 .nav.matrix 以及其它相關部分。

繼承標記

將物件導向的思路用於 CSS 編寫的話,你經常能找到兩部分 CSS 密切相關(其一為基礎,其一為擴充)卻分列兩處。我們可以用繼承標記來在原元素和繼承元素之間建立緊密聯絡。這些在註釋中的寫法如下:

在元素的基本樣式中:

/**
 * Extend `.foo` in theme.css
 */
 .foo{}

在元素的擴充樣式中:

/**
 * Extends `.foo` in base.css
 */
 .bar{}

這樣一來我們就能在兩塊相隔很遠的程式碼間建立緊密聯絡。

編寫 CSS

之前的章節主要探討如何規劃 CSS,這些都是易於量化的規則。本章將探討更理論化的東西,也將探討我們的態度與方法。

編寫新元件

編寫新元件時,要在著手處理 CSS 之前寫好 HTML 部分。這可以令你準確判斷哪些 CSS 屬性可以繼承,避免重複浪費。

先寫標記的話,你就可以關注資料、內容與語義,在這之後再新增需要的 class 和 CSS 樣式。

物件導向 CSS

我以物件導向 CSS 的方式寫程式碼。我把元件分成結構(物件)與外觀(擴充)。正如以下分析(注意此處並非示例):

.room{}

.room--kitchen{}
.room--bedroom{}
.room--bathroom{}

我們在屋子裡有許多房間,它們都有共同的部分:地板、天花板、牆壁和門。這些共享的部分我們可以放到一個抽象的.room{} class 中。不過我們還有其它與眾不同的房間:一個廚房可能有地磚,臥室可能有地毯,洗手間可能沒有窗戶但是臥室會有,每個房間的牆壁顏色也許也會不一樣。物件導向 CSS 的思路使得我們把相同部分抽象出來組成結構部分,然後用更具體的 class 來擴充這些特徵並新增特殊的處理方法。

所以比起編寫大量各自不同的模組,應當努力找出這些模組中重複的設計模式並將其抽象出來,寫成一個可以複用的 class,將其用作基礎然後編寫其它擴充模組的特殊情形。

當你要編寫一個新元件時,將其拆分成結構和外觀。編寫結構部分時用最通用 class 以保證複用性,編寫外觀時用更具體的 class 來新增設計方法。

佈局

所有元件都不要宣告寬度,而由其親元素或格柵系統來決定。

堅決不要宣告高度。高度應當僅僅用於尺寸已經固定的東西,例如圖片和 CSS Sprite。在 puldiv 等元素上不應當宣告高度。如果需要的話可以使用更加靈活的 line-height

格柵系統應當當作書架來理解。是它們容納內容,而不是把它們本身當成內容裝起來,正如你先搭起書架再把東西放進去。比起宣告它們的尺寸,把格柵系統和元素的其它屬性分來開處理更有助於佈局,也使得我們的前端工作更高效。

你在格柵系統上不應當新增任何樣式,它們僅僅是為佈局而用。在格柵系統內部再新增樣式。在格柵系統中任何情況下都不要新增盒模型相關屬性。

UI 尺寸

我用很多方法設定 UI 尺寸,包括百分比,pxemrem 以及乾脆什麼都不用。

理想情況下,格柵系統應當用百分比設定。如上所述,因為我用格柵系統來固定欄寬和頁寬,所以我可以不用理會元素的尺寸。

我用 rem 定義字號,並且輔以 px 以相容舊瀏覽器。這可以兼具 em 和 px 的優勢。下面是一個非常漂亮的 Sass Mixin,假設你在別處宣告瞭基本字號(base-font-size)的話,用它就可以生成 rem 以及相容舊瀏覽器的 px。

@mixin font-size($font-size){
    font-size:$font-size +px;
    font-size:$font-size / $base-font-size +rem;
}

我只在已經固定尺寸的元素上使用 px,包括圖片以及尺寸已經用 px 固定的 CSS Sprite。

字號

我會定義一些與格柵系統原理類似的 class 來宣告字號。這些 class 可以用於雙重標題分級,關於這點請閱讀 Pragmatic, practical font-sizing in CSS

簡寫

CSS 簡寫應當謹慎使用。

編寫像 background: red; 這樣的屬性的確很省事,但是你這麼寫的意思其實是同時宣告 background-image: none; background-position: top left; background-repeat: repeat; background-color: red;。雖然大多數時候這樣不會出什麼問題,但是哪怕只出一次問題就值得考慮要不要放棄簡寫了。這裡應當改為 background-color: red;

類似的,像 margin: 0; 這樣的宣告的確簡潔清爽,但是還是應當儘量寫清楚。如果你只是想修改底邊邊距,就要具體一些,寫成 margin-bottom: 0;

與此同時你需要宣告的屬性也要寫清楚,不要因為簡寫而波及其它屬性。例如如果你只想改掉底部的 margin,那就不要用會把其它邊距也清零的 margin: 0

簡寫雖然好,但也很容易濫用。

ID

在我們開始處理選擇器之前,牢記這句話:

在 CSS 裡堅決不要用 ID。

在 HTML 裡 ID 可以用於 JS 以及錨點定位,但是在 CSS 裡只要用 class,一個 ID 也不要用。

Class 的優勢在於複用性,而且私有度也並不高。在專案中私有度非常容易導致問題,所以將其降低就尤為重要。ID 的私有度是 class 的 255 倍,所以在 CSS 中堅決不要使用。

選擇器

務必保持選擇器簡短高效。

通過頁面元素位置而定位的選擇器並不理想。例如 .sidebar h3 span{} 這樣的選擇器就是定位過於依賴相對位置,如果把 span 移到 h3 和 sidebar 外面時就很難保持其樣式。

結構複雜的選擇器將會影響效能。選擇器結構越複雜(如 .sidebar h3 span 為三層,.content ul p a 是四層),瀏覽器的開銷就越大。

儘量使得樣式不依賴於其定位,儘量保持選擇器簡潔清晰。

作為一個整體,選擇器應當儘量簡短(例如只有一層結構),但是 class 名則不應當過於簡略,例如 .user-avatar 就遠比 .usr-avt 好。

牢記:class 無所謂是否語義化;應當關注它們是否合理。不要強調 class 名要符合語義,而要注重使用合理且不會過時的名稱。

過度修飾的選擇器

由前文所述,過度修飾的選擇器並不理想。

過度修飾的選擇器是指像 div.promo 這樣的。很可能你只用 .promo 也能得到相同的效果。當然你可能偶爾會需要用元素型別來修飾 class(例如你寫了一個 .error 而且想讓它在不同的元素型別中顯示效果不一樣,例如 .error { color: red; } div.error { padding: 14px; }),但是大多數時候還是應當儘量避免。

再舉一個修飾過度的選擇器例子,ul.nav li a{}。如前文所說,我們馬上就可以刪掉 ul 因為我們知道 .nav 是個列表,然後我們就可以發現 a 一定在 li 中,所以我們就能將這個選擇器改寫成 .nav a{}

選擇器效能

雖然瀏覽器效能日漸提升,渲染 CSS 速度越來越快,但是你還是應當關注效率。使用簡短、沒有巢狀的選擇器,不使用全域性選擇器(* {})作為核心選擇器,避免使用日漸複雜的 CSS3 新選擇器可以避免這樣的問題。

譯註,核心選擇器:瀏覽器解析選擇器為從右向左的順序,最右端的元素是樣式生效的元素,是為核心選擇器。

使用 CSS 選擇器的目的

比起努力運用選擇器定位到某元素,更好的辦法是給你想要新增樣式的元素直接新增一個 class。我們以 .header ul {}這樣一個選擇器為例。

假定這個 ul 就是這個網站的全站導航,它位於 header 中,而且目前為止是 header 中唯一的 ul 元素。.header ul{}的確可以生效,但是這樣並不是好方法,它很容易過時,而且非常晦澀。如果我們在 header 中再新增一個 ul 的話,它就會套用我們給這個導航部分寫的樣式,哪怕我們設想的不是這個效果。這意味著我們要麼要重構許多程式碼,要麼給後面的 ul 新寫許多樣式來抵消之前的影響。

你的選擇器必須符合你要給這個元素新增樣式的原因。思考一下,「我定位到這個元素,是因為它是 .header 下的ul,還是因為它是我的網站導航?」這將決定你應當如何使用選擇器。

確保你的核心選擇器不是型別選擇器,也不是高階物件或抽象選擇器。例如你在我們的 CSS 中肯定找不到諸如 .sidebar ul {} 或者 .footer .media {} 這樣的選擇器。

表達清晰:直接找到你要新增樣式的元素,而非其親元素。不要想當然地認為 HTML 不會改變。用 CSS 直接命中你需要的元素,而非投機取巧。

完整內容請參考我的文章 Shoot to kill; CSS selector intent

!important

只在起輔助作用的 class 上用 !important。用 !important 提升優先順序也可以,例如如果你要讓某條規則一直生效的話,可以用 .error { color:red!important; }

避免主動使用 !important。例如 CSS 寫得很複雜時不要用它來取巧,要好好整理並重構之前的部分,保持選擇器簡短並且避免用 ID 將效果拔群。

魔數與絕對定位

魔數(Magic Number)是指那些「湊巧有效果」的數字,使用魔數非常不好,因為它們只是治標不治本而且缺乏擴充性。

例如使用 .dropdown-nav li:hover ul { top: 37px; } 把下拉選單移動下來遠非良策,因為這裡的 37px 就是個魔數。37px 會生效的原因是因為這時 .dropbox-nav 碰巧高 37px 而已。

這時你應該用 .dropdown-nav li:hover ul { top: 100%; },也即無論 .dropbox-down 多高,這個下拉選單都會往下移動 100%。

每當你要在程式碼中放入數字的時候,請三思而行。如果你能用一個關鍵字(例如 top: 100% 意即「從上面拉到最下面」)替換之,或者有更好的解決方法的話,就儘量避免直接出現數字。

你在 CSS 中留下的每一個數字,都是你許下而不願遵守的承諾。

條件判斷

專門為 IE 寫的樣式基本上都是可以避免的,唯一需要為 IE 專門處理的是為了處理 IE 不支援的內容(例如 PNG)。

簡而言之,如果你重構 CSS 的話,所有的佈局和盒模型都不用額外相容 IE。也就是說你基本上不用 <!--[if IE 7]> element{ margin-left:-9px; } < ![endif]--> 或者類似的相容 IE 的寫法。

Debugging

如果你要解決 CSS 問題的話,先把舊程式碼拿掉再寫新的。如果舊的 CSS 中有問題的話,寫新程式碼是解決不了的。

把 CSS 程式碼和 HTML 部分刪掉,直到沒有 BUG 為止,然後你就知道問題出在哪裡了。

有時候寫上一個 overflow: hidden 或者其它能把問題藏起來的程式碼的確效果立竿見影,但是 overflow 方面可能根本就沒問題。所以要治本,而不是單純治標

CSS 前處理器

我用 Sass。使用時應當靈活運用。用 Sass 可以令你的 CSS 更強大,但是不要巢狀得太複雜。在 Vanilla CSS 中,只在必要的地方用巢狀即可,例如:

.header{}
.header .site-nav{}
.header .site-nav li{}
.header .site-nav li a{}

這樣的寫法在普通 CSS 裡完全用不到。以下為不好的 Sass 寫法:

.header{
    .site-nav{
        li{
            a{}
        }
    }
}

如果你用 Sass 的話,儘量這麼寫:

.header{}
.site-nav{
    li{}
    a{}
}

相關文章