likes
comments
collection
share

🎨 从 Ant Design 5.0 中学习 CSS-in-JS

作者站长头像
站长
· 阅读数 53

2022年11月 Ant Design 5.0 正式发布! 这篇文章主要关注 Ant Design 5.0 在主题上的变化。

  1. 全新 Design Token 模型,代替 v4.x 中大量的 less 变量。

在 v4 版本中,我们提出更多的 less 变量以支持主题定制能力。然而除了色板支持完全的派生能力外,其他如字体、行高、间距都没有对应的算法。在 v5 中,我们改造了所有的 Token,使其基于 Seed + Algorithm 可以派生出所有的 Design Token。

  1. 采用 CSS-in-JS,更好地支持动态主题,弃用 less。底层使用 @ant-design/cssinjs 作为解决方案。

在 v4 时期,CSS Variables 停留在了动态主题色而没有进一步提供暗色、其他 Token 的动态能力。在 v5 中改用了 CSS-in-JS 不需要维护中间变量,但是有更多的运行时消耗,在经过一系列尝试后,我们研发了一套针对组件级别的 CSS-in-JS 库 @ant-design/cssinjs

有了 CSS-in-JS 的加持后,动态主题的能力也得到了加强,包括但不限于:

  1. 支持动态切换主题;
  2. 支持同时存在多个主题;
  3. 支持针对某个/某些组件修改主题变量;
  4. ...

Design Token 模型

什么是 Design Token ?

“Token”原本的意思是“令牌”,在工程逻辑中用于用户身份与服务器端进行验证,而在设计体系中,Design Token 则可以简单理解为封装的视觉样式参数。它是通过规定样式参数,并通过一套符合设计师、工程师理解的统一的命名规则,为这些样式参数的定义名称。

🎨 从 Ant Design 5.0 中学习 CSS-in-JS

参考

Design Token 应用指南——设计篇

什么是 Design Token ,以及大厂是如何应用的

Ant Design 5 中的 Design Token

在 5.0 版本中我们把影响主题的最小元素称为 Design Token。通过修改 Design Token,我们可以呈现出各种各样的主题或者组件。

在 Design Token 中我们提供了一套更加贴合设计的三层结构,将 Design Token 拆解为 Seed Token、Map Token 和 Alias Token 三部分。这三组 Token 并不是简单的分组,而是一个三层的派生关系,由 Seed Token 派生 Map Token,再由 Map Token 派生 Alias Token。

Seed Token 基础变量 意味着所有设计意图的起源。比如 colorPrimary 表示主题色。

Map Token 梯度变量 是基于 Seed 派生的梯度变量,通过 theme.algorithm 算法计算得来,以保证 Map Token 之间的梯度关系。比如 colorPrimaryBg

Alias Token 别名变量 用于批量控制某些共性组件的样式,基本上是 Map Token 别名,或者特殊处理过的 Map Token。比如 controlOutline colorLink

🎨 从 Ant Design 5.0 中学习 CSS-in-JS

在大部分情况下,使用 Seed Token 就可以满足定制主题的需要。但如果您需要更高程度的主题定制,您需要了解 antd 中 Design Token 的生命周期。

🎨 从 Ant Design 5.0 中学习 CSS-in-JS

Ant Design 5 定制主题

自定义主题算法

antd 的主题算法就是 基于基础变量和派生规则来生成一组变量。5.0 版本中默认提供三套预设算法,分别是默认算法 theme.defaultAlgorithm、暗色算法 theme.darkAlgorithm 和紧凑算法 theme.compactAlgorithm。可以根据不同的主题算法,进而获得不同的主题风格效果。 🎨 从 Ant Design 5.0 中学习 CSS-in-JS

局部主题自定义

自定义主题算法是一个全局风格的调整,在 v4 及以前,想要 antd 的组件有多套主题同屏的实现难度是非常高的,通常需要自定义 className 覆盖 antd 的默认样式来实现。在 V5 中,得益于 CSSinJS 的动态主题,多套主题模式同屏展示就变得非常简单,只需要更改一个主题算法,就可以在不修改 antd 样式的情况下实现多种主题的兼容,极大提高了主题的可定制化。

<>
  <ConfigProvider theme={{ algorithm: theme.darkAlgorithm}}>
    <Button type="primary">
      darkAlgorithm button
    </Button>
  </ConfigProvider>
  <ConfigProvider theme={{ algorithm: theme.defaultAlgorithm}}>
    <Button type="primary">
      defaultAlgorithm button
    </Button>
  </ConfigProvider>
</>

🎨 从 Ant Design 5.0 中学习 CSS-in-JS

组件级风格自定义

当你想为一个弹窗组件定制主题样式,在 v4 及以前需要写很多自定义样式覆盖 antd 的原始样式,这可能导致写很多的样式来实现弹窗样式的定制。在 V5 中,使用 ConfigProvider 可以非常 通过设置 theme.component 完成样式的自定义。

