我正在参加「掘金·启航计划」,这篇文章我主要想跟大家分享一下我理解的 CSS-in-JS。
一、传统CSS的痛点
CSS 从一开始就是 Web 技术的核心之一,而且近年来 CSS 越来越标准化,功能也越来越强。但是 CSS(截止到目前标准化的)尚不具备现代前端组件化开发所需要的部分领域知识和能力,所以需要其他技术来补足。
这些知识和能力主要包括四个方面:
- 组件样式的作用域需要控制在组件级别;
- 组件样式与组件需要在源码和构建层面建立更强的关联;
- 组件样式需要响应组件数据变化;
- 组件样式需要有以组件为单位的复用和扩展能力。
因此在现代前端组件化开发的历程中社区涌现了非常多的解决方案,主要分为以下几种:
1、命名规范
为了能够将组件样式的作用域控制在组件级别我们能想到最简单直接的方案当然是制定 class 类名定义规范,目前业内比较普遍使用的是 BEM 规范,例如
// style.css
.btn {
background-color: white;
}
.btn__icon {
color: black;
}
import './stype.css'
const TButton = props => {
return (
<button className="btn">
<icon className="btn__icon" name={props.incon}></icon>
</button>
)
}
通过命名规范人为保证组件件的样式隔离,这种方式会存在如下问题:
- 非常依赖开发团队的代码规范化
- 组件样式没有办法响应组件数据的变化
- 没有提供非常细粒度的复用能力
因此这种方式在现代前端大型业务应用中使用并不广泛,这种方式比较适合基础组件库的开发,主要原因是:
- 使用class开发的组件库,业务方可以很方便地由组件样式的覆盖
- 基础组件库一般由专门的团队开发,命名规范能统一
- 使用最基础的class,能有效降低组件库的大小
2、Inline styling
无论是 Vue 还是 React,其实都提供了动态 style 样式的不同实现,以 React 的 JSX 的语法为例:
const App = props => {
return (
<div style={{color: "red"}}>123</div>
)
}
相较于命名规范的实现,Inline Styling 提供了非常直接简单的响应组件内状态变化的方案,同时我们也可以通过抽取部分样式为变量实现样式复用或扩展。
但是这种方式如果在央视属性过多的情况下会让代码显得非常混乱,因此 Inline Styling 往往用于元素部分属性调整的情况。
3、CSS Modules
CSS Module 是目前使用的比较多的解决方案,它不是将 CSS 改造成编程语言,而是在 CSS 的基础上通过构建工具对其进行了扩展,针对前面我们提到的 CSS 面对现代前端组件化开发能力的不足给出了自己的解决方案。
3.1、组件级的隔离:局部作用域
CSS Modules 实现局部作用域的核心是使用独一无二的 class,以下面 React 组件为例:
// App.jsx
import React from 'react';
import style from './App.css';
export default () => {
return (
<h1 className={style.title}>
Hello World
</h1>
);
};
/* App.css */
.title {
color: red;
}
构建工具会将类名 style.title
编译成一个哈希字符串。
<h1 class="_3zyde4l1yATCOkgn-DBWEL">
Hello World
</h1>
App.css 也会同时被编译。
._3zyde4l1yATCOkgn-DBWEL {
color: red;
}
这样一来,这个类名就变成独一无二了,只对App组件有效。
大家可以看到 CSS Modules 实现的核心就在构建工具处理的这个阶段,我们以 webpack 为例,使用 webpack 的 css-loader 可以在打包项目的时候指定该样式的 scope,例如
// webpack config
module.exports = {
module: {
loaders: [
{
test: /.css$/,
loader: 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]'
},
]
},
...
}
这里的 css-loader 会将类名 style.title
转变成 title__app__hash
的格式。
3.2、更灵活的限制:全局作用域
CSS Modules 允许使用 :global(.className)
的语法,声明一个全局规则。凡是这样声明的 class,都不会被编译成哈希字符串。举个例子:
/* App.css */
.title {
color: red;
}
:global(.title) {
color: green;
}
// APP.jsx
import React from 'react';
import styles from './App.css';
export default () => {
return (
// 注意这里的类名写法是普通写法
<h1 className="title">
Hello World
</h1>
);
};
3.3、样式的复用:class 的继承
在 CSS Modules 中,一个选择器可以继承另一个选择器的规则,这称为”组合(composition)“。
/* App.css */
.className {
background-color: blue;
}
.title {
composes: className;
color: red;
}
import React from 'react';
import style from './App.css';
export default () => {
return (
<h1 className={style.title}>
Hello World
</h1>
);
};
这种情况下 App.css 文件会被编译成如下内容:
._2DHwuiHWMnKTOYG45T0x34 {
color: red;
}
._10B-buq6_BEOTOl9urIjf8 {
background-color: blue;
}
而 h1 元素会被编译成 <h1 class="_2DHwuiHWMnKTOYG45T0x34 _10B-buq6_BEOTOl9urIjf8">
。
3.4、变量的引入
CSS Modules 支持使用变量,不过需要安装 postcss-loader 和 postcss-modules-values。
// webpack config
const values = require('postcss-modules-values');
module.exports = {
module: {
loaders: [
{
test: /.css$/,
loader: 'css-loader?modules!postcss-loader
},
]
},
postcss: [
values
]
...
}
接着,在 colors.css 里面定义变量。
@value blue: #0c77f8;
@value red: #ff0000;
@value green: #aaf200;
App.css 可以引用这些变量。
3.5 总结
CSS Modules 的这种做法非常类似 Angular 与 Vue 对样式的封装方案,其核心是以 CSS 文件模块为单元,将模块内的选择器附上特殊的哈希字符串,以实现样式的局部作用域。在 React 项目中我们通过引入相关打包工具配置同样可以实现相同的效果,对于大部分应用开发场景已经可以完全支持。
二、CSS-in-JS
CSS-in-JS 顾名思义是在 JS 中直接编写 CSS 的技术,也是 React 官方推荐的编写 CSS 的方案,在 github.com/MicheleBert… 这个代码仓库中我们可以看到 CSS-in-JS 相关的 package 已经有60多个了。这里我们主要介绍 emotion ,这个框架比起其他框架更注重开发者体验(Developer Experience),功能相对完整,也比其他一些专注于用 JS、TS 语法写样式的框架更“CSS”一些。
1、基本使用
import { css } from '@emotion/react'
const color = 'white'
render(
<div
css={css`
padding: 32px;
background-color: hotpink;
font-size: 24px;
border-radius: 4px;
&:hover {
color: ${color};
}
`}
>
Hover to change color.
</div>
)
可以看到 emotion 对于组件内样式隔离、数据变量都有着比较好的支持,而且对于伪类选择器等 CSS 属性也有着比较好的支持,除此之外,比较常用还有子元素选择器:
import { css } from '@emotion/react'
render(
<div
css={css`
padding: 32px;
background-color: hotpink;
& > span {
background-color: blue;
}
`}
>
Hover to change color.
</div>
)
子选择器 >
对于 KanbanColumn 组件是必要的。如果去掉 >
,仅保留空格,上面三个子选择器就变成了后代选择器,无论在 DOM 树中的深度如何,只要是子孙元素中的 span 元素就会被应用上面的样式,这就会污染传入的 children 子组件的样式,偏离了我们样式隔离的目标。
2、样式的组合与复用
最简单直接的样式复用方式当然是声明一个值为 css 函数执行结果的常量,然后可以在不同组件中赋给 HTML 元素的 css 属性:
import { css } from '@emotion/react'
const commonStyles = css`
padding: 32px;
background-color: hotpink;
& > span {
background-color: blue;
}
`}
render(
<div
css={commonStyles}
>
Hover to change color.
</div>
)
在此基础上我们也可以选择更加灵活的样式组合:
import { css } from '@emotion/react'
const commonStyles = css`
padding: 32px;
background-color: hotpink;
& > span {
background-color: blue;
}
`}
const color = 'white'
render(
<div
css={
css`
${commonStyles}
&:hover {
color: ${color};
}
`
}
>
Hover to change color.
</div>
)
除此之外如果要组合两个或更多 css 函数返回值的变量,还可以用数组的写法,如果其中有重复的 CSS 属性(如 color: red
和 color: blue
),那么后面的会覆盖前面的:
<div css={[style1, style2, style3]}>...</div>
3、基本原理
为了说明 emotion 在背后做了一些什么我们先用 emotion 写一个基本组件:
import React, { useState } from 'react'
import { css } from '@emotion/react'
const KanbanBoard = ({ children }) => (
<main css={css`
flex: 10;
display: flex;
flex-direction: row;
gap: 1rem;
margin: 0 1rem 1rem;
`}>{children}</main>
)
把开发者工具切换到检查器页签,可以看到标签的 class 属性值变成了一个貌似没有意义的类名 css-130tiw0-KanbanBoard
,而这个 CSS 类是在 HTML 文档的里动态插入的
类名中的 130tiw0
是个哈希值,用来保证类名在不同组件间的唯一性,这自然就避免了一个组件的样式污染另一个组件。你不妨将类样式代码格式化,会得到如下片段:
.css-130tiw0-KanbanBoard {
-webkit-flex: 10;
-ms-flex: 10;
flex: 10;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
gap: 1rem;
margin: 0 1rem 1rem;
}
貌似比一开始手写的代码增加了几行?是的,增加的这几行中,-webkit-、 -ms- 这样的前缀称作Vendor Prefix 浏览器引擎前缀,浏览器厂商用这种方式来引入尚未标准化的、实验性的 CSS 属性或属性值。为了提高浏览器兼容性,emotion 框架会自动为较新的 CSS 标准加入带有前缀的副本,不认识这些前缀的浏览器会忽略这些副本,而老版本浏览器会各取所需,这样只需按最新标准编写一次 CSS,就可以自动支持新老浏览器。
这儿 emotion 实际上是做了以下三个事情:
- 将样式写入模板字符串,并将其作为参数传入 css 方法。
- 根据模板字符串生成 class 名,并填入组件的
class="xxxx"
中。 - 将生成的 class 名以及 class 内容放到 style 标签中,然后放到 html 文件的 head 中。