[技术地图]

[技术地图] ?styled-components ?

Bobi.ink

2019-05-29

React 组件设计实践总结 03 - 样式的管理一文中吹了一波 styled-components 后,本文想深入来了解一下 styled-components 的原理. 如果你对 styled-components 还不了解,建议先阅读一下官方文档或前面的文章.

本文基于 styled-components v4.13 版本

目录


从 Tagged Template Literals 说起

标签模板字面量(Tagged Template Literals)是 ES6 新增的特性,它允许你自定义字符串的内插(interpolation)规则, styled-components 正是基于这个特性构建:

它的原理非常简单,所有静态字符串会被拆分出来合并成为数组, 作为第一个参数传入到目标函数,而内插(interpolation)表达式的值则会作为 rest 参数传入:

标签模板字面量相比普通的模板字面量更加灵活. 普通模板字符串会将所有内插值转换为字符串,而标签模板字面量则由你自己来控制:

因为标签模板字符串简洁的语法和灵活性,它比较适用于作为DSL, 不需要在语言层面进行支持,比如前阵子preact作者开发的htm, 口号就是”取代 JSX,而且不需要编译器支持”, 通过这种方式是可以优雅地实现‘你的网站或许不需要前端构建’.

另一个典型的例子就是 jest表格测试, 这样形式可读性更高:

标签模板字面量的脑洞还在继续,比如可以用来写 markdown,再生成 react 组件。限于篇幅就不啰嗦了

扩展:


源码导读

现在来看一下 styled-components 的实现。为了行文简洁,我们只关心 styled-components 的核心逻辑,所以我对源代码进行了大量的简化,比如忽略掉服务端渲染、ReactNative 实现、babel 插件等等.

1. 处理标签模板字面量

先从 styled 构造函数看起:

styled 构造函数接收一个包装组件 target,而标签模板字面量则由css函数进行处理的. 这个函数在 styled-components 中非常常用,类似于 SCSSmixin 角色. css 函数会标签模板字面量规范化, 例如:

css 实现也非常简单:

interleave函数将将静态字符串数组和内插值’拉链式‘交叉合并为单个数组, 比如[1, 2] + [a, b]会合并为[1, a, 2, b]

关键在于如何将数组进行扁平化, 这个由 flatten 函数实现. flatten 函数会将嵌套的 css(数组形式)递归 concat 在一起,将 StyledComponent 组件转换为类名引用、还有处理 keyframe 等等. 最终剩下静态字符串和函数, 输出结果如上所示。

实际上 styled-components 会进行两次 flatten,第一次 flatten 将能够静态化的都转换成字符串,将嵌套的 css 结构打平, 只剩下一些函数,这些函数只能在运行时(比如在组件渲染时)执行;第二次是在运行时,拿到函数的运行上下文(props、theme 等等)后, 执行所有函数,将函数的执行结果进行递归合并,最终生成的是一个纯字符串数组. 对于标签模板字面量的处理大概都是这个过程. 看看 flatten 的实现:

总结一下标签模板字面量的处理流程大概是这样子:


2. React 组件的封装

现在看看如何构造出 React 组件。styled-components 通过 createStyledComponent 高阶组件将组件封装为 StyledComponent 组件:

createStyledComponent 是一个典型的高阶组件,它在执行期间会生成一个唯一的组件 id 和创建ComponentStyle对象. ComponentStyle 对象用于维护 css 函数生成的 cssRules, 在运行时(组件渲染时)得到执行的上下文后生成最终的样式和类名。

再来看看 StyledComponent 的实现, StyledComponent 在组件渲染时,将当前的 props+theme 作为 context 传递给 ComponentStyle,生成类名.


3. 样式和类名的生成

上面看到 StyleComponent 通过 ComponentStyle 类来构造样式表并生成类名, ComponentStyle 拿到 context 后,再次调用 flatten 将 css rule 扁平化,得到一个纯字符串数组。通过使用 hash 算法生成类名, 并使用stylis 对样式进行预处理. 最后通过 StyleSheet 对象将样式规则插入到 DOM 中

stylis是一个 3kb 的轻量的 CSS 预处理器, styled-components 所有的 CSS 特性都依赖于它, 例如嵌套规则(a {&:hover{}})、厂商前缀、压缩等等.


4. DOM 层操作

现在来看一下 StyleSheet, StyleSheet 负责收集所有组件的样式规则,并插入到 DOM 中

看看简化版的 makeTag


5. 总结

代码可能看晕了,通过流程图来梳理一下过程.

上一篇文章技术地图 - vue-cli一点代码也没有罗列,只有一个流程图, 读者可能一下子就傻眼了, 不知道在说些什么; 而且这个流程图太大,在移动端不好阅读. 这期稍微改进一下,新增’源码导读‘一节,代码表达能力毫无疑问是胜于流程图的,但是代码相对比较细节琐碎,所以第一是将代码进行简化,留下核心的逻辑,第二是使用流程图表示大概的程序流程,以及流程主体之间的关系.

如上图 styled-components 主要有四个核心对象:

  • WrappedComponent: 这是 createStyledComponent 创建的包装组件,这个组件保存的被包装的 target、并生成组件 id 和 ComponentStyle 对象
  • StyledComponent: 这是样式组件,在它 render 时会将 props 作为 context 传递给 ComponentStyle,并生成类名
  • ComponentStyle: 负责生成最终的样式表和唯一的类名,并调用 StyleSheet 将生成的样表注入到文档中
  • StyleSheet: 负责管理已生成的样式表, 并注入到文档中