const { token } = theme.useToken();

<ConfigProvider prefixCls="custom"
  theme={{ components: {
    Popover: {
      colorBgElevated: token.colorPrimary,
      colorText: token.colorTextLightSolid,
    },
    Button: {
      colorPrimary: token['blue-7'],
    },
  }}}>
  <Popover
    content={
      <>
        <p>popover text</p>
        <Button type="primary">popover content</Button>
      </>
    }
    trigger="click"
  >
    test popover custom theme
  </Popover>
  <Checkbox>green theme checkbox</Checkbox>
  <Spin />
</ConfigProvider>

🎨 从 Ant Design 5.0 中学习 CSS-in-JS

Ant Design 5 源码分析

在 adtd 5.0 中的 [wrapSSR, hashId]。其中 wrapSSR 是个函数,传入react的元素节点并混入对应 stylehashId 用于标识序列化后的 style 标签的属性

const [wrapSSR, hashId] = useStyle(prefixCls);
return wrapSSR(componentNode);

每个组件的 style 文件夹下都存在API genComponentStyleHook,第一个参数表示组件标识,最终用于 style 标签的属性;第二个参数是一个回调函数 StyleFn,入参 token 是样式配置对象,相当于主题变量StyleFn 经过 genSizeSmallButtonStyle genSizeSmallButtonStyle……一系列计算,最终返回由 token 控制的样式对象,是控制整个组件主题的核心函数。

genComponentStyleHook 实际上是由 @ant-design/cssinjs 的核心API useStyleRegister 返回的。

import { genComponentStyleHook } from '../../theme/internal';

export default genComponentStyleHook('Button', (token) => {
  const { controlTmpOutline, paddingContentHorizontal } = token;

  const buttonToken = mergeToken<ButtonToken>(token, {
    colorOutlineDefault: controlTmpOutline,
    buttonPaddingHorizontal: paddingContentHorizontal,
  });

  return [
    // Shared
    genSharedButtonStyle(buttonToken),

    // Size
    genSizeSmallButtonStyle(buttonToken),
    genSizeBaseButtonStyle(buttonToken),
    genSizeLargeButtonStyle(buttonToken),

    // Block
    genBlockButtonStyle(buttonToken),

    // Group (type, ghost, danger, disabled, loading)
    genTypeButtonStyle(buttonToken),

    // Button Group
    genGroupStyle(buttonToken),

    // Space Compact
    genCompactItemStyle(token, { focus: false }),
    genCompactItemVerticalStyle(token),
  ];
});

主题变量 buttonToken 的格式如下👇,包含了可以描述主题样式的变量,交由

🎨 从 Ant Design 5.0 中学习 CSS-in-JS

例如 genSharedButtonStyle(buttonToken) 返回的样式对象格式如下👇,由 @ant-design/cssinjs 将样式对象注册为样式表。

🎨 从 Ant Design 5.0 中学习 CSS-in-JS

token 是一个组件的主题变量,主要包括 AliasToken 所有组件的混入样式配置项; ComponentTokenMap 组件内部混入的样式配置项。例如: colorPrimary: '#fa8a38'为所有组件混入的配置项,colorPrimary: '#00b96b'Button 组件内部混入的配置项。

<>
  <ConfigProvider prefixCls="custom"
    theme={{ algorithm: undefined, token: {
      colorPrimary: '#fa8a38', // 橙色
    }, components: {
      Button: {
        colorPrimary: '#00b96b', // 绿色
      },
    }}}>
    <Button size="small" type="primary">
      gray theme button
    </Button>
    <Spin />
  </ConfigProvider>
  <Button ghost type="link">
    default theme button
  </Button>
</>

🎨 从 Ant Design 5.0 中学习 CSS-in-JS

主题编辑器

CSS-in-JS

什么是 CSS-in-JS ?

React 等框架的出现使前端开发转向组件思想,把 HTMLCSSJavaScript 混合在一起,关注点在组件,而不是把这三剑客分开。无论是 HTML 还是 CSS 都需要通过 JavaScript 来控制,这种用 JavaScript 来处理样式的思想就是 CSS-in-JS

一句话总结 CSS-in-JS:用 JavaScript 书写 CSS

关于 CSS-in-JS 的争议

在 antd v4 版本中需要通过将 --color 等变量挂载到 :root 的方式,进行动态主题配置。v5 版本的介绍中,通过Design Token 模型可以派生出几乎所有需要定制的样式。利用 CSS-in-JS 不需要在 :root 上挂载变量简化了主题相关的配置。

优点:

1.Locally-scoped styles 当单纯写 css 的时候,很容易会对意想不到的其他组件造成污染。例如我们常见列表的 css 会这样写:

.row {
  padding: 0.5rem;
  border: 1px solid #ddd;
}

将来再有其他组件使用了className="row"就会出现内边距和边框,我们需要用其他className来避免,CSS-in-JS 就可以通过 Locally-scoped styles 来完全解决这个问题,内边距和边框的样式永远不会影响到其他组件。

