React css-in-js

Posted 字节跳动ADFE团队

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React css-in-js相关的知识,希望对你有一定的参考价值。

1

 传统class的痛点


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


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

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

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

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

比如一个遵循BEM规范的表单组件要写成这个样子:


1  <style>

2      .form { }

3      .form--theme-xmas { }

4      .form--simple { }

5      .form__input { }

6      .form__submit { }

7      .form__submit--disabled { }

8  </style>

9  <form class="form form--theme-xmas form--simple">

10   <input type="text" />

11   <input class="form__submit form__submit--disabled" type="submit" />

12 </form>


实在是太繁琐了!如果这是一段业务代码(注意,是业务代码),那团队中的其他人去读这段代码的时候内心一定是比较崩溃的。当然,如果是维护基础组件的话,遵守BEM规范「块(block)、元素(element)、修饰符(modifier)」还是非常重要的。


2

React中编写css的几种方式


2-1、有规范约束的className


使用一些命名规范(比如BEM规范)来约束className,比如下面这种:


1  // style.css

2  .form {

3    background-color: white;

4  }

5  .form__input {

6    color: black;

7  }

8

9  import './stype.css'

10 const App = props => {

11   return (

12     <form className="form">

13       <input type="text" />

14     </form>

15   )

16 }

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

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

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

2-2、inline styling

React css-in-js


1  const App = props => {

2    return (

3      <div style={{color: "red"}}>123</div>

4    )

5  }


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


2-3、css-loader(CSS Module)

React css-in-js


使用webpack的css-loader可以在打包项目的时候指定该样式的scope,比如我们可以这样打包:


1  // webpack config

2  module.exports = {

3    module: {

4      loaders: [

5        {

6          test: /\.css$/,

7          loader: 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]'

8        },

9      ]

10   },

11   ...

12  }


1  // App.css

2  .app {

3      background-color: red;

4  }

5  .form-item{

6    color: red;

7  }


1  import styles from './App.css';

2  const App = props => {

3    return (

4      <div className={style.app}>123</div>

5      <div className={style['form-6  item']}>456</div>

6    )

7  }

 

这样.app就会被编译为.App__app___hash这样的格式了。这种方式是借助webpack实现将组件内的css只作用于组件内样式,相比于直接写inline styling也是一个不错的解决方案。


但使用style['form-item']这种形式去className的值(并且我们单独编写css文件时一般也都会使用“-”这个符号),我觉得不少开发者会觉得很尴尬……


另外虽然webpack支持“-”和驼峰互相转换,但是在实际开发中,如果面对一个样式比较多的组件,在css文件中使用“-”然后在js组件中使用驼峰也是有一定的理解成本的。


2-4、css-in-js

React css-in-js


顾名思义,css-in-js是在js中直接编写css的技术,也是react官方推荐的编写css的方案,在github.com/MicheleBert… 这个代码仓库中我们可以看到css-in-js相关的package已经有60多个了。


下面以emotion为例,介绍一下css-in-js的方案:


1  import { css, jsx } from '@emotion/core'

2  const color = 'white'

3  // 下面这种写法是带标签的模板字符串

4  // 该表达式通常是一个函数,它会在模板字符串处理后被调用,在输出最终结果前

5  // 我们可以通过该函数来对模板字符串进行操作处理

6  // 详细链接 —— https://developer.mozilla.org/en-US/docs/Web/javascript/Reference/Template_literals

7  const App = props => {

8    return (

9    <div

10     className={css`

11       padding: 32px;

12       background-color: hotpink;

13       font-size: 24px;

14       border-radius: 4px;

15     `}

16   >

17     This is test.

18   </div>

19   )

20 }


在开发业务代码的时候,由于维护人员较多且不固定,且代码规模会逐渐增大,不能保证 css 不会交叉影响,所以我们不能只通过规范来约束,而是要通过 css-in-js 这样的方案来解决 css 交叉影响问题。

3

css-in-js方案比较


我们选取了 github.com/MicheleBert… 仓库中支持功能全面且月下载量较多的几个css-in-js方案进行一下比较(其实它们在使用的时候都差距不大,主要是实现原理以及支持的特性有一些不太一样)


package

star

gzip size

feature

styled-components

17306

12.5kB

Automatic Vendor Prefixing、Pseudo Classes、Media Queries

emotion

4101

5.92kB (core)

Automatic Vendor Prefixing、Pseudo Classes、Media Queries、Styles As Object Literals、Extract CSS File

radium

6372

23.3kB

Automatic Vendor Prefixing、Pseudo Classes、Media Queries、Styles As Object Literals

aphrodite

4175

7.23kB

Automatic Vendor Prefixing、Pseudo Classes、Media Queries、Styles As Object Literals、Extract CSS File

