龙空技术网

2023 年的尽头是编译时 CSS-in-JS 方案么?

高级前端进阶 1086

前言:

目前朋友们对“css条件编译”可能比较重视,朋友们都想要分析一些“css条件编译”的相关知识。那么小编也在网络上收集了一些对于“css条件编译””的相关内容,希望同学们能喜欢,咱们一起来了解一下吧!

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

最近,Emotion 排名第二的维护者 Sam 所在公司弃用了 CSS-in-JS 方案,引起了不小的讨论。这也是我第一次开始重点关注 CSS-in-JS,我甚至在头条开了一个合集重点讨论 CSS-in-JS 的方案,下面是已经发表的关于 CSS-in-JS 的文章:

《 2023 年的尽头是编译时 CSS-in-JS 方案么?》《 CSS vs. CSS-in-JS:2023 年你应该如何选择?》《 2023 年最受欢迎的 10 大 CSS-in-JS 库!》《 我们为何选择弃用 css-in-js ? 》

我希望通过系列文章的方式带着大家深入的了解 CSS-in-JS,包括它的优势、缺点、编译时运行时的不同等等,最终让大家对写下的每一行代码都持有足够的信心。话不多说,直接开始进入正题!

1. 运行时 CSS-in-JS 的困境

以前单独写过介绍 CSS-in-JS 的文章,重点论述过 CSS-in-JS 方案的不足,概括起来可以通过以下几个点描述。

1.1 运行时开销和异常

CSS-in-JS 增加了运行时开销。 当组件渲染时,CSS-in-JS 库必须将样式“序列化”为可以插入到文档中的纯 CSS。很明显,这会占用额外的 CPU 周期,从而对应用程序的性能产生影响。

当然,更高效的方法是将样式移到组件外部,以便序列化在模块加载时发生一次,而不是在每次渲染时发生。比如:使用来自 @emotion/react 的 css 函数来做到这一点。

import { jsx, css, Global, ClassNames } from '@emotion/react';const myCss = css({  backgroundColor: 'blue',  width: 100,  height: 100,});function MyComponent() {  return <div css={myCss} />;}

React 核心成员和 React Hooks 的原始设计师 Sebastian Markbåge 在 React 18 工作组中就 CSS-in-JS 展开了丰富的讨论。最终指出:在 React 并发渲染中,CSS-in-JS 导致在 React 渲染时针对所有 DOM 节点的每一帧需要重新计算所有 CSS 规则,最终显著拖慢渲染速度。

总之,运行时 CSS-in-JS 库通过在组件渲染时插入新的样式规则来工作,这从根本上就不利于性能。同时,使用 CSS-in-JS 还有很多可能出错的地方,尤其是在使用 SSR 和/或组件库时。

1.2 额外包体积大小

CSS-in-JS 增加了包大小,每个访问网站的用户都必须下载 CSS-in-JS 库的 JavaScript。 Emotion 压缩和 Gzip (MINIFIED + GZIPPED)后为 7.9 kB,styled-components 为 12.7 kB。

虽然这两个库体积都不是很大,但当所有外部依赖加在一起结果可能发生变化。 比如:react + react-dom 的体积是 44.5 kB 。

1.3 CSS-in-JS 导致 React DevTools 混乱

对于使用 css prop 的每个元素,Emotion 将渲染 和 组件。 如果在许多元素上使用 css prop,Emotion 的内部组件会使 React DevTools 变得混乱,如下所示:

因为组件层级的混乱,对开发者调试带来极大的挑战,这也是 CSS-in-JS 一个比较严重的用户体验问题。

1.4 学习曲线与缓存

如果开发者从未使用过 Web components 或基于组件的框架会有一定的学习成本,这包括:语法、组件化等全新思路。

同时 CSS-in-JS 也无法使用 CSS 缓存,因为没有维护单独的 CSS 文件。

2.编译时 CSS-in-JS 的崛起

2023 年,前端领域看到越来越多的 CSS-in-JS 库在编译时将样式转换为纯 CSS。 典型的库包括:

CompiledVanilla ExtractLinariaastroturfstyle9

