前言:
目前朋友们对“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条件编译