前端开发者学堂 - fedev.cn

React中编写CSS的姿势

发布于 大漠

在任何环境之下其实没有最佳,最有最适合,那么在React中编写CSS也是类似的。在React中有很多编写CSS的方式,在社区中讨论最多的应该是CSS In JS 和 CSS Modules。前段时间在《React中CSS Modules的使用》一文中探讨了在React中怎么使用CSS Modules来管理组件和项目的CSS。事实上,使用CSS Modules还是存有一定的缺陷。这篇文章将会和大家一起聊聊React中编写CSS的姿势,然后再会花一些篇幅来讨论CSS Modules编写CSS的最佳姿势。

React中编写CSS的系列痛点

其实我自己在React中编写CSS并不多,因为以前都是基于Vue的体系来做项目。现在切换到React技术栈,尝试着使用React来构建项目和组件库。到目前为止,我在项目React中使用过两种方式编写CSS:

  • 原生CSS结合处CSS处理器(Sass和PostCSS):该方式样式易于全局污染,多人协作易于发生冲突
  • 使用CSS Modules编写CSS,相比原生CSS是舒服多了,但引用组件库(独立库)就有点蛋疼,组件库样式覆盖令人头疼

这只是自己初步的感觉。但@Vjeux在分享CSS-in-JS时就抛出了React中编写CSS时令人头痛的一系列问题:

  • 全局污染:在CSS中,选择器的作用域概念并不太强,其机制虽然方便我们重写样式,但也带来痛楚,易于污染全局
  • 命名混乱:在多人协作(有的时候甚至是自己个人)在维护或开发一个项目的时候,容易造成样式冲突;而且命名混乱,难于维护。虽然BEM可以这种现象更好些,但还是难于避免命名的混乱,特别是在样式多变的情况之下
  • 依赖管理不彻底:组件应该是相互独立的,在引入一个组件时,应该只引入组件所需要的样式。但现在的做法是除了要引入JavaScript之外,还需要引入组件的CSS。不过组件自身无法彻底的管理相关的样式依赖。当然,到目前为止,Webpack的一些loader可以让这方面有所改善
  • 无法共享变量:复杂组件要使用JavaScript和CSS来共同处理样式,比如通过状态来动态更改样式风格。这样就需要通过JavaScript来处理CSS,就会造成有些变量在JavaScript和CSS中冗余。有可能CSS自定义属性会让这方面有所改变,但需相关验证

第一感觉,就编写CSS而言,在Vue的体系下要比在React的体系下爽多了。

React中编写CSS常见的方式

在React社区中有关于如何在React中编写CSS的讨论也非常的热烈,而常见的方式是以下几种:

  • Inline CSS
  • CSS In JS
  • Styled Components
  • CSS Modules

不过每种方式都有自己的利弊,也各有争议。接下来我们先不好坏,分别来体验一下这几种方式写CSS的不同之处。

在开始之前,先使用Create React APP来构建React的项目,用来验证React项目中编写CSS的不同之处。

╰─➤  npx create-react-app react-styling
╰─➤  cd react-styling
╰─➤  npm run start

React在没有任何其他工具的情况之下,仅支持两种处理样式的方式:style道具(props)与<style>标签和CSS样式表。style道具使用JavaScript对象(CSS属性和值),并最终将其转换为元素的内联样式,如下所示:

// JSX
<p style={{backgroundColor: 'red'}}>Hello, {props.name}</p>

还可以这样写:

// App Components
// JSX
<p style={{ backgroundColor: props.color }}>Hello, {props.name}</p>

// index.js
import App from './App';
ReactDOM.render(<App name="大漠" color="#306"/>, document.getElementById('root'));

第一种是和我们平时在DOM中的元素上使用style属性设置行内样式是一样的;第二种是使用props的给style设置样式,编译出来也是行内样式。这两种方式输出来的样式都是行内样式,如果从选择器权重来说,它的权重是非常高的,在调用组件,要覆盖其样式就不容易了。

我们还可以在一个样式表文件中设置样式,比如在App.css文件中:

.App {
    text-align: center;
}

.App-logo {
    animation: App-logo-spin infinite 20s linear;
    height: 40vmin;
    pointer-events: none;
}

App组件中可以通过className来调用App.css中的类名:

// App.js
import './App.css';

