近日,Meta
开源了一款CSS-in-JS库 —— StyleX
。看命名方式,Style - X
是不是有点像JS - X
,他们有关系么?当然有。
JSX
是一种用JS描述HTML的语法规范,广泛应用于前端框架中(比如React
、SolidJS
...),由Meta
公司提出。
同样的,按照Meta
的设想,StyleX
是一种用JS描述CSS的语法规范。
早在React Conf 2019,Meta
工程师Frank就介绍了这种Meta
内部使用的CSS-in-JS库。
从Meta
内部使用,到大会对外宣传,这期间肯定已经经历大量内部项目的洗礼。而从做完宣传到最终开源,又经历了快5年时间。
那么,这款Meta
出品、打磨这么长时间的CSS-in-JS库,到底有什么特点呢?
本文让我们来聊聊。
免费领取卡颂原创React教程(原价359)、加入人类高质量前端群
为什么需要CSS解决方案
市面上有非常多CSS解决方案,比如:
BEM
命名规范CSS Module
规范- 原子
CSS
(比如TailwindCSS
) CSS-in-JS
(比如emotion
)
为什么需要这些方案?原生CSS
哪里不好?在这里,我们举个小例子(例子来源于React Conf 2019)。考虑如下代码:
CSS
文件如下:
.blue {color: blue;} .red {color: red;}
HTML
文件如下:
<p class="red blue">我是什么颜色?</p>
请问p
标签是什么颜色的?
从class
来看,blue
在red
后面,p
应该是蓝色的么?
实际上,样式取决于他们在样式表中定义的顺序,.red
的定义在.blue
后面,所以p
应该是红色的。
是不是已经有点晕了?再增加点难度。如果.red
和.blue
分别在两个文件中定义呢?
# css文件1 .blue {color: blue;}
# css文件2 .red {color: red;}
那p
的样式就取决于最终打包代码中样式文件的加载顺序。
上面只是原生CSS
中选择器优先级相关的一个缺陷(除此外还有其他缺陷,比如作用域缺失...)。随着项目体积增大、项目维护时间变长、项目维护人员更迭,这些缺陷会被逐渐放大。
正是由于这些原因,才出现了各种CSS解决方案。
StyleX的基本使用
StyleX
的API
很少,掌握下面两个就能上手使用:
stylex.create
,创建样式stylex.props
,定义props
比如:
import * as stylex from 'stylex'; // 创建样式 const styles = stylex.create({ red: {color: 'red'}, }); // 定义props const redStyleProps = stylex.props(styles.red);
使用时:
<div {...redStyleProps}>文字颜色是红色</div>
stylex
是如何解决上面提到的red blue
优先级问题呢?其实很简单,考虑如下代码:
import * as stylex from 'stylex'; // 创建样式 const styles = stylex.create({ red: {color: 'red'}, blue: {color: 'blue'} }); // 使用 <p {...styles.props(styles.red, styles.blue)}></p>
样式的优先级只需要考虑styles.props
中的定义顺序(blue
在red
后面,所以颜色为blue
),不需要考虑样式表的存在。
有些同学会说,看起来和常见的CSS-in-JS
没啥区别啊。那stylex
相比于他们的优势是啥呢?
相比其他CSS-in-JS的优势
首先要明确,stylex
虽然以CSS-in-JS
的形式存在,但本质上他是一种用JS描述CSS的规范。文章开头也提到,他的定位类似JSX
。
既然是规范,那他就不是对CSS
的简单封装、增强,而是一套自定义的样式编写规范,只不过这套规范最终会被编译为CSS
。
作为对比,Less
、Sass
这样的 CSS预处理器就是对CSS
语法的封装、增强
那么,stylex
都有哪些规范呢?
比如,stylex
鼓励将样式与组件写在同一个文件,类似Vue
的SFC
(单文件组件)。这么做除了让组件的样式与逻辑更方便维护,也减少了stylex
编译的实现难度。
再比如,CSS
中各种选择器的复杂组合增强了选择器的灵活性。但同时也增强了不确定性。举个例子,考虑如下三个选择器:
- .className > *
- .className ~ *
- .className:hover > div:first-child
这些对.className
应用的选择器将影响.className
的某些后代。当这样的选择器多了后,很可能会在开发者不知道的情况下改变某些后代元素的样式。
遇到这种情况我们一般会怎么处理呢?正确的选择当然是找到上述影响后代的选择器,再修改他。
但大家工作都这么忙,遇到这种问题,多半就是用新的选择器覆写样式,必要的时候还会加!important
后缀。久而久之,这代码就没法维护了。
为了规避这种情况,在stylex
中,除了可继承样式(指当父元素应用后,子孙元素默认会继承的样式,比如color
)外,不支持这些可以改变子孙后代样式的选择器。
那我该如何让子孙组件获得父组件同样的样式呢?通过props
透传啊~
也就是说,stylex
禁用了CSS
中可能造成混淆的选择器,用JS
的灵活性弥补这部分功能的缺失。
有些同学可能会说:这些功能,其他CSS-in-JS库也能做啊。
这就要谈到CSS-in-JS库最大的劣势 —— 为了计算出最终样式,在运行时会造成额外的样式计算开销。
stylex
通过编译来减少运行时的开销。比如对于上面提到过的stylex
的代码:
import * as stylex from 'stylex'; // 创建样式 const styles = stylex.create({ red: {color: 'red'}, }); // 定义props const redStyleProps = stylex.props(styles.red);
编译后的产物包括如下两部分:
JS
的编译产物:
import * as stylex from 'stylex'; const redStyleProps = {className: 'x1e2nbdu'};
CSS
的编译产物:
.x1e2nbdu { color: red; }
所以,运行时实际运行的代码始终为:
<div {...{className: 'x1e2nbdu'}}>...</div>
对于再复杂的样式,stylex
都会通过编译生成可复用的原子类名。
即使是跨文件使用样式,比如我们在另一个文件也定义个使用color: 'red'
样式的stylex
属性foo
:
import * as stylex from '@stylexjs/stylex'; const styles = stylex.create({ foo: { color: 'red', }, bar: { backgroundColor: 'blue', }, });
会得到如下编译结果,其中x1e2nbdu
是一个原子类名,他是上一个文件中styles.red
的编译产物:
import * as stylex from '@stylexjs/stylex'; const styles = { foo: { color: 'x1e2nbdu', $$css: true, }, bar: { backgroundColor: 'x1t391ir', $$css: true, }, };
随着项目体积增大,样式表的体积也能控制在合理的范围内。这种对原子类名的控制粒度是其他CSS-in-JS库办不到的。
相比于原子CSS的优势
stylex
相比TailwindCSS
这样的原子CSS
有什么优势呢?
这就要谈到原子CSS
的一个特点 —— 使用约定好的字符串实现样式。比如,使用TailwindCSS
定义图片的样式:
<img class="w-24 h-24 rounded-full mx-auto" src="/sarah-dayan.jpg" alt="" width="384" height="512">
效果如下:
由于样式都是由不同的原子类名字符串组合而成,TS
没法分析,这就没法实现样式的类型安全。
什么叫样式的类型安全?通俗的讲,如果我实现一个组件,组件通过style props
定义样式,我只希望使用者能够改变color
与fontSize
两个样式属性,不能修改其他属性。如果能实现这一点,就是样式的类型安全。
样式的类型安全有什么意义呢?举个例子:设想开发基础组件库的团队使用stylex
。那么当业务团队使用该组件库时,就只能自定义组件的一些样式(由组件库团队约束)。
当基础组件库升级时,组件库团队能很好对组件样式向下兼容(因为知道只有哪些样式允许被修改)。
在stylex
中,由于stylex.create
的产物本质是对象,所以我们可以为每个产物定义类型声明。比如在如下代码中,我们限制了组件style props
只能接受如下stylex样式
:
import type {StyleXStyles} from '@stylexjs/stylex'; type Props = { // ... style?: StyleXStyles<{ color?: string; backgroundColor?: string; borderColor?: string; borderTopColor?: string; borderEndColor?: string; borderBottomColor?: string; borderStartColor?: string; }>; };
总结
我猜想,当更多人知道stylex
后,他会收到比当初TailwindCSS
火时更多的两级分化的评价。
毕竟,stylex
的设计初衷是为了解决Meta
内部复杂应用的样式管理。如果:
- 你项目没有达到
Meta
这样的体量 - 你项目没有多年的迭代周期
- 你项目前前后后没有多个工程师经手
那大概率是不能接受stylex
设计理念中的这些约束。
对于stylex
,你怎么看?
技术前沿拓展
前端开发,你的认知不能仅局限于技术内,需要发散思维了解技术圈的前沿知识。细心的人会发现,开发内部工具的过程中,大量的页面、场景、组件等在不断重复,这种重复造轮子的工作,浪费工程师的大量时间。
介绍一款程序员都应该知道的软件 JNPF 快速开发平台,很多人都尝试用过它,它是功能的集大成者,任何信息化系统都可以基于它开发出来。
这是一个基于 Java Boot/.Net Core 构建的简单、跨平台快速开发框架。前后端封装了上千个常用类,方便扩展;集成了代码生成器,支持前后端业务代码生成,实现快速开发,提升工作效率;框架集成了表单、报表、图表、大屏等各种常用的 Demo 方便直接使用;后端框架支持 Vue2、Vue3。如果你有闲暇时间,可以做个知识拓展。