<div className={css`
    padding: 0.5rem;
    border: 1px solid #ddd;
`}>
    ...row item...
</div>

CSS Modules 也提供了 Locally-scoped styles

2. Colocation: React 中开发组件的时候样式文件需要单独写在 .css 文件中,更好的组织代码的方式可能是将相关的代码文件放在同个地方。这种做法称为「共置」。使用 CSS-in-JS 可以直接在 React 组件内部写样式,可维护性将大大提升。

CSS Modules 也提供了 Colocation

3. 在样式中使用 JavaScript 变量:  CSS-in-JS 提供了在样式中使用 JavaScript 变量的能力。

缺点:

  1. CSS-in-JS 的运行时问题。当你的组件进行渲染的时候,CSS-in-JS 库会在运行时将样式代码 ”序列化” 为可以插入文档的 CSS 。会消耗浏览器更多的 CPU 性能。

  2. CSS-in-JS 让你的包体积更大了。 访问页面时都需要加载关于 CSS-in-JS 的 JavaScript。Emotion 的包体积压缩之后是 7.9k ,而 styled-components 则是 12.7 kB 。虽然这些包都不算是特别大,但是如果再加上 react & react-dom 的话,那也是不小的开销。

  3. CSS-in-JS 让 React DevTools 变得难看。 每一个使用 css prop 的 react 元素, Emotion 都会渲染成 <EmotionCssPropInternal> 和 <Insertion> 组件。如果你使用很多的 css prop,那么你会在 React DevTools 看到下面这样的场景。

🎨 从 Ant Design 5.0 中学习 CSS-in-JS

  1. 频繁的插入 CSS 会使浏览器做更多的工作。
  2. 使用 CSS-in-JS ,会有更大的概率导致项目报错。

@ant-design/cssinjs

// 默认样式
const genDefaultButtonStyle = (
    prefixCls: string,
    token: DerivativeToken,
): CSSInterpolation => [{
    [`.${prefixCls}`]: {
        borderColor: token.borderColor,
        borderWidth: token.borderWidth,
        borderRadius: token.borderRadius,

        cursor: 'pointer',

        transition: 'background 0.3s',
    },
},{
    [`.${prefixCls}`]: {
        backgroundColor: token.componentBackgroundColor,
        color: token.textColor,

        '&:hover': {
            borderColor: token.primaryColor,
            color: token.primaryColor,
        },
    },
}];
const wrapSSR = useStyleRegister(
    { theme, token, hashId, path: [prefixCls] },
    () => [
        genDefaultButtonStyle('ant-btn-default', token),
    ],
);
return wrapSSR(
    <button
        className={classNames(prefixCls, typeCls, hashId, className)}
        {...restProps}
    />,
);

编译后的结果:

<style data-rc-order="prependQueue" data-css-hash="ext5be" data-token-hash="4ztxvs" data-dev-cache-path="4ztxvs|ant-btn">
.ant-btn-default {
    border-color: black;
    border-width: 1px;
    border-radius: 2px;
    cursor: pointer;
    transition: background 0.3s;
}
.ant-btn-default {
    background-color: #FFFFFF;
    color: #333333;
}
.ant-btn-default:hover {
    border-color: #1890ff;
    color: #1890ff;
}
...
</style>
<button class="ant-btn ant-btn-default">Default</button>

有哪些流行的 CSS-in-JS 框架?

React 社区最流行的 CSS-in-JS 方案。

@rmotion

@emotion/css 为例

npm install @emotion/css --save
import { css } from '@emotion/css'

const color = 'white'

render(
  <div
    className={css`
      padding: 32px;
      background-color: hotpink;
      font-size: 24px;
      border-radius: 4px;
      &:hover {
        color: ${color};
      }
    `}
  >
    Hover to change color.
  </div>
)

编译结果为

<div class="css-1h3ivcl">Hover to change color.</div>

<style>
.css-1h3ivcl {
    padding: 32px;
    background-color: hotpink;
    font-size: 24px;
    border-radius: 4px
}
.css-1h3ivcl:hover {
    color: white
}
</style>

styled-componentsEmotion 的原理和使用方式类似,都是根据一定算法给标签生成唯一的 hash 作为 className,将样式作用在这个className上,生成一段 <style>...</style> 注入到 <head> 中。

// @/components/Card.js
import styled from 'vue-styled-components'

const color = '#BF4F74;';

const Title = styled.h1<{ bg?: string}>`
    color: ${color};
    border-radius: 12px;
    background: ${props => props.bg}
`;

return (
    <Title bg="#ccc">abc</Title>
);

编译结果为

<h1 bg="#ccc" class="sc-kPNBzt jzTqXL">abc</h1>

<style>
.jzTqXL{
    color:#BF4F74;
    border-radius:12px;
    background:#ccc;
}
</style>
转载自:https://juejin.cn/post/7257708221360111675
评论
请登录