css-in-js-under-the-hood

July 12, 2021

css-in-js是一个 css方案,在react中使用者非常多。喜欢他的人对其称赞不已,讨厌他的人对其嗤之以鼻。关于css-in-js这个技术方案的历史发展和优缺点在前文 已经有分析,本文不做过多讲解,本文将以styled-components探讨一下css-in-js 实现的原理。

用法

style-components官网 可以看到用法

const Button = styled.a`
  /* This renders the buttons above... Edit me! */
  display: inline-block;
  border-radius: 3px;
  padding: 0.5rem 0;
  margin: 0.5rem 1rem;
  width: 11rem;
  background: transparent;
  color: white;
  border: 2px solid white;

  /* The GitHub button is a primary button
   * edit this to target it specifically! */
  ${props => props.primary && css`
    background: white;
    color: black;
  `}
`

render(
  <div>
    <Button
      href="https://github.com/styled-components/styled-components"
      target="_blank"
      rel="noopener"
      primary
    >
      GitHub
    </Button>

    <Button as={Link} href="/docs">
      Documentation
    </Button>
  </div>
)

可以看到,通过调用 styled.a返回的就是一个组件。后面的参数是用 “ 符号包裹的。这是 [tagged templates](Tagged templates)。其实就相当于函数调用。

基础

将函数参数解析为style字符串

通过MDN实例,可以看出,函数的第一个参数是一个数组,是整个参数被${} 分割的字符串。因此可以将函数这样定义。

function evaluate(str, ...expressions) {
  return expressions.reduce((acc, exp, index) => {
    acc += `${exp}${str[index + 1]}`;
    return acc;
  }, `${str[0]}`);
}

我们来试验一下

evaluate`Good ${ Math.random() > 0.5 ? 'afternoon' : 'morning'}` // 随机返回 'Good morning'或者'Good afternoon'

上面这个例子中,evaluate 可以接受动态js 语句。在 style-component中 表达式 也可以是一个一个函数,函数是参数从哪里来呢?其实要知道 style-component返回的是一个组件,在React中,一个接收props返回 vdom的 函数就可以是一个组件。因此,我们将上面的evaluate函数进行一些改写

function evaluate(str, ...expressions) {
  return props => {
    return expressions.reduce((acc, exp, index) => {
      acc += `${evaluateExp(exp, props)}${str[index + 1]}`;
      return acc;
    }, `${str[0]}`);
  };
}

function evaluateExp(exp, props) {
  if (typeof exp === 'function') {
    return exp(props);
  }
  return exp;
}

返回一个组件

在这个实现中,返回值是一个函数,但是这个函数只是返回了一个拼接字符串,并没有渲染任何内容。

让我们再次回到styled-component的使用例子,我们一般都是

const RedLink = styled.a`
	color: red
`

渲染的实际内容是一个 红色的 a标签, 因此我们可以对上文的函数进行改写

import { createElement } from 'react'

const styled = (target) => (strs, ...expressions) => (props) => {
   const cssText = expressions.reduce((acc, exp, index) => {
      acc += `${evaluateExp(exp, props)}${str[index + 1]}`;
      return acc;
    }, `${str[0]}`);
  };

	return createElement(target, {
    style:  cssText
  })
}

function evaluateExp(exp, props) {
  if (typeof exp === 'function') {
    return exp(props);
  }
  return exp;
}

const allElements = [
  'a',
  'h1',
  'h2',
  'p',
  'div',
  // ...
]

allElements.forEach(ele => {
  styled[ele] = styled(ele)
})

到这里为止,我们已经实现了了一个最简陋,最基础的styled-component实现了。接下来我们要对一些细节进行处理

补充一些细节

使用 css dom替换行内css

上面的代码中,我们将生成的style以行内style的形式注入。这种方式不利于样式的复用。我们可以考虑根据这些css 生成一个 className,然后将这一条规则插入到style标签中。

如何根据css生成一个className呢?我们希望相同的css可以生成一样的hash,这样就可以复用了了。可以使用MurmurHash来实现,他是一个高效的hash算法,同样的输入,必定有同样的输出,很符合我们的要求。但是…等等,cssText是由用户输入的,我们只做了拼接。cssText 是由一条一条规则组成的,有时候用户输入的不一样,但是其实是一样的,比如

styled.a`
	color: red;
	font-size: 24;
`

styled.a`
	
`

…未完待续


Profile picture

Written by Colgin who lives and works in China, focus on web development. You can comment on github