Vue中的作用域CSS和CSS模組的差異
現代Web開發中的CSS離完美還差得遠,這並不奇怪。現在,專案通常是相當的複雜的,而CSS樣式又是全域性性的,所以到最後總是極容易地發生樣式衝突: 樣式相互覆蓋 或 隱式地級聯到我們未考慮到的元素 。
為了減輕CSS存在的主要痛點,我們在專案中普遍採用 ofollow,noindex" target="_blank">BEM 的方法來。不過這隻能解決CSS問題中的一小部分。
對我們來說是幸運的,社群已經開發出了可以幫助我們更徹底地解決問題的解決方案。你可能已經聽說過 CSS Modules 、 Styled Componetns 、 Glamorous 或 JSS 。這些只是我們今天可以新增到專案中的一些最流行的工具。如果你對這個話題感興趣,你可以檢視這篇文章: @Indrek Lasn 詳細介紹了 CSS in JS的全部思想 。
使用Vue-cli構建的Vue應用程式提供了兩個很棒的內建解決方案: 作用域CSS 和 CSS Modules 。它們都有一些優點和缺點,所以讓我們仔細看看哪種解決方案更適合你。
作用域CSS
在Vue中引入了CSS作用域 scoped
這個概念, scoped
的設計思想就是讓當前元件的樣式不會影響到其他地方的樣式,編譯出來的選擇器將會帶上 data-v-hash
的方式來應用到對應的元件中,這樣一來,CSS也不需要新增額外的選擇器。也將解決CSS中選擇器作用域和選擇器權重的問題。
在Vue中,為了讓作用域樣式工作,只需要在 <style>
標籤新增 scoped
屬性:
<!-- Button.vue --> <template> <button class="btn"> <slot></slot> </button> </template> <style scoped> .btn { color: red; } </style>
通過使用PostCSS並將上面的示例轉換為以下內容,它僅將我們的樣式應用於相同的元件中的元素:
就像你看到的一樣,整個過程不需要做什麼就可以達到很好的效果: 作用域樣式 (CSS中一直以來令人頭痛的問題之一)。
現在假設你需要調整 Button
元件的寬度,你可以像平常使用一樣,在呼叫這個元件的地方新增一個額外的 class
來設定其樣式:
<!-- App.vue --> <template> <div id="app"> <Button class="btn-lg">click</Button> </div> </template> <script> import Button from "./components/Button"; export default { name: "App", components: { Button } }; </script> <style scoped> .btn-lg { padding: 10px 30px; } </style>
轉換後就像下面這樣:
這次還是一樣,不需要做什麼就可以很好的控制樣式。
不過請注意:這個特性存在一個缺陷,即如果你子元件的元素上有一個類已經在這個父元件中定義過了,那麼這個父元件的樣式就也會應用到子元件上。只不過其權重沒有子元件同類名的重。比如下面這個示例:
<!-- Button.vue --> <template> <button class="btn btn-lg"> <slot></slot> </button> </template> <style scoped> .btn { color: red; } .btn-lg { padding: 10px 20px; border: 2px solid red; } </style> <!-- App.vue --> <template> <div id="app"> <Button class="btn-lg">click</Button> </div> </template> <script> import Button from "./components/Button"; export default { name: "App", components: { Button } }; </script> <style scoped> .btn-lg { padding: 30px; border: 5px solid green; } </style>
編譯出來的效果如下:
還有一些情況是我們需要對子元件的深層次結構設定樣式。雖然這種做法並不受推薦,而且應該儘量去避免。比如下面這個示例, Button
元件下有一個 <span>
標籤,而在呼叫 Button
元件的父元件 App
中設定 span
樣式:
<!-- Button.vue --> <template> <button class="btn"> <span> <slot></slot> </span> </button> </template> <style scoped> .btn { color: red; } </style> <!-- App.vue --> <template> <div id="app"> <Button class="btn-lg">click</Button> </div> </template> <script> import Button from "./components/Button"; export default { name: "App", components: { Button } }; </script> <style scoped> .btn span { color: green; font-weight: bold; border: 1px solid green; padding: 10px; } </style>
編譯出來的結果如下:
從上面的結果可以看出來,在父元件 App.vue
中的樣式:
.btn span { color: green; font-weight: bold; border: 1px solid green; padding: 10px; }
上面這段樣式並沒有編譯出來,運用到子元件 Button.vue
中的 span
中。
在 scoped
樣式中,這種情況可以使用 >>>
連線符或者 /deep/
來解決:
<!-- App.vue --> <style scoped> .btn >>> span { color: green; font-weight: bold; border: 1px solid green; padding: 10px; } </style>
此時雖然依舊是在 App.vue
中 scoped
控制 Button.vue
元件中 span
,但上面不同的是,這次樣式生效。編譯出來的結果如下:
另外使用作用域樣式還存在一個問題。那就是對 v-html
中內在的標籤樣式不生效。比如下面這個示例:
<!-- Button.vue --> <template> <button class="btn"> <slot></slot> </button> </template> <style scoped> .btn { color: red; } </style> <!-- App.vue --> <template> <div id="app"> <Button class="btn-lg" v-html="vhtml"></Button> </div> </template> <script> import Button from "./components/Button"; export default { name: "App", data () { return { vhtml: 'Click <strong>7</strong>' } }, components: { Button } }; </script> <style scoped> strong { color: green; border: 1px solid green; padding: 10px; } </style>
編譯出來的結果如下:
從上圖可以看出來, v-html
中的 strong
標籤樣式並未生效。和前面在父元件的 scoped
中設定子元件內部標籤未生效一樣。當然,其解決方案也是同樣的, 使用 >>>
連線符或 /deep/
可以讓 v-html
中的標籤樣式生效。比如上面的示例,可以將程式碼修改為:
<!-- App.vue --> <style scoped> .btn /deep/ strong { color: green; border: 1px solid green; padding: 10px; } </style>
這個時候 v-html
中的 strong
樣式生效了,如下圖所示:
話又說回來,雖然 >>>
或 /deep/
可以幫助我們穿透已封裝好的元件中的樣式,但這也失去了元件封裝的效果。再次回到以前CSS中令人頭痛的問題: CSS作用域 。
簡單的小結一下,在Vue中 scoped
屬性的渲染規則:
- 給DOM節點新增一個不重複的
data
屬性(比如data-v-7ba5bd90
)來表示他的唯一性 - 在每個CSS選擇器末尾(編譯後生成的CSS)加一個當前元件的
data
屬性選擇器(如[data-v-7ba5bd90]
)來私有化樣式。選擇器末尾的data
屬性和其對應的DOM中的data
屬性相匹配 - 如果元件內部包含有其他元件,只會給其他元件的最外層標籤加上當前元件的
data
屬性
上面我們看到的是Vue機制內作用域CSS的使用。在Vue中,除了作用域CSS之外,還有另外一種機制,那就是 CSS Modules ,即 模組化CSS 。
CSS Modules
CSS Modules的流行起源於React社群,它獲得了社群的迅速的採用。Vue更甚之,其強大,簡便的特性在加上Vue-cli對其開箱即用的支援,將其發展到另一個高度。
在Vue中使用CSS Modules和作用域CSS同樣的簡單。和作用域CSS類似,在 <style>
標籤中新增 module
屬性。比如像下面這樣:
<style module> .btn { color: red; } </style>
然後在 <template>
裡這樣寫:
<template> <button :class="$style.btn">{{msg}}</button> </template>
這個時候編譯出來的效果如下:
正如上圖所示, :class="$style.btn"
會被 vue-template-compiler
編譯成為 .Button_btn_3ykLd
這個類名,並且樣式的選擇器也自動發生了相應的變化。
但在這裡有一點需要注意,我們平時有可能在類名中會使用分隔線,比如:
<style module> .btn-lg { border: 1px solid red; padding: 10px 30px; } </style>
如果通過 $style
呼叫該類名時要是寫成 $style.btn-lg
,這樣寫是一個不合法的JavaScript變數名。此時在編譯的時候,會報一個錯話資訊:
按鈕的樣式也不會生效。如果要生效,我們需要通過下面這樣的方式來寫:
<template> <button :class="$style['btn-lg']">{{msg}}</button> </template>
編譯出來的結果如下:
除了$style.btn-lg這種方式會報錯之外,寫在駝峰($style.btnLg)的也會報錯。
上面說的 module
屬性會經由Vue-loader編譯後,在我們的 component
產生一個叫 $style
的隱藏的 computed
屬性。也就是說,我們甚至可以在Vue生命週期的 created
鉤子中取得由CSS Modules生成的 class
類名:
<script> export default { created () { console.log(this.$style['btn-lg']) } } </script>
在瀏覽器的 console
中可以看到 modules
編譯出來對應的類名:
利用這樣的特性,在 <template>
也可以這樣寫:
<!-- App.vue --> <template> <div id="app"> <Button msg="Default Button" /> <Button :class="{[$style['btn-lg']]: isLg}" msg="Larger Button" /> <Button :class="{[$style['btn-sm']]: isSm}" msg="Smaller Button" /> </div> </template> <script> import Button from './components/Button' export default { name: 'app', components: { Button }, data () { return { isLg: true, isSm: false } } } </script> <style module> .btn-lg { padding: 15px 30px; } .btn-sm { padding: 5px; } </style>
這個時候編譯出來的結果如下:
如上圖所示,當 data
中的 isLg
屬性值為 true
時, Larger Button
按鈕的 padding
變了,按鈕也同時變大了。除此之外,我們還可以通過 props
將 class
傳到子元件中。比如像下面這樣使用:
<!-- Button.vue --> <template> <button :class="[$style.btn, primaryClass]">{{msg}}</button> </template> <script> export default { name: 'Button', props: { msg: String, primaryClass: '' } } </script> <style module> .btn { border: 1px solid #ccc; border-radius: 3px; padding: 5px 15px; background: #fefefe; margin: 5px; } </style> <!-- App.vue --> <template> <div id="app"> <Button msg="Default Button" /> <Button :class="{[$style['btn-lg']]: isLg}" msg="Larger Button" /> <Button :class="{[$style['btn-sm']]: isSm}" msg="Smaller Button" /> <Button msg="Primary Button" :primaryClass="$style['btn-primary']" /> </div> </template> <script> import Button from './components/Button' export default { name: 'app', components: { Button }, data () { return { isLg: true, isSm: false } } } </script> <style module> .btn-lg { padding: 15px 30px; } .btn-sm { padding: 5px; } .btn-primary { background: rgb(54, 152, 244); border-color: rgb(32, 108, 221); color: #fff; } </style>
編譯出來的效果如下圖所示:
如果我們想要在JavaScript裡面將獨立的CSS檔案作為CSS模組來載入的話,需要在 .css
檔名前新增 .module
字首,比如:
<script> import barStyle from './src/style/bar.module.css' </script>
如果你是在專案中引入的是處理器檔案也是如此,比如 .scss
檔案:
<script> import fooSassStyle from './src/scss/foo.module.scss' </script>
如果你覺得這樣比較麻煩,可以在 vue.config.js
檔案中 css.modules
設為 true
:
// vue.config.js module.exports = { css: { modules: true } }
注意,上面的示例建立的專案是使用Vue-cli 3建立的。如果是使用Webpack的話,需要根據Webpack的相關機制進行配製。
從上面的示例中我們可以看出。使用 module
和 scoped
不一樣的地方就是在於所有建立的類可以通過 $style
物件獲取。因此類要應用到元素上,就需要通過 :class
來繫結 $style
這個物件。它的好處是,當我們在HTML中檢視這個元素時,我們可以立刻知道它所屬的是哪個元件。如果你夠細心的話,可以看到編譯出來的類名,都會以元件名為字首,比如:
除了這個好處之外,還有另一個好處,即: 一切都變成顯式的了,我們擁有了徹底的控制權 。
總結
不管是CSS Modules還是作用域CSS,這兩種方案都非常簡單,易用。在某種程度上解決的是同樣的痛點(CSS的痛)。那麼你應該選擇哪種呢?
scoped
樣式的使用不需要額外的知識,給人舒適的感覺。它所存在的侷限,也正它的使用簡單的原因。它可以用於支援小型到中型的Web應用程式。在更大的Web應用程式或更復雜的場景中,對於CSS的運用,我們更希望它是顯式的,更具有控制權。比如說,你的樣式可以在多元件中重用時,那麼 scoped
的侷限性就更為明顯了。反之,CSS Modules的出現,正好解決了這些問題,不過也要付出一定的代價,那就是需要通過 $style
來引用。雖然在 <template>
中大量使用 $style
,讓人看起來很蛋疼,但它會讓你的樣式更加安全和靈活,更易於控制。CSS Modules還有一個好處就是可以使用JavaScript獲取到我們定義的一些變數,這樣我們就不需要手動保持其在多個檔案中同步。
最後還是那句話, 任何解決CSS的方案,沒有最好的,只有最合適的! 我們應該根據自己的專案、場景和團隊進行選擇。當然,不管選擇哪種方案,都是為了幫助我們更好的控制樣式,解決原生CSS中存在的痛點。最後希望這篇文章對大家有所幫助。