function App(props) {
    return (
        <div className="App">
            <header className="App-header">
                <img src={logo} className="App-logo" alt="logo" />
                <p style={{ backgroundColor: props.color }}>Hello, {props.name}</p>
            </header>
        </div>
    );
}

编译出来之后如下图所示:

这是React中编写CSS最基本的方式。这两种基本方式和我们在普通的Web中编写CSS方式是一样的。众所周知,Web中编写CSS是较为痛苦的,因为类名的冲突,全局的污染等等。所以社区才会借助一些工具的方式来改变编写CSS。

Vue的体系下有scoped和CSS Modules;在React中编写的方式就更多,即接下来要介绍的这些方式。

Inline CSS

Inline CSS称为内联CSS,即在HTML标签元素中使用style属性(Attribute)来定义CSS属性,常被称为行内样式。如下面这样:

<p style="font-size: 20px; color:#4a54f1; text-align:center; padding-top:100px;">
    Hello World!!!
</p>

而在Web中,我们一直提倡的是结构和样式分离,所以一直不推荐行内样式这样的写法。而如今天在React框架体系中,又开始提Inline CSS,那么是不是有点倒退了的意思呢?该话题先不讨论,这里咱们只来聊聊Inline CSS在React中的使用,并且该使用是否适合我们的开发习惯。

在React开发中,我们一般使用JSX编写。JSX将被转换为React.createElement(...),所有的属性都将成为props对象的一部分。

有关于JSX的详细介绍,可以阅读《深入了解JSX》一文。

在React中,内联样式不指定为字符串style属性接受带有驼峰格式的JavaScript对象。例如:

margin-top       ─➤ marginTop
border-radius    ─➤ borderRadius
font-weight      ─➤ fontWeight
background-color ─➤ backgroundColor

下面是React中定义内联CSS的基本步骤:

  • 将CSS属性名称用驼峰的格式编写,比如font-size要写成fontSize
  • 创建一个对象,将所有CSS属性作为key,CSS属性的值作为value
  • 将该对象分配给style属性

我们来尝试着将上面的HTML代码,看看在React中怎么来实现:

// App.js
import React from 'react';

function App() {
    const styleObj = {
        fontSize: '20px',
        color: '#4a54f1',
        textAlign: 'center',
        paddingTop: '100px'
    }
    return (
        <p style={ styleObj }>Hello World!!!</p>
    );
}

export default App;

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

你将看到的效果如下:

上面的写法和JSX中在style中引入需要的样式得到的结果是一样的:

<p style={{ fontSize: '20px', color: '#4a54f1', textAlign: 'center', paddingTop: '100px' }}>Hello 大漠!!!</p>

从JSX中的对象移到了React组件中显式声明一个styleObj:

这种方法可以更容易地在组件中协调样式,并理解每个组件具有什么样式。

从上面来看,React原生写样式的方式和内联样式(Inline Style)是相同的

在Web开发中,我们时常会碰到通过JavaScript来动态的修改元素的样式。我们也知道,如果使用JavaScript动态的修改元素样式,一般都会内联到元素标签上(当然,动态添加类名也可以修改元素的样式)。就此而言,在React中的内联样式就显得有用处了。假设我们有一个列表,偶数和奇数项的样式风格不一致,在React中,我们使用内联样式就显得容易得多了。比如下面这个示例:

// ListItem
import React from 'react';

function ListItem({ title, isOdd }) {
    const StyleObj = {
        padding: '.75rem 1.25rem',
        margin: '0 0 10px',
        border: '1px solid transparent',
        borderRadius: '.25rem',
        backgroundColor: isOdd ? '#b8daff' : '#d4edda',
        color: isOdd ? '#004085' : '#155724',
        borderColor: isOdd ? '#b8daff' : '#c3e6cb',
    }
    return (
        <li style={ StyleObj }>{title}</li>
    );
}

export default ListItem;

// ToDo.js
import React from 'react';

import ListItem from './ListItem';

function ToDo(props) {
    const todoList = props.todoList.map(el => <ListItem key={el.title} title={el.title} isOdd={el.isOdd} />)
    return (
        <ul style={{ margin: 0, padding: '10px', list: 'none outside none' }}>
            { todoList }
        </ul>
    );
}

export default ToDo;

// App.js
import React from 'react';
import ToDo from './ToDo';

