大家好,很高兴又见面了,我是"web 前端分享",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发! 随着 Facebook 和 Twitter 最近的生产部署,我认为一种新趋势正在慢慢发展:原子 CSS-in-JS。 在这篇文章中,我们将了解原子 CSS 是什么,它与功能性/实用性优先的 CSS(如 TailwindCSS)有何关系,以及那些大玩家正在其现代 React 代码库中采用它。 1.什么是原子 CSS?1.1 什么是 BEM? BEM 分别代表着:Block(块)、Element(元素)、Modifier(修饰符),是一种组件化的 CSS 命名方法和规范,由俄罗斯 Yandex 团队所提出。其目的是将用户界面划分成独立的(模)块,使开发更为简单和快速,利于团队协作开发。 BEM 的命名模式在社区中有着不同方式,以下为 Yandex 团队所提出的命名规则为: .[Block 块]__[Element 元素]_[Modifier 修饰符] 当然任何一种规范,都是基于实际需求而定,便于团队开发和维护扩展,每个规范都是经过合理评估后所得出的一种"思路"和"建议"。比如 BEM 的下面例子(具体说明可阅读注释): 关于 BEM 的更多内容可继续阅读文末资料。 1.2 什么是 OOCSS? OOP 已在 JavaScript 和后端语言中得到广泛使用,不止是 JS 才会有 OOP 面向对象模式,CSS 中的 OO 写法到现在已经不是一个新概念(2008 年由 Nicole Sullivan 提出,目标是通过应用 Java 和 Ruby 等变成语言普及的面向对象设计原则,使得动态 CSS 更易于管理)。 OOCSS,是 Object Oriented css 的缩写,旨在用最简单的方式编写 CSS 代码,从而提高代码重用性、可维护性、可扩展性。 OOCSS 追求组件的复用,尽量不使用继承选择符,并且 class 命名比较抽象,一般不体现具体内容 。以下面 HTML 为例:![]()
这里是文本 下面是 CSS 内容: media { padding: 10px; } .media:after { display: table; clear: both; content: " "; } .media-image-container { float: left; margin-right: 10px; } .media-image { display: block; } .media-body { overflow: hidden; } /* 主题样式 */ .media-shadow { box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5); } 1.2.1 分离结构和主题( Separate structure and skin) 分离结构和主题是在于将一些视觉样式效果(例如 background、color)作为单独的"主题"来应用。在上面的例子中的阴影效果,没有被直接写在 media 的样式规则内,而是被单独写在了一个名为 media-shadow 的 class 中。因此,它成为了可选择、可拆分的主题。如果不需要对应主题,什么也不要加,如果需要,加上对应的 class 即可。 1.2.2 容器与内容分离 分离容器和内容要求使页面元素不依赖于其所处位置。在上面的例子中,css 的选择符都很短,无继承选择符例如: .header .media { } 所以,这个图文排列的元件,可以在任何地方使用,且会有一致的外观。如果需要在特定的地方让这个元件看起来不一样,继续为这个元件增加 class,将"不一样的部分"作为可配置的选项,即元件的外观仍不依赖其所处位置。 1.3 什么是实用性优先(utility-first )? 上面介绍了各种 CSS 方法论,例如 BEM、OOCSS 等等。最近 Tailwind CSS 及其实用优先的概念流行了起来,其与 Functional CSS 和 Tachyon 很接近。比如下面的例子:
ChitChat
You have a new message! 通过在 HTML 中使用各种 class ,而非为一个 class 添加复杂 css 属性来实现这种功能,比如下面的.chat-notificatio 类就很复杂: .chat-notification { display: flex; max-width: 24rem; margin: 0 auto; padding: 1.5rem; border-radius: 0.5rem; background-color: #fff; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); } 而本文关注的原子 CSS 就像实用优先 CSS 的极端版本,所有 CSS 类都有一个唯一的 CSS 规则。Atomic CSS 最早由 Thierry Koblentz (Yahoo!) 在 2013 年的 Challenging CSS Best Practices 中使用。比如下面的例子: /* 原子CSS */ .bw-2x { border-width: 2px; } .bss { border-style: solid; } .sans { font-style: sans-serif; } .p-1x { padding: 10px; } /* 非原子CSS、因为这个class有2条规则 */ .p-1x-sans { padding: 10px; font-style: sans-serif; } 使用原子 CSS 也许会带来结构层、表示层的耦合,比如当需要更改按钮颜色时,需要修改 HTML 而不是 CSS。 这种紧密耦合的关系在现代 CSS-in-JS 的 React 代码库中也变的可以接受,但似乎与现存的"关注点分离"的主流思是相违背的。 因为使用简单的类选择器,所以"关注点分离"也不再是什么大的问题,它会带来很多不一样的东西: 随着添加新功能,样式表增长得更少 移动 HTML 会同时变更样式 删除功能时能确保同时删除相关样式 只是 Html 有点臃肿,这可能是服务器呈现的 Web 应用程序的一个问题,但是类名中的高冗余使用 gzip 可以很好地压缩,就像它适用于以前在 CSS 文件中找到的重复 css 规则一样。 您不需要为每种情况都使用原子 CSS,只需使用最常见的样式模式即可。一旦原子 CSS 准备就绪,它就不会改变或增长太多,而且可以很容易地缓存它(例如,可以将它附加到 vendor.css,并期望它不会在应用重新部署时失效)并用到其他地方。 2.原子 CSS 有没有限制? 原子 CSS 看起来很有趣,但它们也会有一些问题。人们通常手工编写原子 CSS,精心制定命名约定,但很难确保约定易于使用并保持一致,而且不会随着时间的推移而变得臃肿,特别在多人协作、大量使用的场景下。 在能够使用它的功能之前,您还需要预先处理一个原子样式表。如果 原子 CSS 是由其他人制作的,则您必须先学习类命名约定(即使您对 CSS 了如指掌),这是很多人不能接受的。同时,如果你要的样式原子 CSS 未提供,则您需要自行实现。 3.Tailwind 怎么样? Tailwind 的做法非常方便,其确实也解决了其中的一些问题,但它也并没有真正为所有网站提供唯一的 CSS 文件。 相反,它仅提供共享范围和命名约定(shared scope and naming conventions), 从配置文件,它允许您生成自定义的 CSS。 // 默认情况下,Tailwind 将在项目的根目录中查找可选的 tailwind.config.js 文件,您可以在其中定义任何自定义项 module.exports = { content: ["./src/**/*.{html,js}"], theme: { colors: { blue: "#1fb6ff", purple: "#7e5bef", pink: "#ff49db", orange: "#ff7849", green: "#13ce66", yellow: "#ffc82c", "gray-dark": "#273444", gray: "#8492a6", "gray-light": "#d3dce6", }, fontFamily: { sans: ["Graphik", "sans-serif"], serif: ["Merriweather", "serif"], }, extend: { spacing: { "8xl": "96rem", "9xl": "128rem", }, borderRadius: { "4xl": "2rem", }, }, }, }; 与 postCSS 的插件相结合生成最终代码: module.exports = { plugins: { tailwindcss: { config: "./tailwindcss-config.js" }, }, }; 对于 Tailwind 的知识可以应用到其他应用程序,即它们不使用相同的类名,这让我想起了 React 的"Learn once, write anywhere"的理念。 相关报告显示 Tailwind 可以满足 90% 到 95% 的需求,看起来已经足够了,再也不需要经常编写一次性使用的 CSS 样式。此时您可能会问,为什么使用原子 CSS 而不是 Tailwind? 在执行 1 个规则,1 个类的原子 CSS 规则时,你会得到什么? 更大的 html 页面、不太方便的命名约定? 但是无论如何,Tailwind 已经有很多原子类可以直接使用了,这使得使用 Tailwind 很方便! 那么,我们是否应该放弃原子 CSS 的想法,而简单地使用 Tailwind?虽然 Tailwind 是一个很好的解决方案,但仍有一些问题没有解决: 需要学习一个命名约定 CSS 规则插入顺序仍然很重要 无法轻松删除未使用的规则 未处理剩余的一次性样式 与 Tailwind 相比,手写原子 CSS 可能不是最方便的。 4.与 CSS-in-JS 的比较 CSS-in-JS 和实用/原子 CSS 存在一定的关联。 这两种方法都提倡从标记中设置样式,以某种方式尝试模拟高性能的内联样式,这为它们提供了许多相似的属性。 Christopher Chedeau 极大地帮助在 React 生态系统中传播了 CSS-in-JS 的理念。 在多次演讲中,他解释了 CSS 的问题: 原子 CSS 也解决了其中一些问题,但绝对不是全部(特别是样式的非确定性解析)。 如果它们有相似之处,我们不能一起使用它们吗? 4.原子 CSS-in-JS Atomic CSS-in-JS 可以看作是"自动原子 CSS": 您无需创建类名约定 普通和一次性样式的处理方式相同 能够提取页面的关键 CSS 并进行代码拆分 提供修复 JS 中 CSS 规则插入顺序问题的机会 目前并非所有的 CSS-in-JS 库都支持原子 CSS , 其本身依赖于 CSS-in-JS 库的具体细节。下面是 2 个大规模原子 CSS-in-JS 部署示例: Twitter 上的 React-Native-Web Facebook 上的 Stylex 你也可以使用下面的库: Styletron Fela Style-Sheet cxs otion css-zero ui-box style9 stitches catom compiled 5.React-Native-Web React-Native-Web 是一个非常有趣的库:它允许在网络上呈现 React-Native 原语(React-Native primitives), 这里不过多谈论跨平台的移动/网络开发。 作为 Web 开发人员,您只需要了解 React-Native-Web 是一个常规的 CSS-in-JS 库,其由 Nicolas Gallagher 创建,它带有一小组原始 React 组件。 无论你在哪里看到 View,都可以在脑海中用 p 替换它。 6.Stylex Stylex 是 Facebook 在 2020 年 重构时开发的新 CSS-in-JS 库。 他们计划有一天将其开源,可能会使用不同的名称,值得一提的是,React-Native-Web 的作者 Nicolas Gallagher 于 2 年前被 Facebook 聘用。 看到它的一些概念被 Facebook 重用也就不足为奇了。与 React-Native-Web 不同,Stylex 似乎并不专注于跨平台开发。 7.可扩展性(Scalability) 正如原子 CSS 所预期的那样,在 Twitter 和 Facebook 的应用上都看到了 CSS 的大量减少,而且遵循对数曲线。不过,简单的应用程序可能会有一定的时间、学习成本。 Facebook 分享了具体数字: 旧网站仅用于着陆页就有 413Kb 的 CSS 新站点整个站点为 74Kb,包括暗黑模式 8.原子 CSS 的输入和编译输出 React-Native-Web、Stylex 这 2 个库具有相似且非常简单的 API,值得强调的是,React-Native-Web 将扩展 CSS shortand 语法,如 margin: 0。 9.生产环境验证(Production inspection) 一起看看 Twitter 站点的 HTML 结构: 再看看 Facebook 的页面结构: 多人看到这个可能会被吓到,但它确实有效,站点也没有任何问题。 在 Chrome 检查器中检查样式可能有点困难,但 devtools 可以提供帮助,解决这些问题。 10.CSS 规则顺序 与手写原子 CSS 不同,JS 库能够使样式不依赖于 CSS 规则插入顺序。 您可能知道,在规则冲突的情况下,获胜的不是类属性的最后一个类,而是样式表中最后插入的规则。 这些特异性问题可以仅通过使用简单的基于类的选择器来解决。 实际上,这些库避免在同一元素上输出具有冲突规则的类。它们确保 HTML 中声明的最后一个样式始终具有更高的优先级。 而"被重写的类"被过滤掉了,甚至没有进入 DOM。 const styles = pseudoLib.create({ red: {color: "red"}, blue: {color: "blue"}, }); // That p only will have a single atomic class (not 2!), for the blue color Always blue! // That p only will have a single atomic class (not 2!), for the red color Always red! 如果一个类有多个规则,并且只有其中一个被覆盖,CSS-in-JS 库将无法在不删除未被覆盖的规则的情况下过滤该类。如果一个类有一个单一的速记规则,如 margin: 0,而重写是 marginTop: 10,这是同样的问题。 像 margin: 0 这样的简写语法被扩展到 4 个不同的类,并且该库能够更细粒度地过滤不应出现在 DOM 中的重写类。 11.您是否仍然喜欢 Tailwind? 了解所有 Tailwind 命名约定后,您就可以非常快速地编写 UI 代码, 像在 CSS-in-JS 中那样手动编写每个 CSS 的方式反而感觉效率较低。 如果你觉得它会带来效率提升的话,可以在原子 CSS-in-JS 框架之上构建自己的抽象,甚至可以在 JS 中重用 Tailwind 的命名约定。让我们来看一些 Tailwind 代码: 而如果采用 react-native-web-tailwindcss 的方案代码如下: import { t } from "react-native-tailwindcss";
; 或者使用其他颜色: import { color } from "react-native-tailwindcss"; ; 就生产力而言,这并没有太大的不同,除了后者可以让你避免使用 TypeScript 的拼写错误。 12.结论 以上就是我要说的关于原子 CSS-in-JS 的所有内容,因为未在任何大型生产环境中部署、使用过原子 CSS、原子 CSS-in-JS 或 Tailwind。 如果文章有任何错误,可以直接在评论区留言。但是我认为原子 CSS-in-JS 是 React 生态系统中值得关注的趋势。 参考资料 https://www.bluesdream.com/blog/css-bem-interpretation.html https://sebastienlorber.com/atomic-css-in-js https://tailwindcss.com/docs/utility-first https://news.68idc.cn/makewebs/css/20210923705881.html https://github.com/TVke/react-native-tailwindcss https://tailwindcss.com/docs/configuration https://juejin.cn/post/6917073600474415117