React拾遗:从10种现在流行的 CSS 解决方案谈谈我的最爱 (上)
最近写React一直在纠结样式的问题,今天找了篇看起来很不错的文章,认真读三遍先...
React拾遗:从10种现在流行的 CSS 解决方案谈谈我的最爱 (上)
Strong opinions are very useful to others.
Those who were undecided or ambivalent can just adopt your stance.
But those who disagree can solidify their stance by arguing against yours
鲜明的观点非常有用。摇摆不定的人可以省心直接接受你的观点。不同意的人则可以通过讨论更加巩固自己的观点。
前言
不得不承认 Vue 的css解决方式非常自然简洁,相比之下 css 一直是 React 的痛。 从旧宠 css modules 到 JSS 的各种衍生,到新宠 styled-components。几十种的解决方式,上百篇的教程和比较,已经说明了一切。大家一直在寻找最好的最适合自己的解决方式。 我试着先回顾一下一路下来用过和没用过的各种React的css解决方案,最后说说我最爱的方式。当然只要你喜欢,使用普通的 css 或者是 sass 来完成React的样式是完全可行的。用自己最喜欢的方式编程是最重要的。
普通 css 的不足
随着大家对新开发模式(组件化)下 css 使用的各种反思,个人总结主要有三个:
- 样式与状态相关的情况越来越多,需要动态、能直接访问组件state的css
- 现代web开发已经是组件化的天下,而css并不是为组件化而生的语言。
- 一切样式都是全局,产生的各种命名的痛苦,BEM等命名规则能解决一部分问题,但是当你使用第三方插件的时候却无法避免命名冲突。
Vue 的解决法
<style>
/* 全局样式 */
</style>
<style scoped>
/* 本地样式 */
</style>
一旦加上 scoped 属性,css就只能作用于组件当中。简单漂亮的解决(Even You 真的666)。美中不足的是样式并不能直接访问组件的状态,于是需要另外规定动态的css语法与此合并使用。
回顾 React 的解决法
1. 原生 inline style (对于我个人来说,这种我偶尔还是会用用)
const textStyles = {
color: 'White',
backgroundColor: this.state.bgColor;
}
<p style={textStyles}>inline style</p>
原生的解决方式就是inline style,这种在旧式开发上不推崇的css写法却非常的适合组件化的开发。inline style解决了之前提到的三个问题。但是这个是相对的,个人觉得不喜欢的地方在于:
- 发明了一套新的 css-in-js 语法,使用驼峰化名称等一些规则,需要重新熟悉不说,也没有自动补全 完(为方便讨论下文称作jss)
- 并不是支持所有的css,例如媒体查询, :before 和 :nth-child 等 pseudo selectors
- inline写法如果直接同行影响代码阅读,如果提取出来再加 namespace,比起传统的css都更繁琐
- 第三方插件如果只接受 className 不接受 style 就没法了
由于1,3只是个人偏好问题,所以之后的一批 css-in-js 库都坚持了 inline 和 jss,只是致力于解决对css的不完全支持问题,这些虽然都不是我的菜,但是,都是流行的解决方式。
2. Css-in-Js (对于我个人来说,这种了解就行,基本不用)
JSS
专门针对原生方法不完全支持css的不足,完成的改良版。
// 支持 hover, sass的 &, media query 等。
const styles = {
button: {
fontSize: 12,
'&:hover': {
background: 'blue'
}
},
ctaButton: {
extend: 'button',
'&:hover': {
background: color('blue')
.darken(0.3)
.hex()
}
},
'@media (min-width: 1024px)': {
button: {
width: 200
}
}
}
JSS 是一个底层库, 要在 React 中使用可以用 React-JSS, Styled-JSS等。选择多样,是此类解决法里不错的一个选择。
Radium
import Radium from 'radium'
const Button = () => (
<button style={styles.base}>
{this.props.children}
</button>
)
var styles = {
red: {
backgroundColor: 'red'
}
}
Button = radium(Button);
多个样式使用数组方便合并
<button
style={[styles.base,styles.primary]}>
{this.props.children}
</button>
使用了HOC的方式注入样式,可以方便传入各种配置
Radium(config)(App)
Aphrodite
import React, { Component } from 'react';
import { StyleSheet, css } from 'aphrodite';
const Button = () => (
<span className={css(styles.red)}>
This is red.
</span>
)
const styles = StyleSheet.create({
red: {
backgroundColor: 'red'
},
});
3. Css Modules
css modules 并不是 React 专用解决办法,适用于所有使用 webpack 等打包工具的开发环境。以 webpack 为例, 在css-loader 的 options 里打开 modules: true 现选项即可使用 css modules。
一般配置如下
{
loader: "css-loader",
options: {
importLoaders: 1,
modules: true,
localIdentName: "[name]__[local]___[hash:base64:5]" // 为了生成类名不是纯随机
},
},
使用如下(…这个我就用过)
import styles from './table.css';
render () {
return <div className={styles.table}>
<div className={styles.row}>
<div className={styles.cell}>A0</div>
<div className={styles.cell}>B0</div>
</div>
</div>;
}
/* table.css */
.table {}
.row {}
.cell {}
在解决了 scoped 的同时留下了些许的遗憾:
- class名必须是驼峰的形式,否则不能再js里面使用 styles.table 来引用
- 由于css模块化是默认的,当你希望使用正常的全局css的时候,需要通过 :local 和 :global 切换,不方便。
- 所有的 className 都必须使用 { style.className } 的形式
这个解决方法可以照常写css是一大优势,不过我对 2,3 比较不能容忍。一个轻量级的 babel-plugin-react-css-modules库提出了解决法,你可以照常写 'table-size' 之类带横杠的类名,在js里正常书写字符串类名,唯一的区别在于使用 styleName
关键字代替 className
, 以上例,结果如下
import './table.css';
render () {
return <div styleName='table'>
<div styleName='row'>
<div styleName='cell'>A0</div>
<div styleName='cell'>B0</div>
</div>
</div>;
}
使用styleName这一新关键字,甚至连局部css和全局css的区分也迎刃而解了。
<div className='global-css' styleName='local-module'></div>
使用时.babelrc
配置:
{
"plugins": [
["react-css-modules", {
// options
}]
]
}
这一解决法已经很接近我的喜好了,不过使用 styleName
遇到三方UI库该怎么办呢?
顺带一提,目前 create-react-app
还不支持 Css Modules,但处于 beta 的 create-react-app v2
已经支持。使用方法为一律将css文件命名为 XXX.modules.css
, 以上例,即为 table.modules.css
, 即可使用。这一解决法的优雅在于,全局的css可以正常使用,只有带.modules.css
后缀的才会被modules化。
Css Modules还有一大缺憾:和Vue的解决一样,因为css写在css文件,无法处理动态css。
4. Css-in-Js 新浪潮
有很多人并不买JSS的账(我算一个), Vue 的解决方式也算是一个启发,于是新的库尝试使用了 ES6 的模板字符串,在js文件里面写纯粹的css。
`
.table {
background: #333;
color: rebeccapurple;
}
`
这非常自然地解决了前面提到的原生方法缺陷之2:不支持所有css语法。这类解决法中最有名的是 styled-components,类似的还有 Emotion。起初这一方法的一大不便是编辑器不能格式化,lint和自动补完js中的css,但现在基本每个流行编辑器都能找到相应的插件解决这一问题.
styled-components
import styled from 'styled-components';
// `` 和 () 一样可以作为js里作为函数接受参数的标志,这个做法类似于HOC,包裹一层css到h1上生成新组件Title
const Title = styled.h1 `
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
// 在充分使用css全部功能的同时,非常方便的实现动态css, 甚至可以直接调用props!
const Wrapper = styled.section `
padding: 4em;
background: ${props => props.bgColor;}
`
const App = () => {
<Wrapper bgColor="papaywwhi">
<Title>Hello World, this is my first styled componenet!</Title>
</Wrapper>
}
值得注意的是支持对其他元素引用,且语法相当自然:
const Link = styled.a `
padding: 5px 10px;
background: papayawhip;
color: palevioletred;
`
const Icon = styled.svg `
transition: fill 0.25s;
width: 48px;
height: 48px;
${Link}:hover & {
fill: rebeccapurple;
}
`
并且能方便的给暴露className
props的三方UI库上样式:
const StyledButton = styled(Button)` ... `
有人喜欢给每个需要样式的标签重命名一个更有意义的名称的做法,但也有人觉得重命名非常繁琐。这类人可以试试Emotion
Emotion
import styled, { css } from 'react-emotion'
const Container = styled('div')`
background: #333;
`
const myStyle = css`
color: rebeccapurple;
`
const app = () => (
<Container>
<p className={myStyle}>Hello World</p>
</Container>
)
Emotion 支持 styled-components 的样式注入方式,同时也可以用 css()
关键字直接注入。同时也支持JSS和& :hover
等Sass语法,例如:
// sass
<div
className={css`
background-color: hotpink;
&:hover {
color: ${color};
}
`}
>
// JSS
<div
className={css({
backgroundColor: 'hotpink',
'&:hover': {
color: 'lightgreen'
}
})}
>
之前没注意,Emotion 允许直接写子元素样式!
import { css } from 'emotion'
const paragraph = css`
color: turquoise;
a {
border-bottom: 1px solid currentColor;
}
`
render(
<p className={paragraph}>
Some text. <a>
A link with a bottom border.
</a>
</p>
)
如果使用 babel-plugin-emotion
,你甚至可以直接使用 css 作为 props:
<div
css={`
color: blue;
font-size: ${props.fontSize}px;
&:hover {
color: green;
}
& .some-class {
font-size: 20px;
}
`}
>
非常简洁也独具亮点,且提供了符合各种胃口的解决方式。
Glamorous
Glamorous 和前两个库有很多类似之处,最大的不同是它坚守了JSS的阵线。不支持模板字符串写纯css。 Glamorous 基础用法有三种选择:
import
import glamor from 'glamorous'
// className 注入
const styles = glamor.css({
fontSize: 20,
textAlign: 'center',
})
<div
className={styles}
/>
// 2. 类styled components
const MyStyledDiv = glamor.div({margin: 1, fontSize: 1, padding: 1})
// 3. 使用自带组件,接受样式名和css为props
const { Div } = glamor
<Div
fontSize={20}
textAlign="center"
css={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center'
}}
>
Hello world!
</Div>
// 4. 以及混用
const MyStyledDiv = glamor.div({margin: 1, fontSize: 1, padding: 1})
const myCustomGlamorStyles = glamor.css({fontSize: 2})
<MyStyledDiv className={`${myCustomGlamorStyles} custom-class`} />
最后谈谈,我的最爱
1. styled-jsx
Next.js 的 zeit 出品,必属精品。
2. 意想不到的解决方案 (个人大爱)
两个css库,与 bootstrap 走上完全不同的路线,所谓的“原子类”。写小项目和demo我基本上第一件事就是
yarn add tachyons
篇幅意想不到变得太长了,在下篇我会展开讲解这两个个人偏爱的解决方法。本篇对现行的流行解决法做了个小归纳。方便大家查询和选择。在本篇里我用的最多的还是styled-components
,但现在已经切换到styled-jsx
了。 之后最想尝试的应该是Emotion
。说到底没有孰优孰劣,更多的是个人喜好。希望这篇归纳对大家有帮助。不足之处,也请大家能留言指出,互相学习,谢谢!