当前位置 博文首页 > 前端开发博客:React 中 CSS in JS 的最佳实践

    前端开发博客:React 中 CSS in JS 的最佳实践

    作者:[db:作者] 时间:2021-07-03 22:01

    字节跳动ADFE团队

    https://juejin.cn/post/6935245880568053791

    一、传统class的痛点

    随着React、Vue等支持组件化的MVVM前端框架越来越流行,在js中直接编写css的技术方案也越来越被大家所接受。

    为什么前端开发者们更青睐于这些css-in-js的方案呢?我觉得关键原因有以下几点:

    1. css在设计之初对“组件化”的考虑是不完全的,css直接作用于全局,无法直接作用于某个组件内部的样式。

    2. 在我们的前端组件中有很多“组件样式随着数据变化”的场景,但传统css应对这种场景很无力。

    3. 虽然我们可以通过一些规范来规避问题,但是真正用起来太繁琐了,也不利于跨团队的写作。

    比如一个遵循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)」还是非常重要的。

    二、React中编写css的几种方式

    2-1、有规范约束的className

    使用一些命名规范(比如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>
    ??)
    }
    

    这种方式比较适合基础组件库的开发,主要原因是:

    1. 使用class开发的组件库,业务方可以很方便地由组件样式的覆盖。

    2. 基础组件库一般由专门的团队开发,命名规范能统一。

    3. 使用最基础的class,能有效降低组件库的大小。

    2-2、inline styling

    const?App?=?props?=>?{
    ??return?(
    ????<div?style={{color:?"red"}}>123</div>
    ??)
    }
    

    这种方式是JSX语法自带的设置style的方法,会渲染出来内联样式,它有一个好处是可以在style中使用一些全局变量(但实际上,less等css预处理语言也是支持的)。另外,如果你只是要调一下组件的margin,这种写法也是代码量最小的写法。

    2-3、css-loader(CSS Module)

    使用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组件中使用驼峰也是有一定的理解成本的。

    2-4、css-in-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 交叉影响问题。

    三、css-in-js方案比较

    我们选取了 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实现原理简介

    4-1、emotion效果

    首先让我们来看一下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实际上是做了以下三个事情:

    1. 将样式写入模板字符串,并将其作为参数传入css方法。

    2. 根据模板字符串生成class名,并填入组件的class="xxxx"中。

    3. 将生成的class名以及class内容放到<style>标签中,然后放到html文件的head中。

    4-2、emotion初始化

    首先我们可以看到,在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,
    ????//...
    ??}
    }
    

    4-3、emotion 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
    }
    

    4-4、emotion css方法

    这是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
    下一篇:没有了