这些库旨在提供与运行时 CSS-in-JS 类似的好处,但是没有多余的性能成本。接下来带着大家一起看看这些编译时 CSS-in-JS 库的用法、特点等。

2.1 Compiled

Compiled 是一个由 Atlassian Labs 创建的 React 编译时 CSS-in-JS 库,旨在提供出色的开发人员体验而无需运行时成本。 Compiled 的工作原理是在构建时静态分析代码,将其转换为编译组件,然后在运行时将样式代码移动到文档的头部。

图片来源:

Compiled 利用了在 styled-components 和 Emotion 中发现的样式处理灵感,因此如果开发者使用过其中任何一个,那么对 Compiled 都很容易上手。目前 Compiled 在 Github 上有超过 1.9k 的 star,是一个值得关注的前端开源项目。

下面是的 Compiled 简单示例:

import { styled, ClassNames } from '@compiled/react';// Tie styles to an element<div css={{ color: 'purple' }} />;// Create a component that ties styles to an elementconst StyledButton = styled.button`  color: ${(props) => props.color};`;// Use a component where styles are not necessarily tied to an element<ClassNames>  {({ css }) => children({ className: css({ fontSize: 12 }) })}</ClassNames>;

可以在 Webpack、Babel、Parcel 中使用 Compiled,但是需要预先安装它:

npm install @compiled/webpack-loader --save-dev// webpacknpm install @compiled/parcel-config --save-dev// parcelnpm install @compiled/babel-plugin --save-dev// babel

打开提取(extraction)开关,所有在应用程序中设置样式并通过 NPM 获取的组件都将剥离其运行时并将样式提取到原子样式表中。比如下面的示例:

-import { CC, CS } from '@compiled/react/runtime';--const _2 = '._syaz1q9v{color: hotpink}';-const _ = '._1wybfyhu{font-size: 48px}';-export const LargeHotPinkText = () => (-  <CC>-   <CS>{[_, _2]}</CS>    <span className="_syaz1q9v _1wybfyhu">Hello world</span>-  </CC>);

下面是抽离的原子样式内容:

._1wybfyhu {  font-size: 48px;}._syaz1q9v {  color: hotpink;}
2.2 Vanilla Extract

Vanilla Extract 是 TypeScript 中的零运行时样式表。其使用局部范围的类名和 CSS 变量在 TypeScript(或 JavaScript)中编写样式,然后在构建时生成静态 CSS 文件。

图片来自:

总体上看,Vanilla Extract 是“TypeScript 中的 CSS 模块”,但具有作用域的 CSS 变量(scoped CSS Variables)和堆栈。其具有以下明显特征:

在构建时生成的所有样式——就像 Sass、Less 等。✨ 对标准 CSS 的最小抽象。适用于任何前端框架,或者无框架场景局部范围的类名,就像 CSS 模块一样。局部范围的 CSS 变量、@keyframes 和 @font-face 规则。支持同步主题的高级主题系统,而且没有全局变量!用于生成基于变量的计算表达式的实用程序。通过 CSSType 的类型安全样式。‍♂️ 用于开发和测试的可选运行时版本。用于动态运行时主题的可选 API。

vanilla-extract 零运行时特性很有用,它允许开发者编写样式表并在构建时将它们编译成静态 CSS 文件。 还提供了类型安全和局部作用域样式的诸多好处。

// styles.css.tsimport { createTheme, style } from '@vanilla-extract/css';export const [themeClass, vars] = createTheme({  color: {    brand: 'blue',  },  font: {    body: 'arial',  },});export const exampleStyle = style({  backgroundColor: vars.color.brand,  fontFamily: vars.font.body,  color: 'white',  padding: 10,});

vanilla-extract 还有一个突出特点是它对主题的支持。 开发者可以创建一个全局主题或多个主题,所有主题都具有类型安全的令牌合约,这使得自定义项目的外观变得容易。

Vanilla-extract 的另一个优势是它与框架无关,它具有流行框架和打包器(如 webpack、esbuild、Vite 和 Next.js)的官方集成。总的来说,Vanilla-extract 是一个很优秀的库,适合任何希望利用 TypeScript 的附加优势大规模编写可维护的 CSS 的人。

