最近我在react项目中总能看到别人用styled-components
来定义组件的样式,便也跟着用,但是一直不清楚用它的原因以及它的实现原理,于是想着抽空了解一下~~
一、简介
styled-components以组件的形式来声明样式,让样式也成为组件。它是一种 CSS-in-JS 的实现。
二、简单用法介绍
- 基本用法
import React from 'react';
import styled from 'styled-components';
const Button = styled.button`
color: red;
background: white;
`;
export default () => {
return (
<Button>1111</Button>
)
}
- 支持嵌套、伪元素、伪选择器
const Container = styled.div`
width: 100px;
height: 100px;
.box {
background: red;
}
&:hover {
background: blue;
}
`;
- 支持变量、属性
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> )
}
- 扩展样式
以一个组件为基础生成另一个组件
const NewButton = styled(Button)`
color: blue;
`;
三、优点
Styled Components 的官方网站将其优点归结为:
- Automatic critical CSS:
styled-components
持续跟踪页面上渲染的组件,并自动注入样式。结合使用代码拆分, 可以实现仅加载所需的最少代码。 - 解决了 class name 冲突:
styled-components
为样式生成唯一的 class name,开发者不必再担心 class name 重复、覆盖以及拼写的问题。(CSS Modules
通过哈希编码局部类名实现这一点) - CSS 更容易移除:使用
styled-components
可以很轻松地知道代码中某个 class 在哪儿用到,因为每个样式都有其关联的组件。如果检测到某个组件未使用并且被删除,则其所有的样式也都被删除。 - 简单的动态样式:可以很简单直观的实现根据组件的
props
或者全局主题适配样式,无需手动管理多个 classes。 - 无痛维护:无需搜索不同的文件来查找影响组件的样式,无论代码多庞大,维护起来都是小菜一碟。
- 自动提供前缀:按照当前标准写 CSS,其余的交给
styled-components
处理。
四、缺点
-
因为css-in-js,所以可能会让组件的tsx文件变得很长
-
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传入,则会生成很多样式,造成性能降低。
-
生成的className是随机字符串,不方便debug
-
没有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、创建组件过程
- styled构造函数实现大概长这样:
const styled = (tag: Target) => (...args) => {
return createStyledComponent(tag, {}, css(...args))
}
其中,css函数
用来处理标签模板字面量生成RuleSet
;createStyledComponent
是个高阶组件,应用到传进来的Target上
- 在
createStyledComponent
实现内部,会创建一个唯一componentId
, 一般通过全局count
++,再加上MurmurHash
算法,来确保这个id唯一。
count++;
const componentId = 'sc-' + hash('sc' + count);
-
然后根据
componentId
和css函数生成的RuleSet
计算生成类名className
-
使用stylis对样式预处理,添加浏览器对应前缀等
-
创建style标签,塞进head标签中,然后通过
StyleSheet
对象将样式规则插入到DOM中
styleSheet.inject(
this.componentId,
stringifyRules(flatCSS,`.${className}`, undefined, componentId),
className,
)
- 通过React.createElement创建组件,绑定className