当前位置 博文首页 > 前端开发博客:React 中 CSS in JS 的最佳实践
字节跳动ADFE团队
https://juejin.cn/post/6935245880568053791
随着React、Vue等支持组件化的MVVM前端框架越来越流行,在js中直接编写css的技术方案也越来越被大家所接受。
为什么前端开发者们更青睐于这些css-in-js的方案呢?我觉得关键原因有以下几点:
css在设计之初对“组件化”的考虑是不完全的,css直接作用于全局,无法直接作用于某个组件内部的样式。
在我们的前端组件中有很多“组件样式随着数据变化”的场景,但传统css应对这种场景很无力。
虽然我们可以通过一些规范来规避问题,但是真正用起来太繁琐了,也不利于跨团队的写作。
比如一个遵循BEM规范的表单组件要写成这个样子:
<style>
????.form?{?}
????.form--theme-xmas?{?}
????.form--simple?{?}
????.form__input?{?}
????.form__submit?{?}
????.form__submit--disabled?{?}
</style>
<form?class="form?form--theme-xmas?form--simple">
??<input?class="form__input"?type="text"?/>
??<input?class="form__submit?form__submit--disabled"?type="submit"?/>
</form>
实在是太繁琐了!如果这是一段业务代码(注意,是业务代码),那团队中的其他人去读这段代码的时候内心一定是比较崩溃的。当然,如果是维护基础组件的话,遵守BEM规范「块(block)、元素(element)、修饰符(modifier)」还是非常重要的。
使用一些命名规范(比如BEM规范)来约束className,比如下面这种:
//?style.css
.form?{
??background-color:?white;
}
.form__input?{
??color:?black;
}
?
import?'./stype.css'
const?App?=?props?=>?{
??return?(
????<form?className="form">
??????<input?class="form__input"?type="text"?/>
????</form>
??)
}
这种方式比较适合基础组件库的开发,主要原因是:
使用class开发的组件库,业务方可以很方便地由组件样式的覆盖。
基础组件库一般由专门的团队开发,命名规范能统一。
使用最基础的class,能有效降低组件库的大小。
const?App?=?props?=>?{
??return?(
????<div?style={{color:?"red"}}>123</div>
??)
}
这种方式是JSX语法自带的设置style的方法,会渲染出来内联样式,它有一个好处是可以在style中使用一些全局变量(但实际上,less等css预处理语言也是支持的)。另外,如果你只是要调一下组件的margin,这种写法也是代码量最小的写法。
使用webpack的css-loader可以在打包项目的时候指定该样式的scope,比如我们可以这样打包:
//?webpack?config
module.exports?=?{
??module:?{
????loaders:?[
??????{?
????????test:?/\.css$/,?
????????loader:?'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]'?
??????},
????]
??},
??...
?}
//?App.css
.app?{
????background-color:?red;
}
.form-item{
??color:?red;
}
import?styles?from?'./App.css';
const?App?=?props?=>?{
??return?(
????<div?className={style.app}>123</div>
????<div?className={style['form-item']}>456</div>
??)
}
这样.app
就会被编译为.App__app___hash
这样的格式了。这种方式是借助webpack实现将组件内的css只作用于组件内样式,相比于直接写inline styling也是一个不错的解决方案。
但使用style['form-item']
这种形式去className的值(并且我们单独编写css文件时一般也都会使用“-
”这个符号),我觉得不少开发者会觉得很尴尬……
另外虽然webpack支持“-
”和驼峰互相转换,但是在实际开发中,如果面对一个样式比较多的组件,在css文件中使用“-
”然后在js组件中使用驼峰也是有一定的理解成本的。
顾名思义,css-in-js是在js中直接编写css的技术,也是react官方推荐的编写css的方案,在 https://github.com/MicheleBertoli/css-in-js 这个代码仓库中我们可以看到css-in-js相关的package已经有60多个了。
下面以emotion为例,介绍一下css-in-js的方案:
import?{?css,?jsx?}?from?'@emotion/core'
const?color?=?'white'
//?下面这种写法是带标签的模板字符串
//?该表达式通常是一个函数,它会在模板字符串处理后被调用,在输出最终结果前
//?我们可以通过该函数来对模板字符串进行操作处理
//?详细链接?——?https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
const?App?=?props?=>?{
??return?(
??<div
????className={css`
??????padding:?32px;
??????background-color:?hotpink;
??????font-size:?24px;
??????border-radius:?4px;
????`}
??>
????This?is?test.
??</div>
??)
}
在开发业务代码的时候,由于维护人员较多且不固定,且代码规模会逐渐增大,不能保证 css 不会交叉影响,所以我们不能只通过规范来约束,而是要通过 css-in-js 这样的方案来解决 css 交叉影响问题。
我们选取了 https://github.com/MicheleBertoli/css-in-js 仓库中支持功能全面且月下载量较多的几个css-in-js方案进行一下比较(其实它们在使用的时候都差距不大,主要是实现原理以及支持的特性有一些不太一样)
从体积来看:emotion的体积是最小的。
从技术生态环境(以及流行程度):styled-components的star最多,文档相对来讲也是最完善的。
从支持的特性来看:emotion、aphrodite、jss支持的特性是最多的。
所以新人可以尝试接触styled-components,综合来看emotion是一个相当不错的选择。
我们团队其实很早就开始使用React + emotion进行前端开发了。当时选择emotion主要的考虑就是它拥有最全面的功能,以及在当时的css-in-js方案中相对最小的体积。
而且emotion是为数不多的支持source-map的css-in-js框架之一。
首先让我们来看一下emotion做了什么,这是一个使用了emotion的React组件:
import?React?from?'react';
import?{?css?}?from?'emotion'
const?color?=?'white'
function?App()?{
??return?(
????<div?className={css`
??????padding:?32px;
??????background-color:?hotpink;
??????font-size:?24px;
??????border-radius:?4px;
??????&:hover?{
????????color:?${color};
??????}
????`}>
??????This?is?emotion?test
????</div>
??);
}
export?default?App;
这是渲染出的html:
<html?lang="en">
??<head>
????<title>React?App</title>
????<style?data-emotion="css">
??????.css-czz5zq?{
????????padding:?32px;
????????background-color:?hotpink;
????????font-size:?24px;
????????border-radius:?4px;
??????}
????</style>
????<style?data-emotion="css">
??????.css-czz5zq:hover?{
????????color:?white;
??????}
????</style>
??</head>
??<body>
????<div?id="root">
??????<div?class="css-czz5zq">This?is?React.js?test</div>
????</div>
??</body>
</html>
我们可以看到emotion实际上是做了以下三个事情:
将样式写入模板字符串,并将其作为参数传入css
方法。
根据模板字符串生成class名,并填入组件的class="xxxx"
中。
将生成的class名以及class内容放到<style>
标签中,然后放到html文件的head中。
首先我们可以看到,在emotion实例化的时候(也就是我们在组件中import { css } from 'emotion'
的时候),首先调用了create-emotion
包中的createEmotion
方法,这个方法的主要作用是初始化emotion的cache(用于生成样式并将生成的样式放入<head>
中,后面会有详细介绍),以及初始化一些常用的方法,其中就有我们最常使用的css
方法。
import?createEmotion?from?'create-emotion'
export?const?{
??injectGlobal,
??keyframes,
??css,
??cache,
??//...
}?=?createEmotion()
```
```ts
let?createEmotion?=?(options:?*):?Emotion?=>?{
??//?生成emotion?cache
??let?cache?=?createCache(options)
??//?用于普通css
??let?css?=?(...args)?=>?{
????let?serialized?=?serializeStyles(args,?cache.registered,?undefined)
????insertStyles(cache,?serialized,?false)
????return?`${cache.key}-${serialized.name}`
??}
??//?用于css?animation
??let?keyframes?=?(...args)?=>?{
????let?serialized?=?serializeStyles(args,?cache.registered)
????let?animation?=?`animation-${serialized.name}`
????insertWithoutScoping(cache,?{
??????name:?serialized.name,
??????styles:?`@keyframes?${animation}{${serialized.styles}}`
????})
????return?animation
??}
??
??//?注册全局变量
??let?injectGlobal?=?(...args)?=>?{
????let?serialized?=?serializeStyles(args,?cache.registered)
????insertWithoutScoping(cache,?serialized)
??}
??return?{
????css,
????injectGlobal,
????keyframes,
????cache,
????//...
??}
}
emotion的cache用于缓存已经注册的样式,也就是已经放入head中的样式。在生成cache的时候,使用一款名为Stylis的CSS预编译器对我们传入的序列化的样式进行编译,同时它还生成了插入样式方法(insert)。
let?createCache?=?(options?:?Options):?EmotionCache?=>?{
??if?(options?===?undefined)?options?=?{}
??let?key?=?options.key?||?'css'
??let?stylisOptions
??if?(options.prefix?!==?undefined)?{
????stylisOptions?=?{
??????prefix:?options.prefix
????}
??}
??let?stylis?=?new?Stylis(stylisOptions)
??let?inserted?=?{}
??let?container:?HTMLElement
??if?(isBrowser)?{
????container?=?options.container?||?document.head
??}
??let?insert:?(
????selector:?string,
????serialized:?SerializedStyles,
????sheet:?StyleSheet,
????shouldCache:?boolean
??)?=>?string?|?void
??if?(isBrowser)?{
????stylis.use(options.stylisPlugins)(ruleSheet)
????insert?=?(
??????selector:?string,
??????serialized:?SerializedStyles,
??????sheet:?StyleSheet,
??????shouldCache:?boolean
????):?void?=>?{
??????let?name?=?serialized.name
??????Sheet.current?=?sheet
??????stylis(selector,?serialized.styles)??//?该方法会在对应的selector中添加对应的styles
??????if?(shouldCache)?{
????????cache.inserted[name]?=?true
??????}
????}
??}
??const?cache:?EmotionCache?=?{
????key,
????sheet:?new?StyleSheet({
??????key,
??????container,
??????nonce:?options.nonce,
??????speedy:?options.speedy
????}),
????nonce:?options.nonce,
????inserted,
????registered:?{},
????insert
??}
??return?cache
}
这是emotion中比较重要的方法,它其实是调用了serializeStyles
方法来处理css
方法中的参数,然后使用insertStyles
方法将其插入html文件中,最后返回class名,然后我们在组件中使用<div className={css('xxxxx')}></div>
的时候就能正确指向对应的样式了。
let?css?=?(...args)?=>?{
????let?serialized?=?serializeStyles(args,?cache.registered,?undefined)
????insertStyles(cache,?serialized,?false)
????return?`${cache.key}-${serialized.name}`
}
serializeStyles方法是一个比较复杂的方法,它的主要作用是处理css方法中传入的参数,生成序列化的class。
export?const?serializeStyles?=?function(
??args:?Array<Interpolation>,
??registered:?RegisteredCache?|?void,
??mergedProps:?void?|?Object
):?SerializedStyles?{
??//?如果只传入一个参数,那么直接返回
??if?(
????args.length?===?1?&&
????typeof?args[0]?===?'object'?&&
????args[0]?!==?null?&&
????args[0].styles?!==?undefined
??)?{
????return?args[0]
??}
??
??//?如果传入多个参数,那么就需要merge这些样式
??let?stringMode?=?true
??let?styles?=?''
??let?strings?=?args[0]
??if?(strings?==?null?||?strings.raw?===?undefined)?{
????stringMode?=?false
????styles?+=?handleInterpolation(mergedProps,?registered,?strings,?false)
??}?else?{
????styles?+=?strings[0]
??}
??//?we?start?at?1?since?we've?already?handled?the?first?arg
??for?(let?i?=?1;?i?<?args.length;?i++)?{
????styles?+=?handleInterpolation(
??????mergedProps,
??????registered,
??????args[i],
??????styles.charCodeAt(styles.length?-?1)?===?46
????)
????if?(stringMode)?{
??????styles?+=?strings[i]
????}
??}
??//?using?a?global?regex?with?.exec?is?stateful?so?lastIndex?has?to?be?reset?each?time
??labelPattern.lastIndex?=?0
??let?identifierName?=?''
??let?match
??while?((match?=?labelPattern.exec(styles))?!==?null)?{
????identifierName?+=
??????'-'?+
??????match[1]
??}
??let?name?=?hashString(styles)?+?identifierName
??return?{
????name,
????styles
??}
}
//?生成对应的样式
function?handleInterpolation(
??mergedProps:?void?|?Object,
??registered:?RegisteredCache?|?void,
??interpolation:?Interpolation,
??couldBeSelectorInterpolation:?boolean
):?string?|?number?{
?//?...
}
insertStyles方法其实比较简单,首先读取cache中是否insert了这个style,如果没有,则调用cache中的insert方法,将样式插入到head中。
export?const?insertStyles?=?(
??cache:?EmotionCache,
??serialized:?SerializedStyles,
??isStringTag:?boolean
)?=>?{
??let?className?=?`${cache.key}-${serialized.name}`
??if?(cache.inserted[serialized.name]?===?undefined)?{
????let?current?=?serialized
????do?{
??????let?maybeStyles?=?cache.insert(
????????`.${className}`,
????????current,
????????cache.sheet,
????????true
??????)
??????current?=?current.next
????}?while?(current?!==?undefined)
??}
}
总体来说,如果是进行基础组件的开发,那么使用“有规范约束”的原生css(比如遵守BEM规范的css),或者less之类的预处理语言会比较合适,这能最大幅度地减小组件库的体积,也能为业务方提供样式覆盖的能力。
如果是进行业务开发,个人比较推荐css-in-js的方案,因为它不仅能够做到在组件中直接编写css,同时也能够直接使用组件中的js变量,能有效解决“组件样式随着数据变化”的问题。另外,在业务开发中,由于迭代速度快,开发人员流动性相对大一些,我们直接使用规范对css进行约束会有一定的风险,当项目规模逐渐变大后代码的可读性会很差,也会出现css互相影响的情况。
另外使用CSS Module在业务开发中也是一种不错的方案,但一般大部分前端开发者会使用“-
”来为样式命名,但放到组件中就只能使用style['form-item']
这样的方式去引用了,我个人是不太喜欢这种风格的写法的。
不过没有一项技术是能解决全部问题的,针对不同的场景选择最合适的技术才是最优解。如果团队中还未使用这些技术,并且在开发组件的样式时遇到了文中所述的“传统css在组件开发中的痛点”,那么我建议去尝试一下css-in-js或者css module,具体选择何种方案就看团队成员更能接受哪种写法了。如果已经使用了css-in-js或者css module,那么继续使用即可,这些都是能够cover现有组件开发的场景的。
关于React的更多技巧,推荐你学习这篇:6个React Hook最佳实践技巧
目前主流的前端框架,像vue和angular都针对css的作用域进行了额外的处理,比如vue的scoped,而react这里则是将css作用域处理完全交给了社区,也就出现了各种各样的css-in-js框架,虽说这两种做法其实没什么高下之分,但就个人观感来看,用来用去还是觉得vue sfc中的scoped最香(滑稽)
<style?scoped>
.example?{
??color:?red;
}
</style>
<template>
??<div?class="example">hi</div>
</template>
- EOF -
推荐阅读??点击标题可跳转
从业务讲React Hook
6个React Hook最佳实践技巧
45个有用的JavaScript技巧,窍门和最佳实践
觉得本文对你有帮助?请分享给更多人
推荐关注「前端开发博客」,提升前端技能
如果觉得这篇文章还不错,来个【分享、点赞、在看】三连吧,让更多的人也看到~
“在看转发”是最大的支持
cs