freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Ant Design 的 CSS-in-JS 实现与最佳实践
2025-01-08 16:46:11
所属地 上海

CSS-in-JS优缺点

CSS-in-JS 是一种前端开发技术,它将 CSS 代码直接嵌入到 JavaScript 代码中,以解决传统 CSS 的一些局限性。

CSS-in-JS 最关键的特性就是「运行时」,这意味着使用 css-in-js编写的样式只有在应用运行时才会去解释并应用。

在 React 社区中,目前最流行的 css-in-js 库是 styled-components 和 Emotion。

「优势」

  1. 动态功能:通过 JavaScript 的动态性,可以在 CSS 中实现更多的动态效果,例如基于状态、条件、数据等动态调整样式。
  2. 作用域:没有全局样式冲突。CSS-in-JS 可以轻松地创建局部作用域,避免了全局作用域下样式的命名冲突和污染问题。
  3. 可移植性:将 CSS 代码直接嵌入到 JavaScript 中,使得组件的样式与组件本身紧密关联,方便组件的移植和复用。
  4. 代码维护:CSS-in-JS 可以使样式与组件逻辑紧密结合,降低了样式与组件之间的耦合度,提高了代码的可维护性和可读性。
  5. 自动前缀和优化:一些 CSS-in-JS 库可以自动处理浏览器前缀和样式优化,减少了开发者在这方面的工作量。

「缺点」

css-in-js最大的问题在于你编写的css样式类名经过模块化后是不一致的,每次在react的重渲染下,大量的props带来额外性能开销也是很明显的。

  1. css-in-js 增加了运行时开销

组件每次渲染时,css-in-js 库必须将样式「序列化」为可被插入到页面的 CSS 样式,显然这需要额外的 CPU 消耗,尤其是在 React 18 的并发模式下,会存在无法解决的性能问题。

React 核心团队成员、Hook 设计者 Sebastian Markbåge 在 React 18 工作组的这篇非常有价值的讨论中说道:

❝在并发渲染中,React 可以在渲染之间让出线程给浏览器。如果你在一个组件中插入新的 CSS,然后 React 让出线程,浏览器必须检查这些 CSS 是否适用于现有的树。所以它重新计算样式规则。然后 React 渲染下一个组件,然后这个组件发现新 CSS,那么这个过程会循环往复。❞

「总结来说,css-in-js」 「在运行时插入样式会阻塞 React 的渲染,进而拖慢整个页面的渲染速度,当组件频繁的渲染时就会出现明显的性能瓶颈。」

这个问题目前看来是无解的,因为运行时 css-in-js 库的工作方式就是组件渲染时插入新样式规则,这在根本上和性能是对立的。

  1. css-in-js 增加了包体积:

相比于原生 CSS 写法或者 CSS module方案来说,「css-in-js」 会引入而外的运行时代码(Emotion 是7.9 kB压缩后,styled-components 是12.7 kB)。

  1. 多个不同(甚至是相同)版本的 css-in-js 库同时加载时可能导致错误(example issue)。
  2. 不同 React 版本的 SSR,css-in-js 需要适配不同的实现(example issue)

自定义样式

在真实场景中,通常会出现对于组件样式的层级结构调整,比如修改了 ConfigProvider 的 prefixCls,导致原本的样式层级结构也发生了变化。这可能会导致之前定义的样式选择器失效,因为选择器中使用了固定的前缀,而实际应用的前缀已经发生了变化。

举个例子:

假设原本的 ConfigProvider 的 prefixCls 是 'ant',那么对应的按钮图标的类名可能是 ant-btn-icon。而后来,由于一些需求变更,我们将 ConfigProvider 的 prefixCls 修改为 'abc',那么按钮图标的类名就会变成 abc-btn-icon。

在这种情况下,如果我们之前的样式选择器是这样的:

.ant-btn-icon {
  color: red;
}

那么这个选择器将会失效,因为现在的类名已经不再是 ant-btn-icon,而是 abc-btn-icon。

为了解决这个问题,我们可以使用 CSS-in-JS 的方式,并结合 getPrefixCls 来动态生成类名,从而保证样式选择器能够正确匹配到对应的元素。示例如下:

