引言
三年前 Facebook 开始思考在目前设计系统下面临的问题,那时它们在前端项目、系统组件等部分使用的是 cssmodule 的样式方案。
直至今日,facebook 已经将所有的 Web 前端使用 React 进行重写的同时也使用了一种新的 Atomic Css-in-JS 对于它们的 Css 方案进行了重写。
最近,Facebook 团队开源了他们内部的 Atomic Css 解决方案:stylex,正是这套解决方案让 facebook 首页样式文件体积减少了至少 80%。
这篇文章中我们就这 Atomic Css 来聊聊 facebook 最近刚好开源的 stylex。
Atomic Css
概念
Atomic Css 是一种通过为每个样式声明创建一个规则来减少定义规则总量的方法。
举一个不是那么恰当的例子,比如说你可以将 Atomic 理解为 a、b、c... 一个一个原子化的字母,而每一个元素最终的生效的样式则是通过 a、b、c... 这样一个一个原子化的字母拼接而来。
假设我们想要得到一个长宽为百分百、背景色为红色的正方形,那么使用 Atomic Css 的方式来表示的话:
css
.w-full { width: 100%; };
.h-full { height: 100% };
.bg-red { backgroundColor: red };
html
<div class="w-full h-full bg-red">Square</div>
权衡
在 Acss 首次提出 Atomic Css 的实现方案后,之后有关于 Atomic Css 的相关讨论以及实践在前端社区内就如雨后春笋般四处开花。
无论是 Acss、Unocss、Tailwind 等等之类 Css 库,其实归根结底都是来源于同一个实现思路:Atmoic Css,那么 Atomic Css 究竟有什么好处呢?
接下来的内容中,我会以我自己的个人见解来和你聊聊笔者团队关于 Atomic Css 的看法。
多 Npm Package 下的样式复杂性
笔者所在团队日常除了常规的前端页面开发外,还负责了以下两个方面:
-
日常项目中和业务强相关的偏业务性组件。
-
日常项目中和业务无关性质的基础性组件。
无论是在业务还是基础组件的开发和维护上,如何缩减相关组件体积以至于可以和使用到该组件的不同业务团队最小化的结合一直是我们在寻求的目标。
举一个比较简单的例子,假设我们在开发一个 Button 组件的同时定义了一个 corp-button
的样式:
css
.corp-button {
height: 100%;
width: 100%;
}
此时当我们再次开发另一个 input
组件时,大多时候我们其实会经常用到 height:100%; width:100%
的样式,传统的 Scoped Css 解决方案下难免我们会重新定义一份样式声明:
css
.corp-input {
height: 100%;
width: 100%;
font-weight: 500;
}
显而易见,往往在同一份组件库代码中不同的 class 定义存在无数重复的样式声明,无论是 CssModule 还是 Css-In-Js 都无法将这部分重复样式声明在构建/运行时删除掉。
上边的情况只是在单一存储库中很场景的问题,笔者所在团队日常负责的 NpmPackage 远远不止一个,所以 Atomic Css 的概念可以帮助我们解决这个问题。
只要我们可以保证 Atomic 原子性,那么无论是存在多少 Package 我们也可以在项目中最大程度的保证这份样式文件的复用。
举一个简单的例子,比如上述的 corp-button
经过 atomic css 拆分为将不同的样式声明拆分为了更加原子化的 class 声明:
css
.w-full {
width: 100%;
};
.h-full {
height: 100%;
}
.corp-button => Compiled => .w-full .h-full
而在 input 组件中同样可以使用拆分后的 atomic:
css
.font-normal {
font-weight: 500;
}
.corp-input => Compiled => .w-full .h-full .font-normal
这种情况下,Atomic Css 可以大大减少调用方在使用不同 Npm Package 下的样式文件体积从而对于页面加载性能来说是一种极大的提升。
单一项目复杂度上升时的样式文件体积
往往大多数前端团队由于历史、规模原因无法左右依赖组件方的技术架构方案。
与其息息相关更多的是随着频繁、快速的业务迭代下带来样式文件复杂度直线上升的问题。
那么怎么解释这里的样式文件复杂度直接上升的问题呢,我们来看一个稍微抽象的例子。
比如 A 同学在负责 ProjectA 项目,跟随着频繁的业务迭代下难免一直会有新的页面、功能增加现有的项目中。
那么,对于前端工程师来说随着需求的频繁增加难免需要增加很多个新的 class 来编写这部分新增的样式。
传统的 CssModule 以及 Css-In-Js 方案可以让我们在 class 的声明上让我们无需考虑新的命名会和旧命名重复的问题,但它仍无法解决随着新的需求到来仍然会增加新的样式声明内容从而带来更大的样式文件体积影响页面性能。
如果我们使用 Atomic 方案来处理 Css 文件的话,无论在多么频繁的需求迭代背景下样式文件体积并不会跟随项目复杂度而直线上升,原子化的 Css 文件体积到达一个极限的拐点之后会渐渐趋于平稳。
上面的描述稍微有点抽象,但是并不难理解。就好比如何项目中需要新增一个背景色为红色宽高均为百分五十的 div 时,在之前的方案中我们不难会直接声明:
css
.new-demand-block {
width: 50%;
height: 50%;
background: red;
}
最终构建之后的样式文件中会加入 .new-demand-block
这部分的样式。
而如果使用 Aotmic Css 的方案,由于之前如果已经定义过 width:50%
、height:50%
、background:red
的 Atomic Class 所以新的样式文件中并不会存在这些根据新需求而来的样式。
不过有些同学会疑惑,这不是会将样式文件体积转化到了 HTML 中去了吗?
实际的确是这样,但是这也仅仅是首屏 HTML 会携带这部分 Atomic ClassName。同时会对于 HTML 模版中的相同 Atomic Css,Gzip 会帮我们对于这部分的重复 ClassName 影响处理的非常小了。
日常业务交付标准下的样式复用"错乱"性
同样,Atomic Css 方案还有一个和日常开发息息相关的影响点。
相信绝大多数开发同学都会碰到伴随着新需求上线或者修复某些 Bug 的同时突然发现影响了之前已经经过验证的页面样式。
这也是 Utility Css 会带来的问题,为了节省样式体积或是节约开发成本我们往往会选择在项目中复用相同类型的样式。
但是随着项目日积月累,我们会面临修改这部分样式时带来的隐患:修改 A 模块的 class 样式内容,或许会影响到 B、C 等模块。
这无异对于开发还是测试来说都是一种灾难,Atomic Css 的出现其实可以很好的帮助我们来解决这个问题。
每一处的元素都是由一个一个 Atomic 组成的样式,在编写新的 Css 声明时由于已经是 Atomic 方案所以大可不必担心样式体积的荣誉而抽离一些 Utility Css。
自然,当我们修改某一处样式文件内容时也完全无需担心会影响到别的地方,因为每次我们修改的并不是 class 代表的意义而是使用一个一个 Atomic Class 来拼装获得当前元素最终的样式。
当前,如果你能保证你团队的样式系统是百分百的标准以及 Utility 的声明非常规范化,Atomic Css 在这个问题下的解决方案就稍微显得有些牵强,不过在我看来绝大多数业务项目由于客观原因是无法和组件库之类的对齐做到百分百的样式系统规范化。
Stylex
开始之前
Atmoic Css 在 stylex 出现之前也有许多优秀的解决方案,比如 Tailwind、WindCss、UnoCss 等。
笔者所在的团队目前在使用的也并非 stylex 而是 Tailwind ,这篇文章更多是和大家介绍 Stylex 的用法以及我个人对于 stylex 的一些见解。
我们完全不用片面的认为 Atomic Css 就一定是 Tailwind 或者 Stylex 之类的某种实现框架。
无论 tailwind 还是 stylex 他们都是 Atomic Css 方案的不同实现方案而已,至于应该选择哪一种框架来实现 Atomic Css 更多还是根据大家各自团队中的实际情况来见仁见智。
究竟是 Utility 方式的 Tailwind 还是 Css-In-Js 方式的 Stylex 哪一种更优秀,这篇文章中我并不会进行讨论,因为我觉得讨论这些就好比我在告诉你应该使用 Vue 还是 React 来写前端一样。
简介
stylex 是 Facebook 最近开源的一套 Css-In-Js 的 Atomic Css 解决方案。
Stylex 的工作原理是通过 Babel 在编译阶段将编写的 Css-In-JS 代码生成一个一个 Atomic Css 样式,为输出的元素增加这些 classname 的同时最终输出在样式文件中。
虽然写法上和 Css-In-Js 类似,但是 stylex 几乎没有任何运行时的成本,大多数场景会在编译时在对应元素上添加上 classname 以及生成输出的样式文件。
同时对于需要结合不同变量增加不同样式的运行时场景,Stylex 会在必要时根据不同条件来快速的生成组件的类名字符串添加到对应元素中。
stylex.create/stylex.props
我们可以通过 stylex.create
方法创建 Atomic 样式内容,从而使用 stylex.props
将 stylex.create
方法生成的 Atomic Css 应用到元素上。
比如:
tsx
import * as stylex from '@stylexjs/stylex';
// stylex.create 创建样式内容
const styles = stylex.create({
root: {
backgroundColor: 'red',
padding: '1rem',
paddingInlineStart: '2rem'
},
title: {
backgroundColor: 'blue'
},
dynamic: (opacity) => ({
opacity
})
});
function HomePage() {
return (
// stylex.props 应用创建的样式内容到元素上
<div {...stylex.props(styles.root)}>
<h2 {...stylex.props(styles.title)}>Stylex</h2>
<p {...stylex.props(styles.dynamic(0.2))}>
Introduction to the basics of stylex.
</p>
</div>
);
}
export default HomePage;
上边的代码经过编译后的 Css 样式文件输出如下:
css
.x1uz70x1:not(#\#){padding:1rem}
.x1t391ir:not(#\#):not(#\#){background-color:blue}
.xrkmrrc:not(#\#):not(#\#){background-color:red}
.x1u4uod0:not(#\#):not(#\#){opacity:var(--opacity,revert)}
.xld8u84:not(#\#):not(#\#){padding-inline-start:2rem}
我们可以看到对于 stylex.create
创建的样式内容均被编译成为了一个一个 Atomic Css 的 classname。
同时对于页面上的元素来,在经过 stylex 的 babel 插件编译后元素的 classname 上会增加上一个又一个编译后的 Atomic classname:
唯一有一点需要注意的是:在 p 标签中我们使用了 styles.dynamic
,它表示一个动态生成的 Css 透明度样式。
透过上述编译后的内容,我们也可以清楚的看到在 stylex 内部是将这部分需要运行时生成的 Css 样式内容的值编译为了 css 变量的形式。
从而对于需要使用到动态 Css 变量的元素上动态替换它的 Css 变量值从而实现更新元素样式的效果,这个实现思路还是比较巧妙的。
stylex.defineVars/stylex.createTheme
stylex.defineVars
stylex 中还提供了 defineVars Api 来帮助我们快速定义样式变量的值。
tsx
// src/components/ButtonTokens.stylex.ts
import * as stylex from '@stylexjs/stylex';
// 通过 stylex 定义一系列 Button 相关样式变量
export const buttonTokens = stylex.defineVars({
bgColor: 'green',
textColor: 'red',
cornerRadius: '4px',
paddingBlock: '4px',
paddingInline: '8px'
});
// src/components/Button.ts
import * as stylex from '@stylexjs/stylex';
import './ButtonTokens.stylex';
import { buttonTokens } from './ButtonTokens.stylex';
const styles = stylex.create({
base: {
borderWidth: 0,
backgroundColor: buttonTokens.bgColor,
color: buttonTokens.textColor,
borderRadius: buttonTokens.cornerRadius,
paddingBlock: buttonTokens.paddingBlock,
paddingInline: buttonTokens.paddingInline
}
});
function Button() {
return <button {...stylex.props(styles.base)}>This is Single Button</button>;
}
export default Button;
需要额外注意的是官网文档中明确标注关于 defineVars 方法需要满足在文件名为 *.stylex.js/*.stylex.ts
的文件中被具名导出。
需要注意虽然文档上没提,但是
import './ButtonTokens.stylex';
必不可少。如果缺少这句导入实际样式内容并不会正常显示。
此时页面中的 Button :
stylex.createTheme
stylex.createTheme
接受两个参数,第一个参数为通过 defineVars
创建的变量集合,第二个参数为用于覆盖第一个参数的值,它是一个对象。
我们可以通过 stylex.createTheme 创建一个 StyleXStyles 对象从而提供给 stylesx.props 方法使用。
同时,我们也可以使用 stylex.createTheme
来通过覆盖通过 stylex.defineVars
声明的变量从而创建主题,比如:
我们对上述的按钮稍作修改,让按钮可以支持一个自定义主题的传入:
tsx
import * as stylex from '@stylexjs/stylex';
import './ButtonTokens.stylex';
import { buttonTokens } from './ButtonTokens.stylex';
const styles = stylex.create({
base: {
borderWidth: 0,
backgroundColor: buttonTokens.bgColor,
color: buttonTokens.textColor,
borderRadius: buttonTokens.cornerRadius,
paddingBlock: buttonTokens.paddingBlock,
paddingInline: buttonTokens.paddingInline
}
});
// Button 组件可以额外接受一个 theme 的主题
function Button(props: { theme?: stylex.Theme<typeof buttonTokens> }) {
return (
<button {...stylex.props(props.theme, styles.base)}>
This is Single Button
</button>
);
}
export default Button;
然后在将使用 Button 的地方稍作修改为:
tsx
import * as stylex from '@stylexjs/stylex';
import Button from './components/Button';
import { buttonTokens } from './components/ButtonTokens.stylex';
const otherTheme = stylex.createTheme(buttonTokens, {
bgColor: '#000',
textColor: 'yellow',
cornerRadius: '4px',
paddingBlock: '4px',
paddingInline: '8px'
});
function HomePage() {
return (
<div>
{/* 未传入特定主题,使用默认主题 */}
<Button />
{/* 传入特定主题,覆盖原本主题 */}
<Button theme={otherTheme} />
</div>
);
}
export default HomePage;
此时,页面上会出现两个不同主题的按钮:
实际上 createTheme 对于默认的 defineVars 的覆盖也是通过 Css 变量优先级来确定主题优先级的:
红色文字按钮的样式变量来源于 defineVars 的全局 css 变量,而黄色按钮是通过编译为同样的 css 变量名在在元素上重新定义自然优先级会比全局 css 变量更高。
看法
上边和大家简单聊了些 stylex 的 Api 以及它的基本使用姿势。
实话说笔者对于 stylex 也并没有太多的实践经验,不过目前看起来相较于目前流行的 Tailwind 这种类似 Utility Css 的 atomic css 方案来说,这种 Css-In-Js 的解决方案在代码组织上以及类型约束上的确对于代码的可读性以及组织性会更加便携一些。
不过 stylex 现阶段来无论是从构建生态、内置实现(比如 #197,#40 都是我在编写 Demo 时碰到的一些问题)来说可能对于在生产应用上使用来说还是有所欠缺。
不过总的来讲,Css-In-Js 的 Atomic Css 解决方案无论是在业务代码还是基础 Components 中未来一定会是一个不错的方案。
结尾
文章这里就和大家说声再见了,笔者也会关注 stylex 的后续更新同时未来会为大家带来更多关于 stylex 的实践。
希望文章中的内容可以帮助到大家。