阅读 1685

在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 }
复制代码

可以看到你写的函数逻辑就会运行,还提供了类似于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

快去拿这玩意重构一下你的项目吧!

往期精彩文章