import {
    type GenerateStyle,  // 导入 GenerateStyle 类型
    genComponentStyleHook,  // 导入 genComponentStyleHook 函数
    type FullToken,  // 导入 FullToken 泛型类型
} from 'antd/es/theme/internal';  // 从 antd 库中导入主题相关的模块

import {
    getAnimationBackground,
    getBackgroundAnimation,
    getBorderStyle,
} from './gradientUtil';

import { DOT_PREFIX } from '../constant';  // // DOT_PREFIX: .tech-theme

// ============================== Border ==============================

// 定义生成按钮边框样式的函数 genBorderStyle
const genBorderStyle: GenerateStyle<FullToken<'Button'>> = (token) => {
    const { componentCls, lineWidth } = token;  // 从 token 参数中解构出 componentCls 和 lineWidth

    const backgroundAnimation = getBackgroundAnimation(lineWidth);  // 调用 getBackgroundAnimation 函数获取背景动画相关的样式

    // 返回按钮样式的对象
    return {
        [`${componentCls}${DOT_PREFIX}`]: {  // 使用模板字符串设置类名
            // ======================= Primary =======================
            [`&${componentCls}-primary`]: {  // 定义主要样式的类名
                [`&:not(${componentCls}-dangerous)`]: {  // 排除危险状态的类名
                    ...getAnimationBackground(lineWidth),  // 获取动画背景的样式
                    ...backgroundAnimation,  // 应用背景动画的样式

                    '&:disabled': {  // 定义禁用状态的样式
                        opacity: token.opacityLoading,  // 设置不透明度
                        color: token.colorTextLightSolid,  // 设置文字颜色
                    },
                },
            },

            // ======================= Default =======================
            [`&${componentCls}-default`]: {  // 定义默认样式的类名
                [`&:not(${componentCls}-dangerous)`]: {  // 排除危险状态的类名
                    '&:before': getBorderStyle(lineWidth),  // 设置边框样式

                    '&:not(:disabled):hover': {  // 设置悬停状态的样式
                        color: token.colorTextLightSolid,  // 设置文字颜色
                    },

                    '&:disabled:before': {  // 设置禁用状态的边框样式
                        opacity: token.opacityLoading,  // 设置不透明度
                    },
                },
            },

            // ======================== Hover ========================
            [`&${componentCls}-primary, &${componentCls}-default`]: {  // 定义悬停状态的类名
                [`&:not(:disabled):not(${componentCls}-dangerous)`]: {  // 排除禁用状态和危险状态的类名
                    '&:hover': {  // 设置悬停状态的样式
                        filter: `brightness(120%)`,  // 使用滤镜增加亮度
                    },
                    '&:active': {  // 设置激活状态的样式
                        filter: `brightness(80%)`,  // 使用滤镜降低亮度
                    },
                },
            },
        },
    };
};

// ============================== Export ==============================

// 导出按钮样式的组件样式钩子
export default genComponentStyleHook(['Button', 'techTheme'], (token) => {
    return [genBorderStyle(token)];  // 返回按钮样式的数组
});

这样做可以确保我们的样式选择器与实际应用的类名保持一致,即使 prefixCls 发生了变化,样式也能够正确生效。

对需要继承 className 的场景,拓展也很容易:

function GeekProvider(props: { children?: React.ReactNode }) {
  // 使用 React 上下文获取 ConfigProvider 的配置信息,这里主要是获取按钮的配置信息
  const { button } = React.useContext(ConfigProvider.ConfigContext);

  // 使用自定义按钮样式钩子获取样式
  const { styles } = useButtonStyle();

  // 渲染 ConfigProvider 组件,并将样式应用到按钮
  return (
    <ConfigProvider button={{ className: classNames(button?.className, styles.btn) }}>
      {props.children} {/* 渲染子组件 */}
    </ConfigProvider>
  );
}

gradientUtil.ts

import { Keyframes } from '@ant-design/cssinjs';
import type { CSSObject } from '@ant-design/cssinjs';

// 定义一个线性渐变的背景颜色字符串
export const background = `linear-gradient(135deg, ${[
    '#f7797d',
    '#c471ed 35%',
    '#12c2e9',
].join(',')})`;

