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 的排名:
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 無法控制當前元件以外的元素,例如 body
、html
、#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 就不用寫 class
或 id
了,直接使用標籤選擇器就好了。
雖然可以,但是效能會慢很多倍,因此不建議。
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 做減法。為了追求簡單可控,作者建議遵循如下原則:
- 不使用選擇器,只使用 class 名稱來定義樣式
- 不層疊多個 class,只使用一個 class 把所有樣式定義好
- 所有樣式通過 composes 組合來實現復用
- 不巢狀
前兩條原則,削弱了樣式中最靈活的部分,使得初學者很難接受。
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 吧。
參考資料
- CSS 進化論:從 CSS,SASS,BEM,CSS Modules到Styled Components|章辰@转转
- 新時代的網頁樣式 - CSS Module|Let’s Note Weiwei
- Vue.js 與 CSS Modules|Kuro’s Blog
- CSS Modules 在 Vue 的用法?和 CSS scoped 分別的優勢?|askiebaby
- Vue: scoped 樣式與 CSS Module 對比|yangcheng
- CSS Module|specialCoder
- 魚和熊掌的故事 - CSS Modules還是BEM|benweizhu
- CSS Modules 入門教程|ryn
- 關於 Vue Cli 目錄結構
- Vue 專案中的 css 管理策略 — 利用 scss 將樣式模組化