f随着 Facebook 和 Twitter 最近的产品部署,我认为一个新的趋势正在缓慢增加:Atomic CSS-in-JS。css
在这篇文章中,咱们将看到什么是Atomic CSS(原子 CSS),它如何与 Tailwind CSS 这种实用工具优先的样式库联系起来,目前不少大公司在 React 代码仓库中使用它们。html
因为我不是这方面的专家,因此我不会去深刻探讨它的利弊。我只是但愿能帮助你了解它的大体内容。react
先抛出一个使人开心的结论,新的 CSS 编写和构建方式让 Facebook 的主页减小了 80% 的 CSS 体积。web
什么是原子 CSS?segmentfault
你可能据说过各类 CSS 方法,如 BEM, OOCSS…react-native
<button class="button button--state-danger">Danger button</button>浏览器
如今,人们真的很喜欢 Tailwind CSS[1] 和它的 实用工具优先(utility-first)[2] 的概念。这与 Functional CSS 和 Tachyon[3] 这个库的理念很是接近。缓存
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"服务器
Button
</button>
用海量的实用工具类(utility classes)组成的样式表,让咱们能够在网页里大显身手。框架
原子 CSS 就像是实用工具优先(utility-first)CSS 的一个极端版本: 全部 CSS 类都有一个惟一的 CSS 规则。原子 CSS 最初是由 Thierry Koblentz (Yahoo!)在 2013 年挑战 CSS 最佳实践[4]时使用的。
/ 原子 CSS /
.bw-2x {
border-width: 2px;
}
.bss {
border-style: solid;
}
.sans {
font-style: sans-serif;
}
.p-1x {
padding: 10px;
}
/ 不是原子 CSS 由于这个类包含了两个规则 /
.p-1x-sans {
padding: 10px;
font-style: sans-serif;
}
使用实用工具/原子 CSS,咱们能够把结构层和表示层结合起来:当咱们须要改变按钮颜色时,咱们直接修改 HTML,而不是 CSS!
这种紧密耦合在现代 CSS-in-JS 的 React 代码库中也获得了认可,但彷佛 是 CSS 世界里最早对传统的关注点分离有一些异议。
CSS 权重也不是什么问题,由于咱们使用的是最简单的类选择器。
咱们如今经过 html 标签来添加样式,发现了一些有趣的事儿:
咱们增长新功能的时候,样式表的增加减缓了。
咱们能够处处移动 html 标签,而且能确保样式也一样生效。
咱们能够删除新特性,而且确保样式也同时被删掉了。
能够确定的缺点是,html 有点臃肿。对于服务器渲染的 web 应用程序来讲多是个缺点,可是类名中的高冗余使得 gzip 能够压缩得很好。同时它能够很好地处理以前重复的 css 规则。
一旦你的实用工具/原子 CSS 准备好了,它将不会有太大的变化或增加。能够更有效地缓存它(你能够将它附加到 vendor.css 中,从新部署的时候它也不会失效)。它还具备至关好的可移植性,能够在任意其余应用程序中使用。
实用工具/原子 CSS 的限制
实用工具/原子 CSS 看起来颇有趣,但它们也带来了一些挑战。
人们一般手工编写实用工具/原子 CSS,精心制定命名约定。可是很难保证这个约定易于使用、保持一致性,并且不会随着时间的推移而变得臃肿。
这个 CSS 能够团队协做开发并保持一致性吗?它受巴士因子[5]的影响吗?
巴士系数是软件开发中关于软件专案成员之间讯息与能力集中、未被共享的衡量指标,也有些人称做“货车因子”、“卡车因子”(lottery factor/truck factor)。一个专案或计划至少失去若干关键成员的参与(“被巴士撞了”,指代职业和生活方式变更、婚育、意外伤亡等任意致使缺席的原因)即致使专案陷入混乱、瘫痪而没法存续时,这些成员的数量即为巴士系数。
你还须要预先开发好一个不错的实用工具/原子样式表,而后才能开始开发新功能。
若是实用工具/原子 CSS 是由别人制做的,你将不得不首先学习类命名约定(即便你知道 CSS 的一切)。这种约定是有主观性的,极可能你不喜欢它。
有时,你须要一些额外的 CSS,而实用工具/原子 CSS 并不提供这些 CSS。没有约定好的方法来提供这些一次性样式。
Tailwind 赶来支援
Tailwind 使用的方法是很是便捷的,而且解决了上述一些问题。
它经过 Utility-First 的理念来解决 CSS 的一些缺点,经过抽象出一组类名 -> 原子功能的集合,来避免你为每一个 div 都写一个专有的 class,而后整个网站重复写不少重复的样式。
传统卡片样式写法:
Tailwind 卡片样式写法:
它并非真的为全部网站提供一些惟一的实用工具 CSS,取而代之的是,它提供了一些公用的命名约定。经过一个配置文件[6],你能够为你的网站生成一套专属的实用工具 CSS。
ssh 注:这里原做者没有深刻介绍,为何说是一套命名约定呢而不是生成一些定死的 CSS 呢?
举几个例子让你们感觉一些,Tailwind 提供了一套强大的构建系统,好比默认状况下站长博客它提供了一些响应式的断点值:
// tailwind.config.js
module.exports = {
theme: {
screens: {
'sm': '640px',
// => @media (min-width: 640px) { ... }
'md': '768px',
// => @media (min-width: 768px) { ... }
'lg': '1024px',
// => @media (min-width: 1024px) { ... }
'xl': '1280px',
// => @media (min-width: 1280px) { ... }
}
}
}
你能够随时在配置文件中更改这些断点,好比你所须要的小屏幕 sm 可能指的是更小的 320px,那么你想要在小屏幕时候采用 flex 布局,仍是照常写 sm:flex,遵循一样的约定,只是这个 sm 已经被你修改为适合于项目需求的值了。
在好比说,Tailwind 里的 spacing 掌管了 margin、padding、width 等各个属性里的表明空间占用的值,默认是采用了 rem 单位,当你在配置里这样覆写后:
// tailwind.config.js
module.exports = {
theme: {
spacing: {
'1': '8px',
'2': '12px',
'3': '16px',
'4': '24px',
'5': '32px',
'6': '48px',
}
}
}
你再去写 h-6(height), m-2(margin), mb-4(margin-bottom),后面数字的意义就被你改变了。
也许从桌面端换到移动端项目,这个 6 表明的含义变成了 6rem,可是这套约定却深深的印在你的脑海里,成为你知识的一部分了。
Tailwind 的知识能够迁移到其余应用程序,即便它们使用的类名并不彻底相同。这让我想起了 React 的「一次学习,处处编写」理念。
我看到一些用户反馈说,Tailwind 提供的类名能覆盖他们 90% - 95% 的需求。这个覆盖面彷佛已经足够广了,并不须要常常写一次性的 CSS 了。
此时,你可能想知道为何要使用原子 CSS 而不是 Tailwind CSS?强制执行原子 CSS 规则的一个规则,一个类名,有什么好处?你最终会获得更大的 html 标签和更烦人的命名约定吗?Tailwind 已经有了足够多的原子类了啊。
那么,咱们是否应该放弃原子 CSS 的想法,而仅仅使用 Tailwind CSS?
Tailwind 是一个优秀的解决方案,但仍然有一些问题没有解决:
须要学习一套主观的命名约定
CSS 规则插入顺序仍然很重要
未使用的规则能够轻松删除吗?
咱们如何处理剩下的一次性样式?
与 Tailwind 相比,手写原子 CSS 可能不是最方便的。
和 CSS-in-JS 比较
CSS-in-JS 和实用工具/原子 CSS 有密切关系。这两种方法都提倡使用标签进行样式化。以某种方式试图模仿内联样式,这让它们有了不少类似的特性(好比在移动某些功能的时候更有信心)。
Christopher Chedeau[7] 一直致力于推广 React 生态系统中 CSS-in-JS 理念。在不少次演讲中,他都解释了 CSS 的问题:
全局命名空间
依赖
无用代码消除
代码压缩
共享常量
非肯定性(Non-Deterministic)解析
隔离
实用工具/原子 CSS 也解决了其中的一些问题,但也确实无法解决全部问题(特别是样式的非肯定性解析)。
若是它们有不少类似之处,那咱们可否同时使用它们呢?
探索原子 CSS-in-JS
原子 CSS-in-JS 能够被视为是“自动化的原子 CSS”:
你再也不须要建立一个 class 类名约定
通用样式和一次性样式的处理方式是同样的
可以提取页面所须要的的关键 CSS,并进行代码拆分
有机会修复 JS 中 CSS 规则插入顺序的问题
我想强调两个特定的解决方案,它们最近推进了两个大规模的原子 CSS-in-JS 的部署使用,来源于如下两个演讲。
React-Native-Web at Twitter (更多细节在Nicolas Gallagher 的演讲[8])。
Stylex at Facebook (更多细节在Frank Yan 的演讲[9])。
也能够看看这些库:
Styletron
Fela
Style-Sheet
cxs
otion
css-zero
ui-box
style9
stitches
catom
React-Native-Web
React-Native-Web 是一个很是有趣的库,让浏览器也能够渲染 React-Native 原语。不过咱们这里并不讨论跨平台开发(演讲里有更多细节)。
做为 web 开发人员,你只须要理解 React-Native-Web 是一个常规的 CSS-in-JS 库,它自带一些原始的 React 组件。全部你写 View 组件的地方,均可以用 div 替换。
React-Native-Web 的做者是 Nicolas Gallagher,他致力于开发 Twitter 移动版。他们逐渐把它部署到移动设备上,不太肯定具体时间,大概在 2017/2018 年左右。
从那之后,不少公司都在用它(美国职业足球大联盟、Flipkart、Uber、纽约时报……),但最重要的一次部署,则是由 Paul Armstrong 领导的团队在 2019 年推出的新的 Twitter 桌面应用。
Stylex
Stylex 是一个新的 CSS-in-JS 库,Facebook 团队为了 2020 年的 Facebook 应用重构而开发它。将来可能会开源,有可能用另外一个名字。
值得一提的是,React-Native-Web 的做者 Nicolas Gallagher 被 Facebook 招安。因此里面出现一些熟悉的概念一点也不奇怪。
个人全部信息都来自演讲 :),还须要等待更多的细节。
可扩展性
不出所料,在 Atomic CSS 的加成下,Twitter 和 Facebook 的 CSS体积都大幅减小了,如今它的增加遵循的是对数曲线。不过,简单的应用则会多了一些 初始体积。
Facebook 分享了具体数字:
旧的网站 仅仅首页就用了 413Kb 的 CSS
新的网站 整个站点只用了 74Kb,包括暗黑模式
源码和输出
这两个库的 API 看起来很类似,但也很难说,由于咱们对 Stylex 了解很少。
值得强调的是,React-Native-Web 会扩展 CSS 语法糖,好比 margin: 0,会被输出为 4 个方向的 margin 原子规则。
以一个组件为例,来看看旧版传统 CSS 和新版原子 CSS 输出的区别。
<Component1 classNames="class1" /> <Component2 classNames="class2" />
.class1 {
background-color: mediumseagreen;
cursor: default;
margin-left: 0px;
}
.class2 {
background-color: thistle;
cursor: default;
jusify-content: flex-start;
margin-left: 0px;
}
能够看出这两个样式中 cursor 和 margin-left 是如出一辙的,但它在输出中都会占体积。
再来看看原子 CSS 的输出:
<Component1 classNames="classA classC classD" />
<Component2 classNames="classA classB classD classE" />
class a {
cursor: default;
}
class b {
background-color: mediumseagreen;
}
class C {
background-color: thistle;
}
class D {
margin-left: 0px;
}
class E {
jusify-content: flex-start;
}
能够看出,虽然标签上的类名变多了,可是 CSS 的输出体积会随着功能的增多而减缓增加,由于出现过一次的 CSS Rule 就不会再重复出现了。
生产环境验证
咱们看看 Twitter 上的标签是什么样子的:
如今,让咱们来看看新 Facebook:
不少人可能会被吓到,可是其实它很好用,并且保持了 可访问性[10]。
在 Chrome 里检查样式可能有点难,但 devtools 里就看得很清楚了:
CSS 规则顺序
与手写的工具/原子 CSS 不一样,JS 库能让样式不依赖于 CSS 规则的插入顺序。
在规则冲突的状况下,生效的不是标签上 class attribute 中的最后一个类,而是样式表中最后插入的规则。
以这张图为例,咱们指望的是书写在后的 blue 类覆盖前面的类,但实际上 CSS 会以样式表中的顺序来决定优先级,最后咱们看到的是红色的文字。
在实际场景中,这些库避免在同一个元素上写入多个规则冲突的类。它们会确保标签上书写在最后的类名生效。其余的被覆盖的类名则被规律掉,甚至压根不会出如今 DOM 上。
const styles = pseudoLib.create({
red: {color: "red"},
blue: {color: "blue"},
});
// 只会输出 blue 相关的 CSS
<div style={[styles.red, styles.blue]}>
Always blue!
</div>
// 只会输出 red 相关的 CSS
<div style={[styles.blue, styles.red]}>
Always red!
</div>
注意:只有使用最严格的原子 CSS 库才能实现这种可预测的行为。
若是一个类里有多个 CSS 规则,而且只有其中的一个 CSS 规则被覆盖,那么 CSS-in-JS 库没办法进行相关的过滤,这也是原子 CSS 的优点之一。
若是一个类只有一个简单的 CSS 规则,如 margin: 0,而覆盖的是 marginTop: 10。像 margin: 0 这样的简写语法被扩展为 4 个不一样的原子类,这个库就能更加轻松的过滤掉不应出如今 DOM 上的类名。
仍然喜欢 Tailwind?
只要你熟悉全部的 Tailwind 命名约定,你就能够很高效的完成 UI 编写。一旦你熟悉了这个设定,就很难回到手写每一个 CSS 规则的时代了,就像你写 CSS-in-JS 那样。
没什么能阻止你在原子 CSS-in-JS 的框架上创建你本身的抽象 CSS 规则,Styled-system[11] 就能在 CSS-in-JS 库里完成一些相似的事情。它基于一些约定创造出一些原子规则,在 emotion 中使用它试试:
import styled from '@emotion/styled';
import { typography, space, color } from 'styled-system';
const Box = styled('div')(typography, space, color);
等效于:
<Box
fontSize={4}
fontWeight="bold"
p={3}
mb={[4, 5]}
color="white"
bg="primary"
Hello
</Box>
甚至有可能在 JS 里复用一些 Tailwind 的命名约定,若是你喜欢的话。
先看些 Tailwind 的代码:
<div className="absolute inset-0 p-4 bg-blue-500" />
咱们在谷歌上随便找一个方案,好比我刚刚发现 react-native-web-tailwindcss[12]:
import { t } from 'react-native-tailwindcss';
<View style={[t.absolute, t.inset0, t.p4, t.bgBlue500]} />;
就生产力的角度而言,并无太大的不一样。甚至能够用 TS 来避免错别字。
结论
这就是我要说的关于原子 CSS-in-JS 全部内容。
我历来没有在任何大型生产部署中使用过原子 CSS、原子 CSS-in-JS 或 Tailwind。我可能在某些方面是错的,请随时纠正我。
我以为在 React 生态系统中,原子 CSS-in-JS 是一个很是值得关注的趋势,我但愿你能从这篇文章中学到一些有用的东西。