在 Github 上,vanilla-extract 已经有超过 8k 的 star 和 0.2k 的 fork,有超过 18.6 k 的项目使用 ,是妥妥的前端明星项目。

2.3 Linaria

Linaria 是一个典型的 CSS-in-JS 库,允许开发人员使用 JavaScript 编写 CSS,并在构建期间将样式提取到 CSS 文件中,还提供了动态样式和熟悉的 CSS 语法以及类似 Sass 的嵌套的诸多好处。

除了作为一个零运行时库之外,Linaria 还通过 React 绑定支持基于属性的动态样式。 这利用了幕后的 CSS 变量,使得基于组件 props 的应用样式变得毫不费力。

Linaria 由两个主要部分组成:Babel 插件和打包器集成。 Babel 插件负责在代码中查找 css 和 style 标签,提取 CSS 并将其返回到文件的元数据中, 它还将根据文件名哈希生成唯一的类名。

import { css } from '@linaria/core';import { modularScale, hiDPI } from 'polished';import fonts from './fonts';//在 `css` 标签中写下你的样式const header = css`  text-transform: uppercase;  font-family: ${fonts.heading};  font-size: ${modularScale(2)};  ${hiDPI(1.5)} {    font-size: ${modularScale(2.5)};  }`;//然后用它作为类名<h1 className={header}>Hello world</h1>;

使用 styled 标签时,动态插值将替换为 CSS 自定义属性, 对作用域中常量的引用也将被内联。 如果多次使用相同的表达式,插件将为这些创建单个 CSS 自定义属性。

import { styled } from '@linaria/react';import { families, sizes } from './fonts';// Write your styles in `styled` tagconst Title = styled.h1`  font-family: ${families.serif};`;const Container = styled.div`  font-size: ${sizes.medium}px;  color: ${(props) => props.color};  border: 1px solid red;  &:hover {    border-color: blue;  }  ${Title} {    margin-bottom: 24px;  }`;// Then use the resulting component<Container color="#333">  <Title>Hello world</Title></Container>;

在 Github 上,Linaria 已经有超过 10.4k 的 star 和 0.42k 的 fork,每周的平均下载量达到了 276k。目前有超过 1.5 k 的项目使用 Linaria、项目贡献人数 130+,是妥妥的前端明星项目。

2.4 astroturf

astroturf 允许开发者在 JavaScript 文件中编写 CSS,而无需添加任何运行时层,并使用现有的 CSS 处理管道。拆开来看,主要包括以下几个点:

astroturf 是零运行时 CSS-in-JS, 获得许多与 CSS-in-JS 相同的好处,但不会损失需要特定于框架的 CSS 处理的灵活性,同时保持 CSS 完全静态,无需运行时样式解析。使用现有的工具,如:Sass、PostCSS、Less 等,但仍然在 JavaScript 文件中编写样式定义单文件单组件, 在模板文字中编写 CSS,然后像在单独的文件中一样使用它

总之,利用 编译时的魔力,astroturf 让开发者可以轻松地从 JavaScript(或 TypeScript)文件中定义样式,而且框架可选。

import { stylesheet } from 'astroturf';const height = 2;const styles = stylesheet`  .btn {    appearance: none;    height: ${height}rem;    display: inline-block;    padding: .5rem 1rem;  }  .primary {    color: white:    border: 1px solid white;    background-color: taupe;    &:hover {      color: taupe:      border-color: taupe;      background-color: white;    }  }`;const Button = ({ primary }) => {  const button = document.createElement('button');  button.classList.add(styles.btn, primary && styles.primary);  return button;};

通过 astroturf 专有的“提取过程”,每个样式表都变成了自己的 CSS 文件。 对于那些喜欢模块化方法的人来说,css 标签已经准备就绪,正在等待。 css 标签创建单个 CSS 类:

import React from 'react';import { css } from 'astroturf';const btn = css`  color: black;  border: 1px solid black;  background-color: white;`;export default function Button({ children }) {  return <button className={btn}>{children}</button>;}