// 根据给定的线宽生成背景样式对象
export const getBackground = (lineWidth: number = 0) => ({
    background, // 背景颜色
    backgroundSize: `calc(100% + ${lineWidth * 2}px) calc(100% + ${
        lineWidth * 2
    }px)`, // 背景尺寸
    backgroundPosition: `-${lineWidth}px -${lineWidth}px`, // 背景位置
});

// 定义一个带动画的背景颜色字符串
export const animationBackground = `linear-gradient(-45deg, ${[
    '#f7797d',
    '#c471ed 24%',
    '#12c2e9 48%',
    '#12c2e9 52%',
    '#c471ed 76%',
    '#f7797d',
].join(',')})`;

// 根据给定的线宽生成带动画的背景样式对象,应该是动态流动的
export const getAnimationBackground = (lineWidth: number = 0): CSSObject => ({
    backgroundImage: animationBackground, // 背景图片
    backgroundSize: `calc(200% + ${lineWidth * 4}px) calc(200% + ${
        lineWidth * 4
    }px)`,
    backgroundPosition: `-${lineWidth}px -${lineWidth}px`,
});

// 定义边框遮罩样式数组
export const borderMask = [
    `linear-gradient(#fff 0 0) content-box`,
    `linear-gradient(#fff 0 0)`,
].join(',');

// 根据给定的线宽生成边框样式对象数组
export const getBackgroundAnimation = (lineWidth: number = 0) => {
    const rotateKeyframes = new Keyframes('gradient-rotate', {
        '50%': {
            backgroundPosition: `calc(100% + ${lineWidth}px) -${lineWidth}px`,
        },
    });

    const backgroundAnimation: CSSObject = {
        animationName: rotateKeyframes,
        animationDuration: '10s',
        animationIterationCount: 'infinite',
    };

    return backgroundAnimation;
};

// 根据给定的线宽生成边框样式对象数组
export const getBorderStyle = (lineWidth: number = 0): CSSObject[] => [
    {
        content: '""',  // 内容为空
        position: 'absolute',  // 绝对定位
        inset: -lineWidth,  // 边距。这个属性指定了元素相对于其包含块的位置。
        padding: lineWidth,  // 内边距。这个属性指定了元素的内边距,即元素内容与元素边框之间的距离。
        borderRadius: 'inherit',  // 继承父元素的边框半径
        background,  // 背景颜色
        zIndex: 1,  // 层级。zIndex 越大,元素在堆叠上下文中越靠上。
        transition: 'all 0.3s',  // 过渡动画。对所有属性应用过渡效果,0.3s 表示过渡时间为 0.3 秒。

        pointerEvents: 'none',  // 不接受鼠标事件。设置为 none 表示不接受鼠标事件,鼠标事件会穿透该元素并触发下方的元素。

        mask: borderMask,  // 设置遮罩
        maskComposite: `xor`,  // 设置遮罩模式。xor 表示将遮罩应用到元素上,但不影响元素的其他样式。

        WebkitMask: borderMask,  // Webkit 浏览器遮罩
        WebkitMaskComposite: 'exclude',  // Webkit 浏览器遮罩模式。exclude 表示将遮罩应用到元素上,但不影响元素的其他样式。
    },
    {
        WebkitMaskComposite: `xor`,  // Webkit 浏览器遮罩模式
    },
];

getBackground

...getBackground(1) //越小,颜色变化越多

getBorderStyle

  1. 伪元素模拟边框:
    • 在第一个对象中,我们设置了一个伪元素(通过 content: '""'),并将其绝对定位(position: 'absolute')到其包含块的内部。
    • 使用 inset 属性将伪元素向内偏移 -lineWidth,从而使其覆盖在原元素内部,形成了一个“内边框”的效果。
    • 我们为伪元素设置了与原元素相同的边框半径(borderRadius: 'inherit'),以确保边框与原元素的圆角一致。
    • 为伪元素设置了背景颜色(background),使其看起来像是原元素的边框。
    • 使用 mask 属性将一个遮罩应用到伪元素上,进一步控制其显示区域,以确保它不会溢出到原元素外部。
  1. 过渡动画和指针事件:
    • 我们为伪元素设置了过渡动画(transition: 'all 0.3s'),以使边框在发生变化时平滑过渡。
    • 使用 pointerEvents: 'none' 将伪元素设置为不接受鼠标事件,以确保鼠标事件可以穿透到原元素下方的其他元素上。
  1. 浏览器兼容性:
    • 为了兼容性,我们在第二个对象中为 Webkit 浏览器(如 Chrome、Safari)设置了相同的遮罩模式。