const todoList = [
    {
        title: 'JavaScript',
        isOdd: true
    },
    {
        title: 'CSS',
        isOdd: false
    },
    {
        title: 'React',
        isOdd: true
    }
]

function App() {
    return (
        <ToDo todoList={todoList} />
    );
}

export default App;

得到的效果如下:

我们回过头来思考:

React的一个基本概念是封装。组件可以控制自己的渲染,并且可以基于新的状态来更新组件的UI。

这意味着在React中可以改变基础数据和告诉应用程序状态发生了变化,组件可以根据状态的变化自动去更新视觉层。而内联样式恰好更进一步扩展了React的封装概念。组件不仅知道如何以及何时渲染它们自己,而且还包含它们需要的所有样式,因此内联样式提倡将组件对应的样式放置到组件中,它们的行为和外观就会像你想要的那样。其实这也是Web Component的核心思想。从这个角度来思考的话,内联样式具备较大的优势,但是使用内联样式存在不少的缺点:

  • 几乎没有什么可复用性而言(虽然通过其他方式可以避免,稍后会聊)
  • 不能充分的利用CSS的功能,比如无法使用伪类伪元素媒体查询@keyframes结构性选择器
  • 难于维护、编辑和更新(覆盖),而且大量的内联CSS会降低代码的可读性
  • 它会影响性能,每次得新渲染样式对象时都会重新计算

内联样式虽然有这些不足,但社区的力量是强大的,可以迂回来解决上述提到的不足。比如下面这些库:

虽说内联式有诸多的不足,但存在并定有其意义。它可能在Facebook上运行得很好。或许你有一个,或者只有几个项目,它们都有相同的样式风格,并且有数百个开发人员在一起协同开发。在这种情况之下,大家都不相信全局样式,很有可能会有人更新CSS,一不小心就破坏了你的组件,所以在组件级别上使用内联样式是很有意义的。但是,如果你构建的Web组件是有可能跨多个项目使用时,那么使用内联样式将会极为痛苦的,因为使用内联样式会导致比它解决的问题还要多得多。

简单小结一下,如果你的组件需要跨多项目使用,强烈建议不在组件级别中使用内联样式;对于需要动态更换样式,可以考虑使用内联样式

CSS In JS

@Vjeux分享了CSS-in-JS,即利用解决React中编写CSS。其基本原理是,很多本来很难用CSS实现的事情变得非常容易,那是因为使用JavaScript让事情变得更容易。

一个特别有趣的概念是CSS-in-JS 将CSS抽象到组件级别本身,使用JavaScript以声明性和可维护性的方式描述样式。随着@Max Stoiber的styled-components(稍后会聊到)发布,这个概念在今天比以往任何时候都更加主流。

在CSS-in-JS中是使用JavaScript对象文本的形式编写CSS,看上去非常接近自然语言,并使用style属性将它们应用到组件中。另外,样式可以放在单独的模块文件中,然后使用常见的import语句将分离出来的CSS模块文件导入进来。比如下面这个简单的示例:

// Card.css.js
export default {
    card: {
        position: 'relative',
        // ...
    },
    cardImgTop: {
        maxWidth: '100%',
        // ...
    },
    cardBody: {
        flex: '1 1 auto',
        // ...
    },
    cardTitle: {
        margin: '0 0 .75rem',
        fontSize: '1.25rem',
        
    },
    cardText: {
        margin: '0 0 1rem'
    },
    button: {
        display: 'inline-flex',
        // ...
    }
}

// Card Component: Card.js
import React from 'react';
import logo from './logo.svg';
import styles from './Card.css.js';

function Card(props) {
    return (
        <div style={styles.card}>
            <img src={logo} style={styles.cardImgTop} alt={props.title} />
            <div style={styles.cardBody}>
                <h5 style={styles.cardTitle}>{props.title}</h5>
                <p style={styles.cardText}>{props.text}</p>
                <a href={props.linkUrl} style={styles.button}>{props.linkText}</a>
            </div>
        </div>
    );
}

export default Card;

// App.js
import React from 'react';
import './App.css';

import Card from './Card';

function App(props) {
    return (
        <div className="App">
        <Card title="Card Title" text="Some quick example text to build on the card title and make up the bulk of the card's content." linkText="Go W3cplus" linkUrl="https://www.fedev.cn"/>
        </div>
    );
}

