聊一聊styled-components

2,654 阅读2分钟

最近我在react项目中总能看到别人用styled-components来定义组件的样式,便也跟着用,但是一直不清楚用它的原因以及它的实现原理,于是想着抽空了解一下~~

一、简介

styled-components以组件的形式来声明样式,让样式也成为组件。它是一种 CSS-in-JS 的实现。

二、简单用法介绍

  1. 基本用法
import React from 'react';
import styled from 'styled-components';

const Button = styled.button`
  color: red;
  background: white;
`;

export default () => {
    return (
        <Button>1111</Button>
    )
}
  1. 支持嵌套、伪元素、伪选择器
const Container = styled.div`
  width: 100px;
  height: 100px;
  .box {
      background: red;
  } 
  &:hover {
      background: blue;
  }
`;
  1. 支持变量、属性
const buttonColor = 'red'; 
const Button = styled.button`
    color: ${buttonColor};
    font-size: 16px;
    background: ${props => props.primary ? 'pink' : buttonColor};
`;
export default () => { 
    return ( <Button primary>1111</Button> ) 
}
  1. 扩展样式

以一个组件为基础生成另一个组件

const NewButton = styled(Button)`
    color: blue;
`;

三、优点

Styled Components 的官方网站将其优点归结为:

  • Automatic critical CSSstyled-components 持续跟踪页面上渲染的组件,并自动注入样式。结合使用代码拆分, 可以实现仅加载所需的最少代码。
  • 解决了 class name 冲突styled-components 为样式生成唯一的 class name,开发者不必再担心 class name 重复、覆盖以及拼写的问题。(CSS Modules 通过哈希编码局部类名实现这一点)
  • CSS 更容易移除:使用 styled-components 可以很轻松地知道代码中某个 class 在哪儿用到,因为每个样式都有其关联的组件。如果检测到某个组件未使用并且被删除,则其所有的样式也都被删除。
  • 简单的动态样式:可以很简单直观的实现根据组件的 props 或者全局主题适配样式,无需手动管理多个 classes。
  • 无痛维护:无需搜索不同的文件来查找影响组件的样式,无论代码多庞大,维护起来都是小菜一碟。
  • 自动提供前缀:按照当前标准写 CSS,其余的交给 styled-components 处理。

四、缺点

  1. 因为css-in-js,所以可能会让组件的tsx文件变得很长

  2. styled-components每次渲染都会重新计算cssRule,并且计算出新的className,如果样式表中还没有对应的className,则会插入到样式表中;

举个例子

const Button = styled.button`
    color: red;
    font-size: 16px;
    background: ${props => props.active ? 'pink' : 'blue'};
`;
// active 切换时会生成两个类名
.sfDSDA {
    color: red;
    font-size: 16px;
    background: pink;
}
.eFDFER {
    color: red;
    font-size: 16px;
    background: blue;
}

所以可能会生成很多冗余的样式。

因此也不推荐用来实现动画,如果动画值通过props传入,则会生成很多样式,造成性能降低。

  1. 生成的className是随机字符串,不方便debug

  2. 没有css语法高亮提示

不过这个可以在vscode中安装vscode-styled-components插件就可以了

五、原理解析

1、Tagged Template Literals

标签模板字面量是ES6新增的特性,styled-components就是基于这个特性构建的。

先来看看标签模板是啥?

模板字符串可以紧跟在一个函数名后面,然后该函数会被调用来处理这个模板字符串。

看个例子

alert`hahaha`
// 等同于
alert(['hahaha'])

如果模板字符串里有变量,会稍微复杂一点

const a=1;
const b=2;
alertNew`hahaha${a}hello${b+1}bye`
// 等同于
alertNew(['hahaha','hello','bye'],1,3)

第一个参数是没有变量替换的部分组成的数组,后面的参数是各个变量被替换后的值

那回到styled-components语法上来

styled.button 只是 styled('button')的简写,它实际上相当于调用了一个 button 函数。

const Button = styled.button`
  color: red;
  font-size: 16px;
`;
// 等同于
const Button = styled('button')([
    'color: red;' +
    'font-size: 16px;'
])
const buttonColor = 'red';
const Button = styled.button`
  color: ${buttonColor};
  font-size: 16px;
  background: ${props => props.primary ? 'pink' : buttonColor};
`;
// 等同于
const Button = styled('button')([
    'color: ',
    ';font-size: 16px;background: ',
    ';'
], 'red', 'props => props.primary ? "pink" : "red"')

如何获取样式字符串?

(strs, ...exprs) => {
    return exprs.reduce((res, exp, index) => {
        const val = typeof exp === 'function' ? exp(this.props) : exp;
        return res + val + strs[inex + 1];
    }, [strs[0]])
}

2、创建组件过程

  1. styled构造函数实现大概长这样:
const styled = (tag: Target) => (...args) => {
    return createStyledComponent(tag, {}, css(...args))
}

其中,css函数用来处理标签模板字面量生成RuleSetcreateStyledComponent是个高阶组件,应用到传进来的Target上

  1. createStyledComponent实现内部,会创建一个唯一componentId, 一般通过全局count++,再加上MurmurHash算法,来确保这个id唯一。
count++;
const componentId = 'sc-' + hash('sc' + count);
  1. 然后根据componentId和css函数生成的RuleSet计算生成类名className

  2. 使用stylis对样式预处理,添加浏览器对应前缀等

  3. 创建style标签,塞进head标签中,然后通过StyleSheet对象将样式规则插入到DOM中

styleSheet.inject(
    this.componentId,
    stringifyRules(flatCSS,`.${className}`, undefined, componentId),
    className,
)
  1. 通过React.createElement创建组件,绑定className