Vue.js 與 CSS 管理

在還沒學 Vue.js 之前,我是使用 BEM 來管理我的 CSS 檔案,但開始學 Vue.js 後,使用 Vue CLI 建立專案時,反而不知道該用 Scoped CSS 還是 CSS Modules,而且在套用其他 CSS 框架,反而不知道該怎麼管理 CSS。 因此這篇主要用來紀錄,這幾天查到的資料。

同步發表於 HackMD:Vue.js 與 CSS 管理

CSS 管理

我們都知道,CSS 樣式是全域的,非常難以管理。而且 CSS 一直以來都不算是個程式語言,需要透過預處理器 preprocessor(例如 SASS、LESS 等工具)來編譯,再配合 CSS 的命名學 / 架構(OOCSS、SMACSS、BEM 等)模組化管理。

1. 模組化的好處

當專案變大時,CSS 會變得難以管理,如果還有引入第三方程式,可能會造成程式碼衝突。因此面對大量的程式碼,模組化是好辦法,CSS 又是特別靈活,且容易非常遇到樣式覆蓋,權重等等問題。

模組化的好處:

  • 提升程式碼的復重
  • 提高開發效率、減少溝通成本
  • 降低耦合
  • 便於管理

2. 模組化遇到的問題

一般普遍的做法是使用 BEM 來模組化 CSS,確保元件的唯一性和可重用性,但它還是有多缺點:

  • class 名稱選擇成為了一個乏味的任務
  • HTML 會因為帶有這些長的 class 名稱而變得臃腫
  • 標籤的語義化變得不再必要

3. CSS-in-JS

在現今前端框架中,可以將各個元件拆分開來,每個元件可以
獨立不互相干擾。在某個元件內撰寫的樣式,即使有相同 class 名稱,但最後編輯的樣式只會作用在該元件內。

這類將樣式連同元件寫在一起的作法成為 CSS-in-JS

以下是 2019年 CSS IN JS 的排名:

State of CSS 2019

4. CSS Modules

CSS Modules 意思就是 CSS 模組化,由 webpack 幫我們自動化,動態產生 class 名稱,簡單來說 BEM 是人工手動命名,而 CSS Modules 是交由工具自動化處理。

Vue-loader 在 v9.8.0 之後,CSS Modules 已經內建整合到我們的開發環境中,所以使用 Vue CLI3 創建的 Vue.js 專案內建了 CSS Modules,建立專案後,不需要設定安裝就可以使用。

Scoped CSS

在說明 CSS Modules 之前,順便說明一下 Scoped CSS,之後好說明兩者之間的差異。

1. 使用方式

<style> 標籤上加上 scoped 屬性,即可啟用 scoped CSS:

<style scoped>
.example {
  color: red;
}
</style>

<template>
  <div class="example">hi</div>
</template>

經過編譯,樣式只被應用到這個元件中的元素上:

<style>
.example[data-v-f3f3eg9] {
  color: red;
}
</style>

<template>
  <div class="example" data-v-f3f3eg9>hi</div>
</template>

注意,scoped 無法控制當前元件以外的元素,例如 bodyhtml#app 等等。

2. 混用

你可以在一個元件中同時使用有 scoped 和非 scoped 樣式:

<style>
/* 全域樣式 */
</style>

<style scoped>
/* 區域樣式 */
</style>

3. 子元件

使用 scoped 後,父元件的樣式將不會滲透到子元件中。

例如父元件與子元件都有 .red,父元件的樣式不會去影響到子元件的 .red

但唯一的例外就是子元件的根節點,它會同時受其父元件的 scoped 樣式和子元件的 scoped 樣式的影響。這樣設計是為了讓父元件可以從佈局的角度出發,調整其子元件根元素的樣式。

4. 深度作用選擇器

有的時候我們需要對子元件的深層結構設置樣式,也就是父元件的樣式去影響子元件,這時可以使用 >>> 連接符號。

<style scoped>
.a >>> .b { /* ... */ }
</style>

但做法並不受推薦且應該避免,因為失去了元件的封裝性。

注意,SASS 無法編譯 >>> 連接符號。

5. Scoped 樣式不能代替 class

有人會認為使用 scoped CSS 就不用寫 classid 了,直接使用標籤選擇器就好了。

雖然可以,但是效能會慢很多倍,因此不建議。

6. 動態生成的內容

通過 v-html 創建的 DOM 內容不受 scoped 樣式影響。

7. 缺點

Scoped CSS 雖然也會在 class 名稱加上一個後綴屬性,將元件內 CSS 指定其作用域,避免發生樣式覆蓋的問題,雖然簡單好上手,但它也不是能完全避免衝突,而且不易管理樣式。

另外,引用包含 scoped 的第三方插件時如若需要修改樣式則需要全域修改,而且要注意權重問題,迫不得已再使用 !important

CSS Modules

<style> 標籤上加上 module 屬性即可使用。

<style module> /* ... */ </style>

注意,沒有 s

1. 樣式預設為區域

啟用 CSS Modules 後,相當於給每個 class 名稱外加了一個 :local,以實現區域化。

<style module>
.red {
  color: red;
}

/* 等同以下寫法 */
:local(.red) {
  color: red; 
}
</style>

經過編譯,會生成這樣的 CSS:

.ComponentName_red_27KJo {
  color: red;
}

ComponentName 為該元件的檔案名稱。這樣的好處就是,我們可以知道這個樣式屬於哪個元件,而且有效避開了 CSS 權重和 class 名稱重複的問題。

但要注意,只有 類別選擇器 以及 ID 選擇器 兩種會經過編譯。

假如要在 CSS Modules 內切換到全域,可以使用對應的 :global

<style module>

:global(.green) {
  color: green;
}