export default App;

上面示例的使用方式和Inline Style得到的结果是一致的。CSS样式内联到标签元素的style属性上。那么CSS-in-JS和内联样式有什么不同之处呢?

CSS-In-JS是一种使用JavaScript对组件进行样式化的技巧。当解析此JavaScript时,将生成CSS(通常作为<style>元素)并将其附加到DOM中。不过此功能需要借助于第三方工具来实现。比如后面说的styled-components

另外在上一节也聊到过,内联样式虽然也是可以通过style对象将CSS嵌套到DOM元素上,但并不是所有CSS特性都可以使用JavaScript来处理,比如::before:hover之类。而CSS-in-JS却可以发挥CSS所有特性,因为它会生成实际的CSS,这也就可以使用任意的选择器。

CSS-in-JS和内联样式一样,有利也有弊。在使用CSS-in-JS时常见的弊端如下:

  • 有一定的学习曲线
  • 如果要让JavaScript具备所有CSS的特性,需要依赖一定的工具包
  • 新手难以将其应用在项目中,需要学习更多的东西

而其优势是:

  • 面对组件:不再需要维护一大堆样式文件;CSS-in-JS在组件层面(而不是文档层面)对CSS进行抽象,可以真正的做到模块化
  • 利用JavaScript生态系统的强大能力来增加CSS
  • "真正的能做到样式规则的隔离":对选择器定义作用域还远远不够。如果没有显式定义,CSS的属性就会自动继承自父元素,而使用JSS-isolate插件可以防止JSS规则继承属性
  • 选择器作用域:CSS只有一个全局的命名空间,所以是无法避免出现选择器冲突的。或许BEM这样的命名规范对单个项目来说能起到作用,但要与第三方代码集成时就不一定了。JSS在将JSON编译成CSS时会自动生成唯一的类名
  • 浏览器前缀:CSS规则自动添加了浏览器引擎前缀
  • 代码共享:在JavaScript和CSS之间共享常量和函数
  • 只生成页面会用到的样式
  • 移除无效代码
  • CSS单元测试

可以说,利大于弊。

在社区中,同样有很多插件为CSS-in-JS编写CSS扩展能力。社区中较为流行的几个CSS-in-JS插件如下图所示:

有关于CSS-in-JS更多的讨论还可以阅读下面这些文章:

Styled-Components

**Styled-Components**这个词在前面已经出现过好几次了。

其实他也是CSS-in-JS众多库中的一个,但其有着自己足够强的地位。也常被称为野兽模式

有很多UI库采用Styled-Components来管理他们的CSS

Styled-Components通过定义带有封装样式的组件,而不使用CSS类作为中间层,该库让你的React项目管理CSS(或React组件使用CSS)变得更容易。

Styled-Components是通过使用ES6模板字符串(Template Literal)定义组件。CSS属性可以像使用CSS一样添加到组件中。在解析JavaScript时,Styled-Components将生成唯一的类名,并将CSS注入DOM。它有几个主要概念:

  • 自动渲染页面上使用的组件,可以加载最少的代码
  • 为使用的样式提供唯一的hash名,不会碰到命名的冲突,重叠、或拼写错误的问题
  • 如果组件未使用并删除,则其所有样式都将随之删除

另外,Styled-Components不需要增加额外的学习成本,因为使用Styled-Components编写组件样式的时候,你就是在写JavaScript代码,不需要依赖其他的语言,比如CSS,Sass等。

有关于Style-Components详细的使用可以查阅其官方文档

接下来只是向你展示一些最有用的技巧,通过几个简单的示例向你展示Styled-Components是很强大的,也是一个很优秀的库。在正式向大家演示其功能前,需要先安装该库。可以在你的命令终端上执行:

╰─➤ npm i styled-components -D

这样就可以在React中使用Styled-Components了。

创建组件

React组件的创建可以有很多种方式。Styled-Components库也提供了创建组件的功能。从小的UI组件到复杂的组件都可以。比如我们要创建一个按钮组件,我们可以像下面这样来构建:

// App.js
import styled from 'styled-components'

const Button = styled.button`
    background: transparent;
    border-radius: 3px;
    border: 2px solid palevioletred;
    color: palevioletred;
    margin: 0 1em;
    padding: 0.25em 1em;
`

