什麼是CSS-in-JS?
顧名思義,CSS-in-JS就是可以使用JS來編寫CSS樣式,那麼爲什麼要用JS來編寫CSS呢?我寫CSS寫的好好的,幹嘛非給自己找不自在呢?相信以前大家都聽說過這麼一個詞:關注點分離,就算沒聽過這個詞那麼你肯定至少也聽說過這麼一句話:要把HTML、CSS和JS分開編寫,不要寫在一起形成耦合,不要寫行內樣式和行內腳本等,比如像這樣👇
<p style="line-height: 20px" onclick="console.log('styled-components')">
CSS-in-JS
</p>
但是React的出現打破了這一原則,All in JS是它經典的開發理念,雖然這樣違背了“關注點分離”這個原則,但是卻有利於組件之間的隔離,使得組件間可以高度解耦,可複用性高。
Vue中爲何很少見到類似的庫
相信有過Vue開發經驗的小夥伴們都知道,編寫組件的時候是這樣的👇
<template>
<h1>Vue</h1>
</template>
<script>
export default {
name: 'vue'
}
</script>
<style scoped>
h1 {
color: #999;
}
</style>
CSS直接就很完美的組件化了,不像React那樣組件和css文件是分離的,而是直接集成在一個文件中,壓根就用不到那些花裏胡哨的東西。但是這樣其實也是有一定的弊端的,比如CSS無法接受JS的傳值👇
<template>
<div></div>
</template>
<script>
import img from '@/assets/img.png'
export default {
name: 'vue',
data () {
return {
img
}
}
}
</script>
<style scoped>
div {
width: 100vw;
height: 100vh;
}
</style>
如果想要div有個背景圖應該怎麼辦?在<style>標籤裏應該怎麼寫?好像沒辦法把img這個變量給傳進去……
通常做法是這樣👇
<template>
<div :style="{background: `url(${img})`}"></div>
</template>
<script>
import img from '@/assets/img.png'
export default {
name: 'vue',
data () {
return {
img
}
}
}
</script>
<style scoped>
div {
width: 100vw;
height: 100vh;
}
</style>
但是這樣的話編譯過後就變成了嵌入在div標籤裏的行內樣式,不利於維護。
還有就是有很多樣式其實是公用的,比如flex👇
<template>
<div></div>
</template>
<script>
export default {
name: 'vue'
}
</script>
<style scoped>
div {
display: flex;
align-items: center;
justify-content: center;
}
</style>
如果在每一個組件中都寫這麼一段樣式,不僅繁瑣、還不利於維護,而且最關鍵的是每個用到這段樣式的組件都會生成一份div[data-v-xxx]的樣式,代碼有很大的冗餘不說,還會大幅度增大你的CSS文件體積,拖慢項目的運行速度。
那麼聰明的你也許會想到:我直接定義一份全局CSS樣式不就得了嘛?哪裏需要就給哪裏加個對應的class,像bootstrap那樣,在對應的標籤上加類名。
沒錯這確實是一種很好的解決方式,但是還記不記得用bootstrap的時候不僅僅是加個類名,你還需要安裝人家官方定義的那種DOM結構來寫你的<template>,還是會需要複製粘貼。而且最關鍵的是沒用辦法進行傳參,雖然flex這個樣式比較常用,但並不是所有的地方都是需要居中對齊,也需要動態來改變align-items和justify-content的話,那麼就只能使用笨方法:寫所有可能取值的類了。
也許你會說,那我寫一個公共組件不就得了👇👇👇
<template>
<div :style="{alignItems: align, justifyContent: justify}">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'vue',
props: {
align: {
type: String,
default: 'center'
},
justify: {
type: String,
default: 'center'
},
}
}
</script>
<style scoped>
div {
display: flex;
}
</style>
但是這樣又會帶來一個問題,就是假如我需要一個組件變成這個樣式怎麼辦?豈不是必須要被div給包裹住,於是乎你又會說:這還不簡單?寫成動態組件不就得了?
<template>
<component :style="{alignItems: align, justifyContent: justify}" :is="dom">
<slot></slot>
</component>
</template>
<script>
export default {
name: 'vue',
props: {
align: {
type: String,
default: 'center'
},
justify: {
type: String,
default: 'center'
},
dom: {
type: String,
default: 'div'
}
}
}
</script>
<style scoped>
div {
display: flex;
}
</style>
那假如我需要一堆組件共享一個樣式呢?比如像Element-UI那樣的嵌套寫法,而不是像全局CSS那種很Low的辦法,你可能會一拍胸口:沒問題,看我這就給你給你封裝一個出來👇
// Root.vue
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'root',
provide: {
color: 'yellow'
}
}
</script>
// Child1.vue
<template>
<h1 :style="{color}">
<slot></slot>
</h1>
</template>
<script>
export default {
name: 'child1',
inject: {
color: {
from: 'color',
default: 'blue'
}
}
}
</script>
// Child2.vue
<template>
<h2 :style="{color}">
<slot></slot>
</h2>
</template>
<script>
export default {
name: 'child2',
inject: {
color: {
from: 'color',
default: 'green'
}
}
}
</script>
用的時候只需這樣即可:
// App.vue
<template>
<root>
<child1>Child1</child1>
<child2>Child2</child2>
</root>
</template>
<script>
import Root from '@/components/Root'
import Child1 from '@/components/Child1'
import Child2 from '@/components/Child2'
export default {
name: 'app',
components: {
Root,
Child1,
Child2
}
}
</script>
如果覺得這個顏色看不清,只需把Root組件中的provide裏面的color: 'yellow’變成color: 'gray’然後再刷新一下頁面即可👇
真正意義上的實現了組件之間共享樣式!
但是這樣的話不覺得很麻煩嗎?
不但多了一層不必要的DOM結構,而且只是爲了個樣式重用和樣式組件化,就費了這麼半天勁,甚至一些基礎不是很牢固的小夥伴看到這裏都暈了,其實市面上早就有封裝好的庫,我們這樣就是在重複造輪子。
這時要是再提出幾個需求的話就很難實現了,比如我想繼承樣式,當然你會說用Sass、Less或者Stylus這種預處理器可以做到,那如果我想繼承的是默認標籤的樣式並進行擴展呢?我想繼承a標籤的默認樣式(下劃線、點擊變紅、點完變紫等)然後再加入一些字體大小、行高等樣式,就只能傻傻的純手寫了。那麼接下來我就爲大家介紹一個寫法優(zhuang)雅(bi)、成熟穩定並且在React生態圈大受歡迎但是在Vue生態圈卻無人知曉的大名鼎鼎的CSS-in-JS庫:styled-components!
vue-styled-components
顧名思義,styled-components就是樣式化的組件,由於styled-components是專門爲React量身定做的一個庫,所以不太適合在Vue項目中使用,我知道你看到這裏一定開始想罵人了:我特麼津津有味的看了半天,結果你告訴我styled-components不適合在Vue項目中使用?
不要着急,雖然styled-components沒法在Vue項目中使用,但是styled-components團隊專門爲Vue貼身打造了一個vue-styled-components,和React的styled-components用法非常相似,我們先從一個最簡單的案例進行入門,首先要進行安裝:
npm i -S vue-styled-components
或者
yarn add vue-styled-components
然後寫組件的時候不能再寫xxx.vue了,取而代之的xxx.js。你可以簡單的理解爲這是一個極簡的組件,沒有<template>也沒有<script>,只專注於樣式,寫法如下👇
// Flex.js
import styled from 'vue-styled-components'
const Flex = styled.div`
display: flex;
align-items: center;
justify-content: center;
`
export {
Flex
}
可以看到沒有了之前我們熟悉的<template><script><style>三大件,那麼標籤寫在哪呢?就寫在styled.的後面,你需要什麼標籤,你就styled.什麼標籤,比如styled.ul、styled.li、styled.input等等…
然後在它的前面需要一個變量來接收,起什麼名字都可以,如:const Okay = styled.xxx
接下來是一個ES6的模板字符串語法``,不懂的話可以參考一下阮一峯老師的博客
然後就可以在你的字符串模板裏面寫你想要的任何CSS樣式啦!不僅寫法和CSS一模一樣,甚至還支持類似於Sass、Less和Stylus的那種嵌套語法:
// Flex.js
import styled from 'vue-styled-components'
const Flex = styled.div`
display: flex;
align-items: center;
justify-content: center;
&:hover { background: gray; }
> :first-child { align-self: flex-start }
> :last-child { align-self: flex-start }
`
export {
Flex
}
平時你是怎麼用xxx.vue組件的,你就怎麼用這個xxx.js組件:
// App.vue
<template>
<flex>
<p>styled</p>
<span>components</span>
</flex>
</template>
<script>
import { Flex } from '@/components/Flex'
export default {
name: 'child1',
components: {
Flex
}
}
</script>
可以發現我們並沒有定義<slot>卻能插入正常位置當中,他會自動找到默認位置進行插入,因爲每一個標籤即是一個組件,不會存在比較複雜的DOM嵌套結構。接下來看看如何給CSS傳參:
// Ok.js
import styled from 'vue-styled-components'
const Ok = styled('div', {
bg: {
type: String,
default: '#eee'
}
})`
width: 100px;
height: 100px;
background: ${ props => props.bg }
`
export { Ok }
現在styled後面不是不是直接跟一個點和字符串模板了,而是把點換成括號,第一個參數是一個字符串,代表了你想要什麼DOM標籤,第二個參數和你寫Vue組件中的props一模一樣,也可以偷懶寫成數組形式:
// Ok.js
import styled from 'vue-styled-components'
const Ok = styled('div', ['bg'])`
width: 100px;
height: 100px;
background: ${ props => props.bg }
`
export { Ok }
${}裏面可以寫一個函數,這個函數返回的結果就會渲染成最後的CSS值,函數的第一個參數就是你的定義的屬性的集合,定義過後接下來我們看看如何進行使用:
// App.vue
<template>
<ok bg="#333"/>
</template>
<script>
import { Ok } from '@/components/Ok'
export default {
name: 'app',
components: { Ok }
}
</script>
可以看到我們傳進去的值完美生效,函數裏面也可以寫任何表達式:
// Ok.js
import styled from 'vue-styled-components'
const Ok = styled('div', ['bg'])`
width: 100px;
height: 100px;
background: ${props => {
alert(666)
return props.bg ? 'green' : 'blue'
}}
`
export { Ok }
可以看到你寫的函數邏輯就會運行,我們還可以給<router-link>添加一個樣式:
import styled from 'vue-styled-components'
// 不能直接引入router-link, 而是像這樣取到router-link
const RouterLink = Vue.component('router-link')
const StyledLink = styled(RouterLink)`
color: #333;
font-size: 1em;
text-decoration: none;
`
export default StyledLink
用的時候只需把<router-link>換成<styled-link>即可:
<styled-link to="/">Custom Router Link</styled-link>
還提供了類似於Element-UI的共享數據寫法:
import {ThemeProvider} from 'vue-styled-components'
new Vue({
// ...
components: {
'theme-provider': ThemeProvider
},
// ...
})
使用的時候<theme-provider>爲根組件:
<theme-provider :theme="{
primary: 'black'
}">
<wrapper>
// ...
</wrapper>
</theme-provider>
子組件需要接收一下數據:
const Wrapper = styled.default.section`
padding: 4em;
background: ${props => props.theme.primary};
`
再來看一個繼承的例子:
import StyledButton from './StyledButton'
const TomatoButton = StyledButton.extend`
color: tomato;
border-color: tomato;
`
export default TomatoButton
這樣就可以非常完美的實現樣式的擴展及複用,甚至還可以擴展原生標籤:
const Button = styled.button`
background: green;
color: white;
`
const Link = Button.withComponent('a')
與Vue原生組件的對比
vue-styled-components編譯過後會生成一個隨機的類名:
而Vue原生組件寫的樣式會生成一個data-v-xxx的隨機屬性:
這樣一來選擇器的效率就會有着較大差距,vue-styled-components只有一個選擇器,原生組件卻是兩個選擇器合併,而且屬性選擇器的效率比較低,無形之中就拉開了差距。
傳值方面原生組件給CSS傳值只能通過這種方式:
而vue-styled-components傳值後屬性不會顯示在標籤上,而是直接嵌入到CSS裏:
而且vue-styled-components由於是JS,所以可以寫JS代碼,非常的方便。
還有更多有趣好玩的功能請戳:styled-components
快去拿這玩意重構一下你的項目吧!