教你在Vue項目中使用React超火的CSS-in-JS庫: styled-components

什麼是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

快去拿這玩意重構一下你的項目吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章