function App(props) {
    return (
        <div className="App">
        <header className="App-header">
            <img src={logo} className="App-logo" alt="logo" />
            <p style={{ backgroundColor: props.color }}>Hello, {props.name}</p>
            <Button> Click Me</Button>
        </header>
        </div>
    );
}

你将看到的效果如下:

正如我们所看到的,可以通过使用JavaScript将每个元素都单独的声明一个小组件,类似Atomic Design中的ATOMS(即元素):

这样一来,我们可以把App.js中的所有元素按类似的方式来改造(就可以不需App.css):

// App.js
import React from 'react';
import logo from './logo.svg';

import styled, { keyframes } from 'styled-components'

const Button = styled.button`
    background: transparent;
    border-radius: 3px;
    border: 2px solid palevioletred;
    color: palevioletred;
    margin: 0 1em;
    padding: 0.25em 1em;
`

const AppContainer = styled.div`
    text-align: center;
`
const AppHeader = styled.header`
    background-color: #282c34;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    font-size: calc(10px + 2vmin);
    color: white;
`
const AppLogoSpin = keyframes`
    0% {
        transform: rotate(0deg);
    }
    100% {
        transform: rotate(360deg);
    }
`
const AppLogo = styled.img`
    animation: ${AppLogoSpin} infinite 20s linear;
    height: 40vmin;
    pointer-events: none;
`

function App(props) {
    return (
        <AppContainer>
            <AppHeader>
                <AppLogo src={logo} alt="logo" />
                <p style={{ backgroundColor: props.color }}>Hello, {props.name}</p>
                <Button> Click Me</Button>
            </AppHeader>
        </AppContainer>
    );
}

export default App;

效果如下:

其实这样做还有另一个好处,可以让你的JSX代码更为干净,并且组件层次结构更具语义化。

在Styled-Components中任何一个元素都可以是一个样式组件,这和Atomic Design中的Atomic很稳合。如果你钟情于Atomic Design的话,我想你会特喜欢Styled-Components。

使用props改变样式

在一些UI框架上,不少组件在不同的状态有不同的效果,比如Bootstrap中的按钮,他就有多种UI风格:

在React中如果借助Styled-Components来控制按钮的颜色或样式,我们可以通过props来样式组件传递值。如果把propsprimary的值设置为true时,按钮的样式就会发生变化。

const Button = styled.button`
    display: inline-flex;
    font-weight: 400;
    justify-content: center;
    align-items: center;
    user-select: none;
    padding: .375rem .75rem;
    font-size: 1rem;
    border-radius: .25rem;
    margin: 5px;

    // Adapt the colors based on primary prop

    border: 1px solid ${props => props.primary ? '#007bff' : '#fefefe'};
    background: ${props => props.primary ? '#007bff' : '#fefefe'};
    color: ${props => props.primary ? '#fff' : '#212529'};
`

<Button primary> Primary Button</Button>
<Button>Default Button</Button>

效果如下:

扩展样式

上面我们看到了props可能让我们根据传递的值的真假来给按钮处理不同的样式。

还是回到Bootstrap的按钮组件来举例,我们除了默认按钮的风格还有很多种不同的样式风格。在Styled-Components中可以创建一个继承它的样式的新组件,将它封装在style()构造函数中。比如:

const DefaultButton = styled.button`
    display: inline-flex;
    font-weight: 400;
    justify-content: center;
    align-items: center;
    user-select: none;
    padding: .375rem .75rem;
    font-size: 1rem;
    border-radius: .25rem;
    margin: 5px;
    border: 1px solid #fefefe;
    background: #fefefe;
    color: #212529;
`

const PrimaryButton = styled(DefaultButton)`
    color: #fff;
    background-color: #007bff;
    border-color: #007bff;
`

const SecondaryButton = styled(DefaultButton)`
    color: #fff;
    background-color: #6c757d;
    border-color: #6c757d;
`

const SuccessButton = styled(DefaultButton)`
    color: #fff;
    background-color: #28a745;
    border-color: #28a745;
`

const DangerButton = styled(DefaultButton)`
    color: #fff;
    background-color: #dc3545;
    border-color: #dc3545;
`

const WarningButton = styled(DefaultButton)`
    color: #212529;
    background-color: #ffc107;
    border-color: #ffc107;
`

