去年我寫了一篇「從 Vue 來看 CSS 管理方案的發展」來談現代主流前端框架對 CSS 的各種處理方案,相信對 Vue.js 已經熟悉的朋友,都知道 Vue file 裡面是透過「Scoped CSS」也就是 <style> 內的 scoped 屬性來隔離 component 之間的樣式規則。

即便如此,Vue-Loader 在 v9.8.0 之後也內建了 CSS Modules 的整合,提供開發者另一種不同的 scoped CSS 替代方案。

前言

CSS Modules 的概念,我想最早大約可以從 2013、14 年左右說起。

當時前端領域可以說是進入了前所未見的爆炸發展,JavaScript 各種框架如雨後春筍般冒出,而 CSS 的管理卻沒有一個好的方式,很大的原因在於 CSS 的程式化與 JavaScript 比較其實是相對困難的,至少在命名管理以及 CSS 的有效範圍 Scope 一直是 web 開發者長年的夢靨。 工具方面,雖然有 LESS、SASS、Stylus 這類的預處理器 (preprocessor) 來幫助我們做到繼承、重用、複寫等功能,然而即便是這樣,global scoped CSS 的問題卻始終沒有一個好的解決辦法。

當然,也有另一派人馬提倡的是,由 CSS 的命名學 / 架構論 來達到 CSS 模組的管理與複用,如 OOCSS、SMACSS、BEM 等。

不過遺憾的是,人性天生就是懶,像 BEM 這麼囉唆的命名方式天生就違反人性。 規範一旦沒有嚴格遵守,那就跟沒有一樣,更不用說手動處理還可能出錯。

於是後來在 React 陣營就出現了 CSS Modules 這類的解決方案 [註],透過工具來處理那些人工手動處理命名規範要做的事。 也就是說,由 webpack 做了 BEM 的事情,過去 BEM 是人工手動做,而 webpack 是交給工具自動化做,以類似 Javascript module 的方式來處理 CSS,簡化了人工流程,也減少了手動犯錯的機會。

如何使用

幸運的是,Vue-loader 在 v9.8.0 之後,CSS Modules 已經內建整合到我們的開發環境了。 所以當我們透過 vue-cli v3 執行 vue create [專案名稱] 建立專案後,完全無須多餘的設定,就可以直接來使用 CSS Modules。

使用的方式很簡單,我們只要在 <style> 加上 module 屬性,如:

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

然後,我們在 template 裡面這樣寫:

<template>
  <div id="app">
    <h1 :class="$style.red">Hello Vue!</h1>
  </div>
</template>

這時候,我們打開 console 來觀察,就會發現模板上的 :class="$style.red" 會被 vue-template-compiler 編譯成為 App_red_DWJpy 這個 class,並且在 style 也自動生成對應的樣式規則。

但是這裡要特別注意的是,如果你在 css modules 裡面的取名有分隔線,如:

<style module>
.title-color {
  color: red;
}
</style>

在透過 $style 取用的時候,如果寫成 $style.title-color 這是一個不合法的 JS 變數名稱,而且也不能寫成 $style.titleColor,只能透過 $style["title-color"] 的方式來取得。

上面說的 module 屬性會經由 Vue-Loader 編譯後,在我們的 component 產生一個叫 $style 的隱藏 computed 屬性。 換句話說,我們甚至可以在 script 裡面取得由 CSS Modules 生成的 class 名稱:

<script>
export default {
  created () {
    console.log(this.$style.red);  // App_red_DWJpy
  }
}
</script>

反過來說,我們當然也可以利用這種特性,在 template 這樣寫:

<template>
  <div id="app">
    <p :class="{ [$style.blue]: isBlue }">Am I blue?</p>
    <p :class="[$style.red, $style.bold]">Red and bold</p>
  </div>
</template>
<script>
export default {
  name: 'app',
  data (){
    return {
      isBlue: true
    }
  }
}
</script>
<style module>
.red {
  color: red;
}

.blue {
  color: blue;
}

.bold {
  font-weight: bold;
}
</style>

如上圖,當 data 裡的 isBlue 屬性為 true 時,「Am I blue?」的字樣就會新增對應的 class 變成藍色。

甚至透過 props 將 class 傳入子層元件也是可以的,像這樣:

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld
      msg="Welcome to Your Vue.js App"
      :titleClass="$style.titleColor"
    />
  </div>
</template>
<style module>
.titleColor{
  color: #f00;
}
</style>

子層元件 HelloWorld.vue

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String,
    titleClass: String
  }
}
</script>
<template>
  <div class="hello">
    <h1 :class="titleClass">{{ msg }}</h1>
  </div>
</template>

像這樣,我們就可以在 template 用 vue 的 class binding 語法來處理樣式了。 這也意味著我們在控制樣式的時候,可以更輕鬆地利用程式來組織。


如果我們想要在 JavaScript 裡面將獨立的 css 檔案作為 CSS Modules 來載入的話,要記得在檔名加上 .module. 的前綴,如:

import styles from './foo.module.css'

當然透過像 less/sass/scss/stylus 等預處理器編譯的文件也是 (相關 loader 要另行安裝) :

// works for all supported pre-processors as well
import sassStyles from './foo.module.scss'

如果不想在檔名加入 .module. 的前綴,則可以在 vue.config.js 裡面的 css.modules 設定為 true

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

以上就是在 Vue 的開發流程中使用 CSS Modules 的簡單介紹,關於 CSS Modules 的更多特性,大家可以參考這份 官方文件 會有更多詳細的說明。

工商服務插播

這是我最近在五倍紅寶石開設的 VueJS 入門課程,時間在九月底。 如果你對 VueJS 有興趣,卻總是不得其門而入,歡迎點此 https://5xruby.tw/talks/vue-js 報名參加,聽說早鳥票有打折,而且折扣還不少。

結論

不管是 Scoped Style 或是 CSS Modules ,其實都可以發現它們在使用上相當簡單,也在某種程度上解決了同樣的問題。 就開發上來說,Scoped Style 可以帶來完全無痛的開發體驗,在 <style> 加個 scoped 就可以隔離 component 之間的樣式。

但是當 css style 需要在多個 component 被 reuse 的時候,Scoped Style 在這方面反而顯得有些力不從心。 這時 CSS Modules 的出現,正好解決了這個問題,代價就是需要透過 $style 這個被生成的屬性來控制。 當然,適合的場景不同,彼此之間各有優缺點,端看當時專案的規模與需求來挑選使用就可以了。


註: 有人整理了一份 CSS-in-JS 的各種方案比較: https://github.com/MicheleBertoli/css-in-jsCSS Modules 也是其中之一。 如果你對 CSS-in-JS 有興趣的話,這裏推薦閱讀這篇 All You Need To Know About CSS-in-JS 非常徹底地解釋了各種關於 CSS-in-JS 的原理與想法。