import React from 'react';
import {
    background,
    getBackground,
    getBorderStyle,
    getAnimationBackground
} from './config/styles/gradientUtil';

const ButtonComponent = () => {
    return (
        <div style={{
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            height: '100vh',
            backgroundColor: '',
        }}>
            <div
                style={{
                    ...getBorderStyle(3)[0],  // 使用边框样式函数,并取第一个样式对象
                    // 线宽越大,边框越宽
                    margin: 'auto',
                    width: '200px',  // 设置按钮宽度
                    height: '50px',  // 设置按钮高度
                }}
            >
                Click me
            </div>
        </div>
    );
};

export default ButtonComponent;

1736324889_677e37191dbaa2648c10b.png!small?1736324888650

Antd的css-in-js优缺点

Antd最大的特点是组件级别的缓存,那么hash最终的结果要受到组件名 + token + version 等,这么做很明显是为了props防止重渲染。

token

token是一个组件样式修改的变量

AliasToken:node_modules/antd/es/theme/interface/alias.d.ts

ComponentTokenMap:node_modules/antd/es/theme/interface/components.d.ts

最主要的是AliasToken和ComponentTokenMap这两个类型,一个对全局所有组件的混入修改,一个是对于组件的以及组件内部的混入修改。包括附带的一些前缀用于区分组件。所以token是可以控制优先级的一个样式配置对象。

Token 的组成

可以注意到上文中注入 CSS-in-JS 样式时,我们利用 genButtonStyle 方法返回了一个 CSSObject,这个 function 的唯一参数就是 token。 token 是一些变量的集合,其中就包括了上文中提到的 Design Token,在 antd 中我们对这些 Design Token 作了二次封装,产生了一系列 AliasToken,比如 token.padding 等等,我们可以利用 token 消费 Design Token。除了 Design Token 之外,当前组件中定义的 ComponentToken 也会存在于 token 中,同样作为设计资源消费。 为了辅助注入样式,我们还在 token 中提供了一些常用的变量:

{
  prefixCls: string;
  componentCls: string;
  iconCls: string;
  antCls: string;
};
  • prefixCls:即为调用 useStyle 时传入的 prefixCls。
  • componentCls:实现为 .${prefixCls} ,即做了 prefixCls 到 CSS 选择器的转化。
  • iconCls:值为 .anticon,是 ant icon 的固定 className。
  • antCls:值为 .ant,是 antd 的 className 前缀,常用于在组件中处理非当前组件的样式。

genComponentStyleHook 遍历生成组件样式对象的集合

// 导出一个默认的生成组件样式钩子的函数,传入组件名称和一个生成样式的回调函数
export default genComponentStyleHook('Collapse', (token) => {
  // 合并默认的 token 和传入的 token,生成 CollapseToken
  const collapseToken = mergeToken<CollapseToken>(token, {
    // 折叠内容的背景色
    collapseContentBg: token.colorBgContainer,
    // 折叠头部的背景色
    collapseHeaderBg: token.colorFillAlter,
    // 折叠头部的内边距
    collapseHeaderPadding: `${token.paddingSM}px ${token.padding}px`,
    // 折叠面板的边框半径
    collapsePanelBorderRadius: token.borderRadiusLG,
    // 折叠内容的水平内边距(固定值)
    collapseContentPaddingHorizontal: 16,
  });

  // 返回一个通过 JavaScript 对象描述的样式表
  return [
    // 生成基础样式
    genBaseStyle(collapseToken),
    // 生成无边框样式
    genBorderlessStyle(collapseToken),
    // 生成透明样式
    genGhostStyle(collapseToken),
    // 生成箭头样式
    genArrowStyle(collapseToken),
    // 生成折叠动画样式
    genCollapseMotion(collapseToken),
  ];
});