const InfoButton = styled(DefaultButton)`
    color: #fff;
    background-color: #17a2b8;
    border-color: #17a2b8;
`

<DefaultButton>Default Button</DefaultButton>
<PrimaryButton>Primary Button</PrimaryButton>
<SecondaryButton>Secondary Button</SecondaryButton>
<SuccessButton>Success Button</SuccessButton>
<DangerButton>Danger Button</DangerButton>
<WarningButton>Warning Button</WarningButton>
<InfoButton>Info Button</InfoButton>

效果如下:

给任何组件添加样式

Styled-Components可以在你自己的或第三方组件上完美工作。可以将className传递给一个DOM元素:

const Link = ({className, children}) => (
    <a className={className}>{children}</a>
)

const StyledLink = styled(Link)`
    color: orange;
    font-weight: 700;
`

<Link>Own Components</Link>
<StyledLink>Styled, Exciting Link</StyledLink>

效果如下:

动态改变元素

假设你有一个BodyText组件。但是有时候希望BodyText充当<a>链接的角色,但又不想复制样式。在Styled-Components中,可以很容易地交互样式化的元素。

const BodyText = styled.span`
    font-size: 1rem;
    color: red;
    padding-bottom: 10px;
    border-bottom: 3px double currentColor;
`

<BodyText>Body Text</BodyText>
<BodyText as="a">This is now a Link</BodyText>

在CSS中引用样式化组件

有时候,你可能会希望在父元素悬浮的时候更改子元素的样式。在CSS中会使用.parent:hover .childer{}这样的选择器,而这些选择器对重构来说是较为麻烦的。在Styled-Components中,做这样的事情变得更为容易,只需直接引用在CSS中创建的组件。

const ParentElement = styled.div`
    color: purple;
    background: #fff;
    padding: 10px 15px;
    cursor: pointer
    margin: 10px;
`

const ChildElement = styled.div`
    background: purple;
    color: #fff;
    padding: 5px;
    border-radius: 5px;
    margin: 10px;
    transition: all .28s ease;

    ${ParentElement}:hover & {
        background: orange;
        color: purple;
        filter: drop-shadow(2px 4px 6px black);
    }
`

<ParentElement>
    This is Parent Element, Please hover me!
    <ChildElement> This is Child Element</ChildElement>
</ParentElement>

CSS Modules

上图简单的阐述了CSS Modules。如果你想更深入的了解CSS Modules,可以先阅读@ahfarmer的《What are CSS Modules? A visual introduction》一文。

简单地说,CSS Module是一个CSS文件,默认情况下,所有类名和动画名的作用域都是局部的(组件内)

CSS Modules是非常强大的,也是社区中编写CSS的主流方式之一。当然,CSS Modules不仅仅是为React体系而生,在Vue中也可以使用CSS Modules来维护和管理CSS;在不借助任何JavaScript框架体系,使用PostCSS也可以通过CSS Modules来维护和管理CSS

另外,使用CSS Modules相对而言更为复杂一些,除了要了解CSS Modules的特性和使用之外,还需要了解怎么将其添加到项目的构建体系中。特别是在React中,配置CSS Modules相关的特性要比Styled-Components复杂的多。有关于这方面不做过多详细介绍,要是感兴趣的话,可以阅读《React中CSS Modules的使用》一文。

React中CSS Modules的使用》一文除了介绍了怎么配置CSS Modules的工作环境之外,还详细的介绍了一些有关于CSS Modules的特性和具体使用。

再补充一点,在配置CSS Modules会使用到不少关于Webpack的相关知识,如果你对这方面了解不多的话,建议你花点时间阅读下面两篇文章:

感觉有点跑题了,那么在React中如何使用CSS Modules,我想使用@Alcides Queiroz的《Using CSS Modules in React》教程中的一张图来阐述:

正所谓,一图胜过千言万语。如果你想更深入的了解,可以阅读《React中CSS Modules的使用》一文。

如何解决编写CSS的痛点

编写CSS的痛点是什么不再多说,谁写谁知道。

时至今日,在React社区中编写和维护CSS讨论最多的两大阵营就是CSS-in-JSCSS Modules。那么我们就来一起看看这两大阵营又是如何解决编写CSS的痛点的。

命名空间和避免冲突

