原文:https://weser.io/blog/inline-styles-on-steroids
作者:Robin
译者:ChatGPT 4 O
编者注:由于 RSC 的出现,现有 CSS in JS 方案大多不再兼容,包括作者曾创建的 Fela 方案。作者询求一些新的解法,目前选中的是 css-hooks,基于 css 变量的小技巧可以在内联样式中实现伪类、媒体查询等高级用法。但是,缺点是存在潜在的性能问题。
几乎 8 年前,我发表了我的第一篇文章 Style as a Function of State,介绍了 Fela 的第一个稳定版本。那时候对我来说非常激动。React 生态系统变化迅速,每个月都有新的样式解决方案发布,许多聪明的人贡献了许多创意,最终导致 Fela 发展到今天的模样。
在过去的 8 年里,Fela 是我解决样式问题的工具,我仍然很高兴地在许多网站和应用上使用它。然而,自那时以来,React 发生了很大的变化,不幸的是,它的发展方向并不真正兼容 Fela 的设计。
问题
注意:我不想详细探讨这些问题,但如果你感到好奇,请联系我,我可能会写一篇额外的文章来详细说明这些问题。
更具体地说,新的以服务器优先的架构与 React Server Components (RSC) 结合流式渲染,对 Fela 几乎是一个死刑。事实上,我们并不是唯一一个面临这个问题的库。许多运行时 CSS-in-JS 库都在努力添加对 RSC 的支持。
我不能替其他库发言,但在 Fela 的情况下,这几乎是无法解决的。选择输出 Atomic CSS 伴随着对通用缓存的重度依赖,以确保结果确定性,从而防止 Flash of Unstyled Content (FOUC) 以及错位的即刻加载。
重要:这并不意味着你完全不能使用 Fela 或者你必须立即重构你的 Fela 应用,但如果你真的想使用 RSC 和流式渲染,我建议寻找一个替代方案。我将在本文中也会介绍一个潜在的替代方案。
重新开始
好了,重新开始,寻找一个适用于现代 React 的样式解决方案。自从发布 Fela 的第一个版本以来,样式空间发生了很多变化。早在 2016 年,没有 styled-components,没有静态 CSS 抽取,甚至 Tailwind 那时也闻所未闻。
我有很多选择,但不得不承认,我已经相当习惯 Fela,并且不太喜欢大多数解决方案。是的,我也不是 Tailwind 的大粉丝。并不是说我看不到它的优点,只是我不喜欢它以字符串为中心的使用体验和编写体验。
所以,与其评估和比较库,我想从头开始思考我理想的解决方案会是什么样子。
长话短说,我最后又开始考虑内联样式。如果它们支持伪类、媒体查询和容器查询之类的东西呢?我可能根本不会写 Fela。结果发现,我不是唯一这样想的人。
根据来自 CSSWG 的 Lea Verou 的消息,原生浏览器支持已经在计划中并且会在某个时候推出,但实现起来并不简单。
在那之前,我们无法在内联样式中获得合适的 CSS 特性。或者?
CSS Hooks
如果我告诉你,其实这是可能的呢?只需要一个巧妙的 CSS 变量小技巧。这是一个名为 css-hooks (来自 Nick Saunders)。
它允许我们使用伪类和媒体查询等原生 CSS 特性与标准内联样式。它是框架无关的,并且可以与包括 React 和 Solid 在内的不同库一起工作。运行时是最小的,主要通过转换样式对象来支持这些特性,从而增强编写体验。
它不会生成 CSS,也没有涉及编译器。而且,由于它是纯内联样式,它在流媒体和 RSC 中表现出色。
CSS 变量技巧
Nick 写了一篇很棒的文章,解释了创建 css-hooks 的思考过程以及其内部工作原理。
简而言之,CSS 变量规范允许设置一个回退值,当变量未设置时使用,例如 --var(--my-variable, red)
。如果变量设置为 initial
,则使用回退值;如果设置为空值,则使用该值,这对声明没有影响。
这种行为用于实现伪类和媒体查询。
* {
--hover-off: initial;
--hover-on: ;
}
:hover {
--hover-off: ;
--hover-on: initial;
}
<div style="color: var(--hover-on, red) var(--hover-off, blue)">Hover me</div>
要点
免责声明:本文使用的是旧版本 1.x.x
,API 略有变化,但有一个方便的助手可以实现无缝迁移。
设置 css-hooks 很简单。我们只需传递一个包含所有要启用的钩子的对象。它返回一个包含静态 CSS 字符串和一个类似于 react-fela、emotion 等库提供的 css
助手的元组。
import { createHooks } from '@css-hooks/react'
// stylesheet 是一个静态 CSS 字符串
// 需要在根处插入
const [stylesheet, css] = createHooks({
// 还有 @css-hooks/recommended,帮助创建钩子
':hover': ':hover',
':active': ':active',
})
const style = css({
color: 'red',
':hover': {
color: 'blue',
},
})
缺点与陷阱
当然,没有一种解决方案是没有缺点的。对于 css-hooks,我们可能会遇到两个主要问题:
-
伪元素:
不幸的是,这个技巧不适用于伪元素,如::before
、::after
或::placeholder
。唯一的变通方法是增加一些额外的 CSS 来实现这些效果。
话虽如此,我几乎从来不需要用到伪元素,即使用到,大多数情况下也只是用于设计系统相关的事情,比如给复选框或单选按钮设置样式。 -
后代选择器:
它支持选择器,但它们可能不会像你期望的那样工作。
所有选择器最终都会定位到接收样式对象的元素上。如果我们想根据元素的父元素有条件地对其进行样式设置,我们必须将样式直接传递给元素本身,例如使用.special-parent &
来代替从父元素中定位子元素使用> .child
。
这可能一开始会让人困惑,但我很快就习惯了,有时甚至有助于隔离地思考组件。
性能:你可能会好奇为什么我没有提到性能。我们被告知内联样式很慢,那为什么我们还要考虑再次使用内联样式?
Daniel Nagy 写了两篇 很棒的文章 关于它的文章!
编辑:感谢 一条推文,我了解到在基于 Chromium 的浏览器中,CSS 变量可能存在潜在的性能问题。希望这个问题能尽快得到解决。
桥接 Fela Ergonomics
虽然我对使用 css-hooks 重写网站印象深刻并且非常享受,但我很快开始怀念过去 8 年来习惯的 Fela 的一些便利功能。
因此,我开始桥接一些功能,最终创建了一个小工具包,扩展了 css-hooks 的核心,增加了插件、关键帧和主题支持等功能。
引入 Brandeur
遵循我的命名惯例,我将其命名为 brandeur。
感谢 css-hooks 提供的简单 API,我实际上能够支持大多数现有的Fela 插件。
import { createHooks } from 'brandeur'
import responsiveValue from 'brandeur-plugin-responsive-values'
import customProperty from 'fela-plugin-custom-property'
const breakpoints = {
'@media (min-width: 480px)': '@media (min-width: 480px)',
'@media (min-width: 1024px)': '@media (min-width: 1024px)',
}
const theme = {
colors: {
foreground: {
primary: 'red',
},
},
}
const marginX = (value) => ({
marginLeft: value,
marginRight: value,
})
// brandeur 是 css-hooks 的超集,镜像其 API
const [stylesheet, css] = createHooks({
hooks: {
':hover': ':hover',
':active': ':active',
...breakpoints,
},
plugins: [
responsiveValue(Object.keys(breakpoints)),
customProperty({ marginX }),
],
keyframes: {
fadeIn: {
from: { opacity: 0 },
to: { opacity: 1 },
},
},
})
const style = css(({ theme, keyframes }) => ({
color: 'blue',
animationName: keyframes.fadeIn,
fontSize: [20, , 16],
marginX: 4,
':hover': {
color: theme.colors.foreground.primary,
},
}))
原始组件
此外,我还创建了tehlu,它进一步扩展了 brandeur,构建了一个提供一组有用的原始布局组件的系统,在启动项目时非常有用。
注意:Tehlu 仍在开发中,可能会引入重大变化。
import { createSystem, createBox, createText } from 'tehlu'
const system = createSystem({
// ... brandeur 配置
baselineGrid: 4,
typography: {
body: {
fontFamily: 'Inter',
fontSize: 16,
},
title: {
fontFamily: 'Futura',
fontSize: 26,
},
},
})
const Text = createText(system)
const Box = createBox(system)
const App = (
<Box paddingInline={4} gap={2} justifyContent="center">
<Text variant="title">Hello World</Text>
<Text variant="body">Welcome to my page</Text>
</Box>
)
结论
事实证明,内联样式比我们想象的更强大。在创建和使用 CSS-in-JS 解决方案超过 8 年后,我很高兴回到了我开始的地方。
我非常享受这段旅程,并且非常感激我的开源贡献为我带来的一切。Fela 是、曾经是并且可能永远是我最大的非工作相关项目,我为它的成就感到自豪。
同时,看到生态系统随着像 CSS 变量技巧这样巧妙的新解决方案而进化,这真是太棒了。我很高兴能够通过仅使用内联样式来简化我的技术栈。
我的网站现在完全由内联样式驱动,我迫不及待地想要在未来使用原生浏览器支持来替代 css-hooks!
变更
2024年5月24日:添加了一个关于示例中使用的 css-hooks 版本的免责声明,以及一个关于潜在性能影响的注释。