styled-components 性能优化建议

styled-components 每次渲染都会重新计算 cssRule,并进行 hash 计算出 className,如果已经对应的 className 还没插入到样式表中,则使用 stylis 进行预处理,并插入到样式表中;

另外 styled-components 对静态 cssRule(没有任何内插函数)进行了优化,它们不会监听 ThemeContext 变化, 且在渲染时不会重新计算。

通过这些规则可以得出以下性能优化的建议:

  • 静态化的 cssRule 性能是最好的
  • 降低 StyledComponent 状态复杂度. styled-components 并不会对已有的不变的样式规则进行复用,一旦状态变化 styled-component 会生成一个全新的样式规则和类名. 这是最简单的一种实现, 避免了样式复用的复杂性,同时保持样式的隔离性, 问题就是会产生样式冗余。 例如 const Foo = styled.div<{ active: boolean }>` color: red; background: ${props => (props.active ? 'blue' : 'red')}; `; active 切换之间会生成两个类名: .cQAOKL { color: red; background: red; } .kklCtT { color: red; background: blue; } 如果把 StyledComponent 看做是一个状态机,那么 styled-components 可能会为每一个可能的状态生成独立的样式. 如果 StyledComponent 样式很多, 而且状态比较复杂,那么会生成很多冗余的样式.
  • 不要用于动画。上面了解到 styled-component 会为每个状态生成一个样式表. 动画一般会有很多中间值,在短时间内进行变化,如果动画值通过props传入该StyledComponent来应用样式,这样会生成很多样式,性能非常差: const Bar = styled.div<{ width: boolean }>` color: red; // 千万别这么干 width: ${props => props.width}; `; 这种动画场景最好使用 style 内联样式来做

OK, 行文结束。styled-components 不过如此是吧?


技术地图

  • CSS 相关
    • @emotion/unitless 判断属性值是否需要单位
    • css-to-react-native 将 css 转换为 ReactNative style 属性
    • stylis 轻量的 CSS 预处理器
  • React 相关
    • @emotion/is-prop-valid 判断是否是合法的 DOM 属性
    • hoist-non-react-statics 提升React组件的静态属性,用于高阶组件场景
    • react-is: 判断各种 React 组件类型
    • react-primitives 这是一个有意思的库,这个库试图围绕着构建 React 应用提出一套理想的原语,通俗的说就是通过它可以导入不同平台的组件。
    • react-frame-component 将react渲染到iframe中。也是一个比较有意思的库
    • react-live react实时编辑器和展示,主要用于文档
  • 构建相关
    • bundlesize 检查包大小
    • codemod 使用babel-plugin来重写Javascript或Typescript代码, 一般用于制作升级脚本
    • microbundle 一个零配置的打包器,基于Rollup,可以用于库的打包和开发, preact作者开发必属精品

Please enable JavaScript to view the comments powered by Disqus.

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏胡哥有话说

构建工具篇 - react 的 yarn eject 构建命令都做了什么

前段时间,一直在研究 react 技术栈,对于项目的构建方面,又有一定的特殊需求,通过 npx create-react-app [filename] 安装以后...

11610
来自专栏无人打理的花园

CRA (create-react-app) IE 兼容方案

其实都是 ES6 语法不支持导致了,理论上经过 babel 处理后就好。尝试了在入口文件中加入官方提供的 react-app-polyfill 和 babel ...

40520
来自专栏萌兔it

AngularJS vs Vue.js:对于两个流行前端框架的比较

多年来,Web的前端开发经历了各种各样的发展,新框架不断的涌现。如您所想,为了保持竞争优势,框架都是东拼西凑来开发的。在当今的环境下,Angul...

17730
来自专栏前端入门学习

web前端与手机应用的这些重点和知识点,你知道多少呢

随着互联网、移动互联网的发展,HTML5成为了客户端软件开发的主流技术,HTML5实际上是由:HTML5语言、CSS3、JAVASCRIPT语言组成。

12940
来自专栏杨龙飞前端

RN中webview的一些思考

刚开始只是对接一下RN,h5部分,嵌套在RN里的webview里需要隐藏一些原生的按钮,遇到很多沟通上的问题,本来没使用RN之前,也是嵌套在webview里,也...

25640
来自专栏胡哥有话说

如何优雅的开发一个Vue插件

vue.js和React.js是前端开发框架的两架马车,React是扎哥 的Facebook推广开源的,Vue是尤雨溪(Evan You)个人主要进行开源维护的...

8140
来自专栏胡哥有话说

基于Taro的微信小程序模板消息-获取formId功能模块封装实践

在微信小程序中,小程序提供了一种能力-模板消息,官方文档是这样描述的:“基于微信的通知渠道,我们为开发者提供了可以高效触达用户的模板消息能力,以便实现服务的闭环...

32520
来自专栏胡哥有话说

基于Taro的的微信小程序分享图片功能实践

在各种小程序(微信、百度、支付宝)、H5、NativeApp 纷纷扰扰的当下,给大家强烈安利一款基于React的多终端开发利器:京东Taro(泰罗·奥特曼),T...

26610
来自专栏Web技术布道师

Fast Web Scraping With ReactPHP

Have you ever needed to grab some data from a site that doesn’t provide a public...

15010
来自专栏前端迷

Typescript配合React实践

使用ts写React代码写了将近三个月,从刚开始觉得特别垃圾到现在觉得没有ts不行的一些实践以及思考。如果按部就班的写React就体会不到使用ts的乐趣,如果多...

12420

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励