默认情况之下,CSS-in-JS和CSS Modules的都有作用域的存在,都是本地作用域名(局部作用域),这意味着没有必要使用命名空间。如果你不计较类名的命名的语义化,那么可以使用极短的命名,短到只用一个字母都有可能。就算是有命名空间之最的BEM也是无法匹敌的。

没了命名空间的烦恼的话,也就没有命名冲突和选择器权重一说。CSS-in-JS和CSS Modules定义的样式的方式:

  • CSS-in-JS一般使用对象属性,比如{button:{}}
  • CSS Modules一般使用简单的类名,比如.button{}

消除无用的代码

在CSS使用一个复杂的选择器,一般是作者(CSSer)无法或者很难知道是否以及在哪里使用了某些样式,特别是在多人协作的情景中,该现象最为普遍。使用模块和依赖项就能让编写CSS的同学容易知道使用了什么,以及可以随心所意(足够自信)地删除未使用的样式。

在CSS-in-JS和CSS Modules中用到的都是些局部变量,这也意味着构建工具也可以发现任何未使用的局部变量并删除它们。

组合模式

CSS-in-JS和CSS Modules都提供了复用样式的解决方案。在这方面,CSS-in-JS会更为灵活一些,毕竟CSS也是JavaScript的一个对象,所以可以利用JavaScript提供的任何东西(特性),比如Object.assign()来复用重复性的样式。

CSS Modules在这方面略为逊色一点,需要使用compposes规则:

.button {
    display: inline-flex;
    font-weight: 400;
    justify-content: center;
    align-items: center;
    user-select: none;
    padding: .375rem .75rem;
    font-size: 1rem;
    border-radius: .25rem;
    margin: 5px;
    border: 1px solid #fefefe;
    background: #fefefe;
    color: #212529;
}

.primaryButton {
    composes: button;

    color: #fff;
    background-color: #007bff;
    border-color: #007bff;
}

依赖关系

CSS-in-JS是纯JavaScript,所以它可以与任何模块系统导出(export)、导入(import)样式;CSS Modules中同样要借助关键词composes来引用其他样式文件中的类名:

.primaryButton {
    composes: button from './styles/button.css';
    color: #fff;
    background-color: #007bff;
    border-color: #007bff;
}

有条件的样式

有条件的样式指的是根据状态或其他条件来更改样式。这种方式常常会用于一些带交互的场景或数据发生改变的场景,该方式也是另一种的组合方式。

在CSS-in-JS中常常类似下面这样来使用:

const Button = styled.button`
    display: inline-flex;
    font-weight: 400;
    justify-content: center;
    align-items: center;
    user-select: none;
    padding: .375rem .75rem;
    font-size: 1rem;
    border-radius: .25rem;
    margin: 5px;

    // 有条件的改变样式
    border: 1px solid ${props => props.primary ? '#007bff' : '#fefefe'};
    background: ${props => props.primary ? '#007bff' : '#fefefe'};
    color: ${props => props.primary ? '#fff' : '#212529'};
`

<Button primary> Primary Button</Button>
<Button>Default Button</Button>

而在CSS Modules中,需要像下面这样来使用:

// Button.css
.button {
    background-color: black;
}

.disabledButton {
    composes: button;
    opacity: .5;
}

// Button.jsx
import styles from './Button.css';

function Button(props) {
    let className = props.isDisabled ? styles.disabledButton : styles.button;
    return (
        <button className={className} />
    )
}

export default Button;

可能还会有其他的痛点我还没有体会到,如果你有这方面的体验和相关经验,欢迎在下面的评论中与我们一起分享。

小结

正如早前《写CSS的姿势》所说:

无论是Sass,BEM,CSS Modules还是Styled-Components,无论使用什么技术。对于一个团队而言,应该根据自己的团队成员去做最适合的选择。

在编写CSS或维护CSS时,建议您先按下图这样的一个流程进行:

就我们这篇文章中提到的CSS-in-JS和CSS Modules而言,选择就会变得更为简单,也用一张图来列出他们对比的结果:

另外,近几年来CSS的变化也非常的大,不管是其新特性还是编写方式,维护手段都有着极大的变化。可以说技术不断的革新,写CSS的姿势也在不断的革新

最后我还想表达的是,没有最好的,只有最合适的!如果真的有最好的,社区也不会有着这么激烈的讨论和各种不同声音的争议了