你会看到在每个组件的styles文件夹都会有一个genComponentStyleHook的api,第一个就是组件标识肯定会用来做属性的区分等,第二个是一个回调传入的token是一个样式配置对象,所以重点是这个token对象的样式配置,

这个token你可以理解为可以切换不同优先级的配置对象,可以覆盖原有的样式,你可以把它想象成一个主题变量,控制所有的组件对应的样式修改,也可以对单个组件的某个样式属性修改。

第二个参数回调其实是StyleFn,是控制整个组件主题的核心函数。 genBaseStyle 最终返回的是一个受token控制的js的样式对象,接下来去找genComponentStyleHook。

// 从 '@ant-design/cssinjs' 导入 useStyleRegister 函数
import { useStyleRegister } from '@ant-design/cssinjs';

//...
// 导出默认的生成组件样式钩子的函数,接受组件名称、样式生成函数和可选的默认 token 获取函数
export default function genComponentStyleHook<ComponentName extends OverrideComponent>(
  component: ComponentName,  // 组件名称
  styleFn: (token: FullToken<ComponentName>, info: StyleInfo<ComponentName>) => CSSInterpolation, // 样式生成函数
  getDefaultToken?:  // 可选的默认 token 获取函数
    | OverrideTokenWithoutDerivative[ComponentName]
    | ((token: GlobalToken) => OverrideTokenWithoutDerivative[ComponentName]),
) {
  // 注意!这里其实就是 useStyle 这个函数
  return (prefixCls: string): UseComponentStyleResult => {
    // 使用 useToken 获取主题、token 和 hashId
    const [theme, token, hashId] = useToken();
    // 从上下文获取配置
    const { getPrefixCls, iconPrefixCls } = useContext(ConfigContext);
    const rootPrefixCls = getPrefixCls();

    // 为 antd 组件中的所有 a 标签生成样式
    useStyleRegister({ theme, token, hashId, path: ['Shared', rootPrefixCls] }, () => [
      {
        // Link 样式
        '&': genLinkStyle(token),
      },
    ]);

    return [
      // 注册并生成组件样式
      useStyleRegister(
        { theme, token, hashId, path: [component, prefixCls, iconPrefixCls] },
        () => {
          // 统计 token 并获取代理 token 和刷新函数
          const { token: proxyToken, flush } = statisticToken(token);

          // 获取默认组件 token
          const defaultComponentToken =
            typeof getDefaultToken === 'function' ? getDefaultToken(proxyToken) : getDefaultToken;
          // 合并默认组件 token 和传入的 token
          const mergedComponentToken = { ...defaultComponentToken, ...token[component] };

          // 组件类名
          const componentCls = `.${prefixCls}`;
          // 合并 token
          const mergedToken = mergeToken<
            TokenWithCommonCls<GlobalTokenWithComponent<OverrideComponent>>
          >(
            proxyToken,
            {
              componentCls,
              prefixCls,
              iconCls: `.${iconPrefixCls}`,
              antCls: `.${rootPrefixCls}`,
            },
            mergedComponentToken,
          );

          // 生成样式插值
          const styleInterpolation = styleFn(mergedToken as unknown as FullToken<ComponentName>, {
            hashId,
            prefixCls,
            rootPrefixCls,
            iconPrefixCls,
            overrideComponentToken: token[component],
          });
          // 刷新组件 token
          flush(component, mergedComponentToken);
          // 返回通用样式和插值样式
          return [genCommonStyle(token, prefixCls), styleInterpolation];
        },
      ),
      hashId,  // 返回 hashId
    ];
  };
}

你会发现最核心的是useStyleRegister这个api返回的就是genComponentStyleHook,那么useStyle又返回就是warpssr,所以重点就是useStyleRegister,那么我们看看在@ant-design/cssinjs包里面useStyleRegister做了什么。

useStyleRegister 注册全局的样式表

/**
 * 注册全局的样式表
 */