jss

5900

6.73kB

Automatic Vendor Prefixing、Pseudo Classes、Media Queries、Styles As Object Literals、Extract CSS File


从体积来看:emotion的体积是最小的。


从技术生态环境(以及流行程度):styled-components的star最多,文档相对来讲也是最完善的。


从支持的特性来看:emotion、aphrodite、jss支持的特性是最多的。


所以新人可以尝试接触styled-components,综合来看emotion是一个相当不错的选择。


我们团队其实很早就开始使用React + emotion进行前端开发了。当时选择emotion主要的考虑就是它拥有最全面的功能,以及在当时的css-in-js方案中相对最小的体积。


而且emotion是为数不多的支持source-map的css-in-js框架之一。

4

 emotion实现原理简介


4-1、emotion效果

React css-in-js


首先让我们来看一下emotion做了什么,这是一个使用了emotion的React组件:


1  import React from 'react';

2  import { css } from 'emotion'

3  const color = 'white'

4  function App() {

5    return (

6      <div className={css`

7        padding: 32px;

8        background-color: hotpink;

9        font-size: 24px;

10       border-radius: 4px;

11       &:hover {

12         color: ${color};

13       }

14     `}>

15       This is emotion test

16     </div>

17   );

18 }

19 export default App;


这是渲染出的html


1  <html>

2    <head>

3      <title>React App</title>

4      <style data-emotion="css">

5        .css-czz5zq {

6          padding: 32px;

7          background-color: hotpink;

8          font-size: 24px;

9          border-radius: 4px;

10      }

11    </style>

12    <style data-emotion="css">

13      .css-czz5zq:hover {

14        color: white;

15      }

16    </style>

17  </head>

18  <body>

19    <div id="root">

20      <div>This is React.js test</div>

21    </div>

22   </body>

23 </html>


我们可以看到emotion实际上是做了以下三个事情:

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

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

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

4-2、emotion初始化

React css-in-js


首先我们可以看到,在emotion实例化的时候(也就是我们在组件中import { css } from 'emotion'的时候),首先调用了create-emotion包中的createEmotion方法,这个方法的主要作用是初始化emotion的cache(用于生成样式并将生成的样式放入<head>中,后面会有详细介绍),以及初始化一些常用的方法,其中就有我们最常使用的css方法。


1  import createEmotion from 'create-emotion'

2  export const {

3    injectGlobal,

4    keyframes,

5    css,

6    cache,

7    //...

8  } = createEmotion()

9  ```

10 ```ts

11 let createEmotion = (options: *): Emotion => {

12   // 生成emotion cache

13   let cache = createCache(options)

14   // 用于普通css

15   let css = (...args) => {

16     let serialized = serializeStyles(args, cache.registered, undefined)

17     insertStyles(cache, serialized, false)

18     return `${cache.key}-${serialized.name}`

19   }

20   // 用于css animation

21   let keyframes = (...args) => {

22     let serialized = serializeStyles(args, cache.registered)

23     let animation = `animation-${serialized.name}`

24     insertWithoutScoping(cache, {

25       name: serialized.name,

26       styles: `@keyframes ${animation}{${serialized.styles}}`

27     })

28     return animation

29   }

30  

31   // 注册全局变量

32   let injectGlobal = (...args) => {

33     let serialized = serializeStyles(args, cache.registered)

34     insertWithoutScoping(cache, serialized)

35   }

36   return {

37     css,

38     injectGlobal,

39     keyframes,

40     cache,

41     //...

42   }

43 }


4-3、emotion cache

React css-in-js


emotion的cache用于缓存已经注册的样式,也就是已经放入head中的样式。在生成cache的时候,使用一款名为Stylis的CSS预编译器对我们传入的序列化的样式进行编译,同时它还生成了插入样式方法(insert)。


1  let createCache = (options?: Options): EmotionCache => {

2    if (options === undefined) options = {}

3    let key = options.key || 'css'

4    let stylisOptions

5    if (options.prefix !== undefined) {

6      stylisOptions = {

7        prefix: options.prefix

8      }

9    }

10   let stylis = new Stylis(stylisOptions)

11   let inserted = {}

12   let container: HTMLElement

13   if (isBrowser) {

14     container = options.container || document.head

15   }

16   let insert: (

17     selector: string,

18     serialized: SerializedStyles,

19     sheet: StyleSheet,

20     shouldCache: boolean

21   ) => string | void

22   if (isBrowser) {

23     stylis.use(options.stylisPlugins)(ruleSheet)

24     insert = (

25       selector: string,

26       serialized: SerializedStyles,

27       sheet: StyleSheet,

28       shouldCache: boolean

29     ): void => {

30       let name = serialized.name

31       Sheet.current = sheet

32       stylis(selector, serialized.styles)  // 该方法会在对应的selector中添加对应的styles

33       if (shouldCache) {

34         cache.inserted[name] = true

35       }

36     }

37   }

38   const cache: EmotionCache = {

39     key,

40     sheet: new StyleSheet({

41       key,

42       container,

43       nonce: options.nonce,

44       speedy: options.speedy

45     }),

46     nonce: options.nonce,

47     inserted,

48     registered: {},

49     insert

50   }

51   return cache

52 }