处理后,css 块将被提取到 .css 文件中,利用配置为处理 css 的所有其他加载程序。同时,astroturf 为开发者提供了与 React.JS 的内置集成。

import * as React from 'react';import { css } from 'astroturf';function Button({ children, ...props }) {  return (    <button      {...props}      css={css`        color: blue;        border: 1px solid blue;        padding: 0 1rem;      `}    >      {children}    </button>  );}

className 的 props 会自动与提供的 css 结合,无需额外处理。

在 Github 上,astroturf 已经有超过 2.2k 的 star,超过 0.86 k 的项目使用 astroturf,是一个值得长期关注的编译时 CSS-in-JS 方案。

2.5 style9

style9 是受到了 Facebook 的 stylex 启发的 CSS-in-JS 编译器,具有接近零的运行开销、支持原子 CSS 提取和 TypeScript 。比如下面的代码示例:

import style9 from 'style9';const styles = style9.create({  blue: {    color: 'blue',  },  red: {    color: 'red',  },});document.body.className = styles('blue', isRed && 'red');

编译器将生成以下输出:

/* JavaScript */document.body.className = isRed ? 'cRCRUH ' : 'hxxstI ';/* CSS */.hxxstI { color: blue }.cRCRUH { color: red }

要充分发挥 style9 的能力可以考虑与 Webpack 打包方案集成。以下是将样式提取到 CSS 文件所需的最低限度 Webpack 设置。

const Style9Plugin = require('style9/webpack');const MiniCssExtractPlugin = require('mini-css-extract-plugin');module.exports = {  // Collect all styles in a single file - required  optimization: {    splitChunks: {      cacheGroups: {        styles: {          name: 'styles',          type: 'css/mini-extract',          // For webpack@4 remove type and uncomment the line below          // test: /\.css$/,          chunks: 'all',          enforce: true,        },      },    },  },  module: {    rules: [      {        test: /\.(tsx|ts|js|mjs|jsx)$/,        use: Style9Plugin.loader,      },      {        test: /\.css$/i,        use: [MiniCssExtractPlugin.loader, 'css-loader'],      },    ],  },  plugins: [new Style9Plugin(), new MiniCssExtractPlugin()],};

style9 还支持诸多高级特性,比如:

条件样式多个声明中组合样式伪选择器媒体查询KeyframesREM 中的字体大小共享样式自动前缀主题配置等

比如下面示例展示了如何使用 style9 的样式组合能力,通过将 style9 作为具有生成的样式对象的属性的函数来调用。这不受与使用样式功能相同的限制,并且可以是完全动态的。

import style9 from 'style9';const someStyles = style9.create({  blue: {    color: 'blue',  },});const someOtherStyles = style9.create({  tilt: {    transform: 'rotate(45deg)',  },});document.body.className = style9(someStyles.blue, someOtherStyles['ti' + 'lt']);

在 Github 上,相对于其他上述的编译时 CSS-in-JS 库,style9 虽然只有 0.6k 的 star,但是是一个值得长期关注的编译时 CSS-in-JS 方案。

3.宇宙的尽头是编译时 CSS-in-JS 么

虽然在实际项目中,我没有使用任何编译时 CSS-in-JS 库,但与 Sass 模块相比,我仍然认为它们有缺点。

以下是在查看 Compiled 库时看到的一些缺点:

当组件第一次挂载时,样式仍然被插入,迫使浏览器重新计算每个 DOM 节点样式。动态样式无法在构建时提取,因此 Compiled 使用 style 属性(也称为内联样式)将值添加为 CSS 变量。 众所周知,当应用许多元素时,内联样式会导致性能不佳。Compiled 仍然将样板组件插入到 React 树中, React DevTools 依然混乱,就像运行时 CSS-in-JS 一样。

与任何技术一样,编译时 CSS-in-JS 也有其优点和缺点。 作为开发人员,需要评估这些优缺点,然后就该技术是否适合用例做出明智的决定。 后面我也会单独出文介绍编译时 CSS-in-JS 的替代方案,欢迎大家持续关注。

参考资料

封面图:来自 Dmitry Nozhenko 的《9 Ways To Implement CSS in React JS》

标签: #css条件编译