当前位置: 首页 > 科技观察

CSS-in-JS库如何工作?

时间:2023-03-13 23:09:31 科技观察

前言在最近学习MaterialUI的过程中,笔者发现MaterialUI的组件都是用CSS-in-JS的方式编写的。我想我以前在社区看到过很多批评CSS-in-JS的文章,这有点令人惊讶。CSS-in-JS库如何工作?是什么让MaterialUI选择了CSS-in-JS的方式来开发组件库?这不禁勾起笔者的好奇心,于是决定探索并实现一个属于自己的CSS-in-JS库。目前在社区中流行的主要有两个CSS-in-JS库:emotionstyled-components。两者的API基本相同。鉴于情感源码中JavaScript、Flow、TypeScript混杂在一起,笔者阅读起来确实有些吃力。于是果断放弃了学习情感的想法。所以把styled-components作为学习对象,看看它是如何工作的。如何使用styled-components的核心能力打开styled-components官方文档[1],点击导航栏中的Documentation[2],找到APIReference[3]一栏,首先展示的是styled-components的核心APIstyled-components——styled,这个用法相信了解React的同学都或多或少熟悉:constButton=styled.div`background:palevioletred;边界半径:3px;边框:无;颜色:白色;`;constTomatoButton=styled(Button)`background:tomato;`;在React组件中使用模板字符串编写CSS实现了一个自带样式的React组件。把styled-components的GitHubRepo[4]clone到本地,安装依赖,用VSCode打开,会发现styled-components是一个monorepo,核心包和Repo同名:直接启动从src/index.ts查看源码:默认导出的styledAPI是从src/constructors/styled.tsx导出的,继续向上溯源。src/constructors/styled.tsx中的代码很简单,去掉了type,简化代码如下:importcreateStyledComponentfrom'../models/StyledComponent';//HTML标签列表importdomElementsfrom'../utils/domElements';importconstructWithOptionsfrom'./constructWithOptions';//构造基本样式方法constbaseStyled=(tag)=>constructWithOptions(createStyledComponent,tag);conststyled=baseStyled;//实现HTML标签的快捷调用方法domElements.forEach(domElement=>{styled[domElement]=baseStyled(domElement);});exportdefaultstyled;从styledAPI的入口我们可以知道,在上面的使用方法[5]部分,示例代码:constButton=styled.div`background:palevioletred;边界半径:3px;边框:无;颜色:白色;`;实际上与:constButton=styled('div')`background:palevioletred;边界半径:3px;边框:无;颜色:白色;`;styledAPI为了方便封装了快捷调用方法,可以通过styled[HTMLElement]方法快速创建基于HTML标签的组件。接下来继续向上追溯,找到styledAPI相关的baseStyled的创建方法:constbaseStyled=(tag)=>constructWithOptions(createStyledComponent,tag);找到constructWithOptions方法所在的src/constructors/constructWithOptions.ts,去掉type并简化最终代码如下:importcssfrom'./css';exportdefaultfunctionconstructWithOptions(componentConstructor,tag,options){consttemplateFunction=(initialStyles,...interpolations)=>componentConstructor(tag,options,css(initialStyles,...interpolations));}returntemplateFunction;}精简后的代码变得极其简单,baseStyled由constructWithOptions函数工厂创建并返回。constructWithOptions函数工厂的核心其实就是templateFunction方法,调用组件构造函数componentConstructor,返回一个带有样式的组件。至此,我们就要进入styled-components的核心了,一起来看看吧。核心源码constructWithOptions函数工厂调用的组件构造方法componentConstructor是从外部传入的,这个组件构造方法是整个styled-components的核心。上面源码中,baseStyled是组件构造方法createStyledComponent传给componentConstructor后返回的templateFunction。templateFunction的参数是模板字符串写入的CSS样式,最终会传入组件构造方法createStyledComponent。文字描述非常混乱。画一张图梳理一下创建带样式组件的过程:即当用户使用带样式API创建带样式组件时,本质上是在调用createStyledComponent的组件构造函数。createStyledComponent的源代码在src/models/StyledComponent.ts中。由于源码比较复杂,详细阅读需要移步GitHub:styled-components/StyledComponent.tsatmainstyled-components/styled-components[6]从源码可以知道,createStyledComponent的返回值是一个有样式的组件WrappedStyledComponent,在返回之前会对组件进行一些处理,大部分是在组件上设置一些属性,查看源码可以暂时跳过。从返回值向上追溯,发现WrappedStyledComponent是使用React.forwardRef创建的组件。该组件调用HookuseStyledComponentImpl并返回Hook的返回值。继续从返回值追根溯源,发现useStyledComponentImplHook中的大部分代码都是与我们理解styled-components工作原理无关的代码。都是看源码可以暂时跳过的部分,但是有一个Hook的名字让笔者觉得它是整个styled-components的核心部分:constgeneratedClassName=useInjectedStyle(componentStyle,isStatic,context,process.env.NODE_ENV!=='production'?forwardedComponent.warnTooManyClasses:undefined);在写这篇文章之前,笔者大致知道CSS-in-JS库是通过在运行时解析模板字符串,动态创建标签来在页面中插入样式来实现的。从useInjectedStyleHook的名字来看,它的行为是动态创建标签并将其插入到页面中。深入useInjectedStyle,去掉类并精简后的代码如下:conststylis=useStylis();常量类名=isStatic?componentStyle.generateAndInjectStyles(EMPTY_OBJECT,styleSheet,stylis):componentStyle.generateAndInjectStyles(resolvedAttrs,styleSheet,stylis);returnclassName;}useInjectedStyle在参数传入的componentStyle上调用generateAndInjectStyles方法,将样式传入,返回样式对应的className。进一步查看componentStyle,它在createStyledComponent中实例化并传递给useInjectedStyle:所以componentStyle上的generateAndInjectStyle方法其实就是这个类的一个实例,对应的源码比较长,比较复杂,需要移步GitHub详细阅读。核心是解析模板字符串,类名哈希后返回className:styled-components/ComponentStyle.tsatmainstyled-components/styled-components[7]至此核心源码分析完毕,其余源码主要是为了提供更多基本功能以外的API,提高易用性。solid-sc实现对styled-components核心源码的解析后,回到CSS-in-JS出现的原因。最重要的因素可能是JSX的出现,在前端领域掀起了一股AllinJS的浪潮。上面提到的CSS-in-JS库就这样诞生了。那么,我们是不是可以理解为CSS-in-JS和JSX属于一种绑定关系呢?不管这个问题的答案如何,至少一个可以使用JSX语法的前端框架应该可以使用CSS-in-JS技术方案。最近笔者也在研究和学习SolidJS,希望能参与到SolidJS的生态建设中。我注意到在SolidJS社区中没有可以匹配emotion/styled-components的CSS-in-JS库,尽管已经有可以满足大部分需求的库。SolidStyledComponents:https://github.com/solidjs/solid-styled-components[8]但是仅仅会用并不是作者的追求。希望自己尝试实现一个,所以有一个MVP版:learning-styled-components/index.tsxatmasterwjq990112/learning-styled-components[9]不熟悉SolidJS的同学可以暂时看成React.上面承载的样式在运行时解析,赋予一个唯一的className,然后塞入