4-4、emotion css方法


这是emotion中比较重要的方法,它其实是调用了serializeStyles方法来处理css方法中的参数,然后使用insertStyles方法将其插入html文件中,最后返回class名,然后我们在组件中使用<div className={css('xxxxx')}></div>的时候就能正确指向对应的样式了。


1  let css = (...args) => {

2      let serialized = serializeStyles(args, cache.registered, undefined)

3      insertStyles(cache, serialized, false)

4      return `${cache.key}-${serialized.name}`

5  }


serializeStyles方法是一个比较复杂的方法,它的主要作用是处理css方法中传入的参数,生成序列化的class。


1  export const serializeStyles = function(

2    args: Array<Interpolation>,

3    registered: RegisteredCache | void,

4    mergedProps: void | Object

5  ): SerializedStyles {

6    // 如果只传入一个参数,那么直接返回

7    if (

8      args.length === 1 &&

9      typeof args[0] === 'object' &&

10     args[0] !== null &&

11     args[0].styles !== undefined

12   ) {

13     return args[0]

14   }

15 

16   // 如果传入多个参数,那么就需要merge这些样式

17   let stringMode = true

18   let styles = ''

19   let strings = args[0]

20   if (strings == null || strings.raw === undefined) {

21     stringMode = false

22     styles += handleInterpolation(mergedProps, registered, strings, false)

23   } else {

24     styles += strings[0]

25   }

26   // we start at 1 since we've already handled the first arg

27   for (let i = 1; i < args.length; i++) {

28     styles += handleInterpolation(

29       mergedProps,

30       registered,

31       args[i],

32       styles.charCodeAt(styles.length - 1) === 46

33     )

34     if (stringMode) {

35       styles += strings[i]

36     }

37   }

38  // using a global regex with .exec is stateful so lastIndex has to be reset each time

39   labelPattern.lastIndex = 0

40   let identifierName = ''

41   let match

42   while ((match = labelPattern.exec(styles)) !== null) {

43     identifierName +=

44       '-' +

45       match[1]

46   }

47   let name = hashString(styles) + identifierName

48   return {

49     name,

50     styles

51   }

52 }

53 // 生成对应的样式

54 function handleInterpolation(

55   mergedProps: void | Object,

56   registered: RegisteredCache | void,

57   interpolation: Interpolation,

58   couldBeSelectorInterpolation: boolean

59 ): string | number {

60  // ...

61 }

62


insertStyles方法其实比较简单,首先读取cache中是否insert了这个style,如果没有,则调用cache中的insert方法,将样式插入到head中。


1  export const insertStyles = (

2    cache: EmotionCache,

3    serialized: SerializedStyles,

4    isStringTag: boolean

5  ) => {

6    let className = `${cache.key}-${serialized.name}`

7    if (cache.inserted[serialized.name] === undefined) {

8      let current = serialized

9      do {

10       let maybeStyles = cache.insert(

11         `.${className}`,

12         current,

13         cache.sheet,

14         true

15       )

16       current = current.next

17     } while (current !== undefined)

18   }

19 }


5

 一些总结


总体来说,如果是进行基础组件的开发,那么使用“有规范约束”的原生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现有组件开发的场景的。


6

 写在最后

目前主流的前端框架,像vue和angular都针对css的作用域进行了额外的处理,比如vue的scoped,而react这里则是将css作用域处理完全交给了社区,也就出现了各种各样的css-in-js框架,虽说这两种做法其实没什么高下之分,但就个人观感来看,用来用去还是觉得vue sfc中的scoped最香(滑稽)

1  <style scoped>

2  .example {

3    color: red;

4  }

5  </style>

6  <template>

7    <div>hi</div>

8  </template>



扫二维码


以上是关于React css-in-js的主要内容,如果未能解决你的问题,请参考以下文章

「首席架构师推荐」React生态系统大集合

import * as react from 'react' 与 import react from 'react' 有啥区别

“使用 JSX 时,React 必须在范围内”(react/react-in-jsx-scope 与 index.js 上的“window.React = React”)

React 系列教程

React学习笔记-1-什么是react,react环境搭建以及第一个react实例

react 导入中的 as