export default function useStyleRegister(
  info: {
    theme: Theme<any, any>; // 主题
    token: any; // 样式 token
    path: string[]; // 样式路径
    hashId?: string; // 可选的哈希 ID
    layer?: string; // 可选的样式层
  },
  styleFn: () => CSSInterpolation, // 样式生成函数
) {
  const { token, path, hashId, layer } = info;
  const {
    autoClear, // 是否自动清除样式
    mock, // mock 环境
    defaultCache, // 默认缓存
    hashPriority, // 哈希优先级
    container, // 样式容器
    ssrInline, // SSR 内联样式
    transformers, // 样式转换器
    linters, // 样式检查器
  } = React.useContext(StyleContext); // 从 StyleContext 中获取配置
  const tokenKey = token._tokenKey as string; // 获取 token 的键

  // 注意这里的 fullPath 用于缓存查找的 key,已经做很细粒度的 path
  const fullPath = [tokenKey, ...path];

  // 根据环境判断是否要处理样式
  let isMergedClientSide = isClientSide;
  if (process.env.NODE_ENV !== 'production' && mock !== undefined) {
    isMergedClientSide = mock === 'client';
  }

  // 使用全局缓存获取样式字符串、token 键和样式 ID
  const [cachedStyleStr, cachedTokenKey, cachedStyleId] = useGlobalCache(
    'style',
    fullPath,
    // 创建缓存(如果需要)
    () => {
      const styleObj = styleFn(); // 生成样式对象
      const [parsedStyle, effectStyle] = parseStyle(styleObj, {
        hashId,
        hashPriority,
        layer,
        path: path.join('-'),
        transformers,
        linters,
      }); // 解析样式
      const styleStr = normalizeStyle(parsedStyle); // 规范化样式字符串
      const styleId = uniqueHash(fullPath, styleStr); // 生成唯一的样式 ID

      if (isMergedClientSide) {
        const style = updateCSS(styleStr, styleId, {
          mark: ATTR_MARK,
          prepend: 'queue',
          attachTo: container,
        }); // 更新 CSS

        (style as any)[CSS_IN_JS_INSTANCE] = CSS_IN_JS_INSTANCE_ID;

        // 用于 `useCacheToken` 批量删除 token 时使用
        style.setAttribute(ATTR_TOKEN, tokenKey);

        // 开发模式下,用于轻松找到哪个缓存路径生成了这个样式
        if (process.env.NODE_ENV !== 'production') {
          style.setAttribute(ATTR_DEV_CACHE_PATH, fullPath.join('|'));
        }

        // 注入客户端效果样式
        Object.keys(effectStyle).forEach((effectKey) => {
          if (!globalEffectStyleKeys.has(effectKey)) {
            globalEffectStyleKeys.add(effectKey);

            // 注入
            updateCSS(
              normalizeStyle(effectStyle[effectKey]),
              `_effect-${effectKey}`,
              {
                mark: ATTR_MARK,
                prepend: 'queue',
                attachTo: container,
              },
            );
          }
        });
      }

      return [styleStr, tokenKey, styleId]; // 返回样式字符串、token 键和样式 ID
    },
    // 如果不需要则移除缓存
    ([, , styleId], fromHMR) => {
      if ((fromHMR || autoClear) && isClientSide) {
        removeCSS(styleId, { mark: ATTR_MARK });
      }
    },
  );

  return (node: React.ReactElement) => {
    let styleNode: React.ReactElement;

    // 一个用于非 SSR 服务端渲染,一个用于客户端渲染,一个用于外部配置,如果不满足就返回空
    if (!ssrInline || isMergedClientSide || !defaultCache) {
      styleNode = <Empty />;
    } else {
      styleNode = (
        <style
          {...{
            [ATTR_TOKEN]: cachedTokenKey, // token 的缓存标识
            [ATTR_MARK]: cachedStyleId, // 样式的缓存标识
          }}
          dangerouslySetInnerHTML={{ __html: cachedStyleStr }} // 内联样式字符串
        />
      );
    }

    return (
      <>
        {styleNode}
        {node}
      </>
    ); // 返回包含样式节点和传入节点的片段
  };
}


