译:内联样式加强版

原文: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-felaemotion 等库提供的 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,我们可能会遇到两个主要问题:

  1. 伪元素
    不幸的是,这个技巧不适用于伪元素,如 ::before::after::placeholder。唯一的变通方法是增加一些额外的 CSS 来实现这些效果。
    话虽如此,我几乎从来不需要用到伪元素,即使用到,大多数情况下也只是用于设计系统相关的事情,比如给复选框或单选按钮设置样式。

  2. 后代选择器
    它支持选择器,但它们可能不会像你期望的那样工作。
    所有选择器最终都会定位到接收样式对象的元素上。如果我们想根据元素的父元素有条件地对其进行样式设置,我们必须将样式直接传递给元素本身,例如使用 .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 版本的免责声明,以及一个关于潜在性能影响的注释。