/* 定義多個全域樣式 */
:global {
  .green {
    color: green;
  }
  .yellow {
    color: yellow;
  }
}
</style>

或者直接新建一個 <style 來放置全域樣式。

2. 動態類別

module 屬性與 scoped 屬性最大的不同就是,對於所有創建的類別可體透過元件的 $style 物件訪問。

我們可以透過 JS 訪問樣式名:

export default {
  created () {
    console.log(this.$style.red);
  },
}

必須在模板中透過一個動態類別 :class 綁定來使用:

<template>
  <div>
    <h1 :class="$style.red">Test1</h1>
  </div>
</template>

如果有連字號,需使用 [字串]

<template>
  <div>
    <h1 :class="$style['my-red']">Test1</h1>
  </div>
</template>

$style 是隱藏的計算屬性,所以它也支持 :class 的物件/陣列語法:

<template>
  <div>
    <p :class="{ [$style.red]: isRed }">Am I red?</p>
    <p :class="[$style.red, $style.bold]">Red and bold</p>
  </div>
</template>

因為樣式是透過 $style 物件獲取的,因此可以通過 props 將樣式傳遞到子元件中。

<template>
  <div>
    <HelloWorld :titleClass="$style.titleColor"></HelloWorld>
  </div>
</template>

通過 props 將這些類傳遞到任何我們希望的深度中,這樣,在子元件中的任意位置使用這些 class 名稱就會變得極其容易。

3. 獨立檔案

如果要在 JavaScript 中將獨立的 CSS 檔案作為 CSS Modules 來載入,需要將在檔案名稱加上 .module. 的前綴。

import styles from './style.module.css';
export default {
  created() {
    this.$style = styles;
    console.log(this.$style);
  },
};

另外,如果想要省略 .module. 的前綴,可以將 vue.config.js 設定檔中的 css.modules 設定為 true

// vue.config.js
module.exports = {
  css: {
    modules: true
  }
}

4. 實現 SASS 變數與 JS 共享

CSS Modules 的 :export 關鍵字可以將 SASS 的變數輸出到 JS 中。

<script>
export default {
  created() {
    console.log(this.$style.primaryColor); // #10446a
  },
};
</script>

<style lang="scss" module>
$primary-color: #10446a;

:export {
  primaryColor: $primary-color;
}
</style>

5. 組合(Composition)

對於樣式復用,CSS Modules 提供了組合(Composition)來處理。

舉例一個按鈕元件來說明:

<style module>
.base {
  padding: 0.375rem 0.75rem;
  outline: none;
  border: none;
  background-color: #10446a;
  color: #eee;
  font-size: 1rem;
}

.danger {
  composes: base;
  background-color: #c32222;
}
</style>

如果使用 .title-red

<template>
  <button :class="$style.danger">BUTTON</button>
</template>

會被編譯成:

<button class="ButtonComponent_danger_2kVty ButtonComponent_base_2oiSC">BUTTON</button>

.danger 編譯後會變成兩個 class。

也可組合外部的 CSS 檔案的樣式:

.title-red {
  composes: red from './color.css';
}

注意,由於 composes 不是標準的 CSS 語法,會有錯誤提示。因此如果有使用預處理器,還是建議使用預處理器的繼承語法較佳。

6. keyframes

CSS Modules 對於 CSS 動畫有一點必須注意,就是動畫名稱必須先寫。

animation: animationName 0.5s; 

如果畫名稱寫在「後面」就會解析失敗。

7. CSS Modules 使用技巧

CSS Modules 是對現有的 CSS 做減法。為了追求簡單可控,作者建議遵循如下原則:

  1. 不使用選擇器,只使用 class 名稱來定義樣式
  2. 不層疊多個 class,只使用一個 class 把所有樣式定義好
  3. 所有樣式通過 composes 組合來實現復用
  4. 不巢狀

前兩條原則,削弱了樣式中最靈活的部分,使得初學者很難接受。

CSS Modules 的命名規範是從 BEM 擴展而來。BEM 將樣式分成 3 個級別,分別是:

  • Block 區塊:對應模組名稱
  • Element 元素:對應模組中的節點名
  • Modifier 修飾符:對應節點的相關的狀態

CSS Modules 中,元件名稱,剛好對應到 Block 區塊,因此只需要考慮到 Element 和 Modifier。

當然,都使用 CSS Modules 了,可以結合 composes,將 Element 和 Modifier 放到一起。

Scoped CSS 和 CSS Modules 比較

Scoped CSS 的使用不需要額外的知識,使用簡單好上手,可用於小型的專案。

但在更大型或更複雜的專案中,對於 CSS 的管理上,就必須更謹慎,雖然使用 CSS Modules 在模板中有大量的 $style 看起來並不那麼舒服,但它是有校避免樣式衝突的解決方案。

Vue 專案中的 CSS 管理

對於一般的專案來說,使用 SASS 來統一管理 CSS 還是最簡單的。

管理 CSS 檔案主要放置在 src 的 assets 內。

scss 資料夾:

  • utils:Sass 輔助工具
    • functions 函式
    • mixins 混合工具
    • variables 變數
  • config / base:網站基礎
    • reset 重置 CSS 樣式
    • basic 基本樣式
    • typography 文字樣式
  • helpers:輔助工具
  • components:元件
  • layout:整體布局
  • pages:view 相關樣式
  • theme:主題
  • vendors:第三方 CSS 檔
  • vendors-extensions:外部 CSS 自訂檔
  • all.scss:引入所有 scss 檔

總結

因為沒接觸過中、大型專案與多人協作,目前還無法理解 CSS Modules 帶來的好處,目前可能還是會使用單一 SASS 檔案管理 Vue 專案的 CSS 吧。

參考資料

TOP