首先这里通过useGlobalCache函数,传入对应的fullPath,然后执行传入的函数,会执行前面提到过的StyleFn这个函数,styleFn执行时组件本身的样式和被合并的token就被加载到一个StyleObj对象上了。

通过parseStyle函数传入的path、hashId、以及暴露在外的api最终解析出来的是一个内部key都被序列化的对象。将返回的cachedStyleStr, cachedTokenKey, cachedStyleId这三个缓存的值传入style这个标签。

在html中,style标签是使用来定义html文档的样式信息,在该标签中你可以规定浏览器怎样显示html文档内容。那么存入了对应的token缓存标识、样式的缓存标识、以及样式的字符串,最终被解析渲染,那么你会发现其实他的样式是运行时,同时也是组件级别的样式按需更新。

核心原理总结

首先在组件内的useStyle传入了warpSSR和hashID,执行genComponentStyleHook,最终返回useStyleRegister这个函数并传入styleFn,核心执行useGlobalCache函数,styleFn执行时组件本身的样式和被合并的token就被加载到一个StyleObj对象上了。通过parseStyle函数传入的path、hashId、以及暴露在外的api最终解析出来的是一个被序列化的对象,最终cachedStyleStr, cachedTokenKey, cachedStyleId渲染到style标签上,这样可以让组件本身具备了更细粒度的包体积和性能。

CSS in JS 迁移指南

这里以 Button 组件为例

1. 注释 components/button/style/index.less 文件,确保该组件的所有样式都已移除。
2. 用以下代码替换 components/button/style/index.tsx,实际请替换 Button 为相应的组件名:

// deps-lint-skip-all
import { genComponentStyleHook } from '../../_util/theme';
import type { FullToken } from '../../_util/theme';

/** Component only token. Which will handle additional calculation of alias token */
export interface ComponentToken {
  // Component token here
}

interface ButtonToken extends FullToken<'Button'> {
  // Custom token here
}

// ============================== Export ==============================
export default genComponentStyleHook(
  'Button',
  token => [
    // Gen-style functions here
  ],
);


3. 同时在 components/_util/theme/interface.ts 中的 interface OverrideToken 添加相应组件的 ComponentToken:

// ...
import type { ComponentToken as ButtonComponentToken } from '../../button/style';

export interface OverrideToken {
  derivative?: Partial<DerivativeToken & AliasToken>;
  
  // Customize component
  Button?: ButtonComponentToken;
}
// ...


4. 在组件中引入 components/button/style/index.tsx 导出的 hook,该 hook 要求传入参数是该组件的 prefixCls,返回一个数组,第一个元素是一个 wrapper,要求包裹最终 return 的元素;第二个元素则是上文中提到过的 hashId,我们需要将它作为 className 添加给该组件的最外层元素:

// ...
+ import useStyle from './style';

const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (props, ref) => {
  // ...
  const prefixCls = getPrefixCls('btn', customizePrefixCls);
  
+ // Style
+ const [wrapSSR, hashId] = useStyle(prefixCls);
  
  //...
  const classes = classNames(
    prefixCls,
+   hashId,
    {
      // ...
    },
    className,
  );
  
  let buttonNode = (
    // ...
  );

- return buttonNode;
+ return wrapSSR(buttonNode);
};


5. 到这一步为止, CSS-in-JS 的链路已经打通,我们可以试着为组件添加一些样式:

// deps-lint-skip-all
import { genComponentStyleHook } from '../../_util/theme';
import type { FullToken, GenerateStyle } from '../../_util/theme';

/** Component only token. Which will handle additional calculation of alias token */
export interface ComponentToken {
  // Component token here
}

interface ButtonToken extends FullToken<'Button'> {
  // Custom token here
}

const genButtonStyle: GenerateStyle<ButtonToken> = token => {
  const { componentCls } = token;
  
  return {
    [componentCls]: {
      border: '1px solid red',
    }
  };
};

// ============================== Export ==============================
export default genComponentStyleHook(
  'Button',
  token => [
    // Gen-style functions here
    genButtonStyle(token),
  ],
);

运行项目,打开 Button 页面,可以看见样式已经生效。

文章参考

# web
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录