Antd Button 源码分析(三)

395 阅读8分钟

AliasToken(别名变量) 的生成过程

Alias Token 主要用来在生成组件样式时,提供样式值。它可以看成是主题与用户 Token 整个后的产物。

Alias Token 的生成主要是在 useToken 函数中进行处理,因此从 useToken 函数开始分析。

有些概念和函数前面两篇中提到的,会本篇中则会滤过。

Token 的处理

这里简单介绍一下对 Token 的处理过程:

                Theme                   OverrideToken
               派生算法                  覆盖/扩充属性
  DesignToken --------> DerivativeToken -----------> AliasToken 

DerivativeToken

就是前面Antd Button 源码分析(一)中的 MapToken(梯度变量)。它可以看成是主题的实例。

Override Token

它表示用户配置的 Token,是这对默认主题的修改。

可通过如下配置 Token。

    <ConfigProvider theme={{
        colorPrimary: '#00b96b',
        borderRadius: 2,}}>
      <Button>click</Button>
    </ConfigProvider>

之后可通过 DesignTokenContext 中的 override 获取。

  const {
    override,
  } = React.useContext(DesignTokenContext);

useToken 流程分析

主要流程:

  1. 调用 useCacheToken 生成 Alias Token 并缓存。如果开启 css 变量,会在组件挂载时,插入仅包含css 变量声明的 css 类选择器。
  2. 返回主题和 Alias Token 等。

一般 DesignTokenContext 中配置的 token 与 override.overrride 相同。

node_modules/antd/es/theme/useToken.js

export default function useToken() {
  const {
    token: rootDesignToken,
    hashed,  // 默认为 true
    theme,
    override,
    cssVar
  } = React.useContext(DesignTokenContext);
  // 如 5.12.5-true
  const salt = `${version}-${hashed || ''}`;
  // 如果没有在 DesignTokenContext 中配置 theme,
  // 则使用默认提供的主题
  const mergedTheme = theme || defaultTheme;
  // 没有配置 DesignTokenContext 的情况下 defaultSeedToken 和 rootDesignToken 相同,
  // 否则 rootDesignToken 会先覆盖 defaultSeedToken 中的样式。
  const [token, hashId, realToken] = useCacheToken(mergedTheme, [defaultSeedToken, rootDesignToken], {
    salt,
    override,
    getComputedToken,
    // formatToken will not be consumed after 1.15.0 with getComputedToken.
    // But token will break if @ant-design/cssinjs is under 1.15.0 without it
    formatToken,
    cssVar: cssVar && {
      prefix: cssVar.prefix, // 作为变量的前缀 如:--ant-
      key: cssVar.key,
      unitless,  // 在它里的变量,不需要单位。
      ignore,  // 在它里的变量,不进行处理,直接过滤掉。
      preserve // 在它里面的变量不需要转换为 css 变量
    }
  });
  return [mergedTheme, realToken, hashed ? hashId : '', token, cssVar];
}

DesignTokenContext 默认初始化

DesignTokenContext 支持对主题和 Token 进行配置。如果要对 token 中使用的值改为 css 变量方式需要配置 cssVar 属性。
DesignTokenContext 默认仅配置如下。 node_modules/antd/es/theme/context.js

// To ensure snapshot stable. We disable hashed in test env.
export const defaultConfig = {
  token: defaultSeedToken,
  override: {
    override: defaultSeedToken // override 用来覆盖 token 中的样式。
  },
  hashed: true
};
export const DesignTokenContext = /*#__PURE__*/React.createContext(defaultConfig);

seedToken 的初始化

上面 DesignTokenContext 中的 defaultSeedToken 就是 seedToken。 seedToken 会作为基础变量,用来根据主题进行派生。
node_modules/antd/es/theme/themes/seed.js

export const defaultPresetColors = {
  blue: '#1677ff',
  purple: '#722ED1',
  cyan: '#13C2C2',
  green: '#52C41A',
  magenta: '#EB2F96',
  ...
};
const seedToken = Object.assign(Object.assign({}, defaultPresetColors), {
  // Color
  colorPrimary: '#1677ff',
  colorSuccess: '#52c41a',
  colorWarning: '#faad14',
  colorError: '#ff4d4f',
  colorInfo: '#1677ff',
  colorLink: '',
  colorTextBase: '',
  colorBgBase: '',
  // Font
  fontFamily: `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji'`,
...
};

默认主题初始化

没有在 DesignTokenContext 中配置主题的情况下会使用默认主题。
下面看默认主题的初始化。

export const defaultTheme = createTheme(defaultDerivative);

缓存中没有,则创建一个新的主题。 node_modules/@ant-design/cssinjs/lib/theme/createTheme.js

/**
 * Same as new Theme, but will always return same one if `derivative` not changed.
 */
function createTheme(derivatives) {
  var derivativeArr = Array.isArray(derivatives) ? derivatives : [derivatives];
  // 检查是否已经生成过主题。
  // Create new theme if not exist
  if (!cacheThemes.has(derivativeArr)) {
    // 创建主题,并缓存。
    cacheThemes.set(derivativeArr, new _Theme.default(derivativeArr));
  }

  // Get theme from cache and return
  return cacheThemes.get(derivativeArr);
}

主题主要与派生函数关联。通过派生函数对 Token 进行派生生成 MapToken(梯度变量)。

/**
 * Theme with algorithms to derive tokens from design tokens.
 * Use `createTheme` first which will help to manage the theme instance cache.
 */
var Theme = exports.default = /*#__PURE__*/function () {
  function Theme(derivatives) {
    (0, _classCallCheck2.default)(this, Theme);
    (0, _defineProperty2.default)(this, "derivatives", void 0);
    (0, _defineProperty2.default)(this, "id", void 0);
    this.derivatives = Array.isArray(derivatives) ? derivatives : [derivatives];
    this.id = uuid;
    if (derivatives.length === 0) {
      (0, _warning.warning)(derivatives.length > 0, '[Ant Design CSS-in-JS] Theme should have at least one derivative function.');
    }
    uuid += 1;
  }
  (0, _createClass2.default)(Theme, [{
    key: "getDerivativeToken",  // 用来派生 Token
    value: function getDerivativeToken(token) {
      return this.derivatives.reduce(function (result, derivative) {
        return derivative(token, result);
      }, undefined);
    }
  }]);
  return Theme;
}();

派生算法会生成的 MapToken(梯度变量) 结果如下。它们都是根据 SeedToken 中的变量生成。

* SeedToken: 定义了其他类型的 token 会以这里定义的颜色作为生成梯度的基础色。  
* ColorMapToken:它继承了一堆颜色相关的 Token。Token 主要为几类,如主色,文本色,危险色,错误色,链接色等。  
                每个 Token 中的颜色主要分为文字,描边,背景几种,每种又有对应深浅色和激活与悬浮态等。   
* SizeMapToken:定义了不同的大小。如:XXL,XL,LG,MD,MS,默认,SM,XS,XXS。  
* HeightMapToken:内部组件高度 如 多选的内部子项,分为:XS(更小),SM(较小),LG(较高)。  
* StyleMapToken:定义了线宽和圆角。线宽是一个单值,圆角分为: XS,SM,LG 和外部圆角。  
* FontMapToken: 定义了字体大小和行高,它们有划分为标题和非标题。字体:小号(SM),标准(Stand),大号(LG),超大号(XL)。行高:文本行高,SM,LG。标题类的字体和行高有一到五级。  
* CommonMapToken:继承自 StyleMapToken,增加了动效的播放速度。快速,中速,慢速。
node_modules/antd/es/theme/interface/maps/index.d.ts
```js
export interface MapToken extends SeedToken, ColorPalettes, LegacyColorPalettes, ColorMapToken, SizeMapToken, HeightMapToken, StyleMapToken, FontMapToken, CommonMapToken {
}

useCacheToken

  1. 生成 alisa token。
  2. 如果开启 css 变量,将 token 中的属性值转换为 css 变量,并生成一个仅包含变量声明的类选择器。
  3. 将上面结果进行缓存。
  4. 在组件挂载时,如果开启css 变量则将包含变量声明的类选择器插入到 Head 中。
  5. 在组件卸载时,删除相关的样式。

开启 css 变量,会如下转换。
例:

 aliasToken = {
  backGround: #fff,
 }

转换为

aliasToken = {
  backGround: var(--ant-back-ground),
}

cssStr = ".xxxx{--ant-back-ground: #fff}"

在副作用更新时将插入 cssStr

<style data-rc-order="prependQueue" data-rc-priority="-999" data-css-hash="1s7a99k" data-token-hash="pi6o7t">
cssStr
</style>

缓存 key: 将 token,override token 和 cssVar 三个对象分别偏平化后与 salt 和 theme id 一起作为缓存的 key。
扁平化过程:

  1. 如果有存在则直接返回。
  2. 遍历对象的 key。reult+= key,检查 value 的类型
  3. 如果是主题,则 reult+= 主题 id
  4. 如果是对象,递归调用
  5. 不属于 3 和 4 类型,直接拼接 reult+= value
  6. 参数作为 key,缓存 reult。

node_modules/@ant-design/cssinjs/es/hooks/useCacheToken.js

/**
 * Cache theme derivative token as global shared one
 * @param theme Theme entity
 * @param tokens List of tokens, used for cache. Please do not dynamic generate object directly
 * @param option Additional config
 * @returns Call Theme.getDerivativeToken(tokenObject) to get token
 */
export default function useCacheToken(theme, tokens) {
  var option = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
  var _useContext = useContext(StyleContext),
    // StyleContext 初始化时会创建一个缓存,并生成一个 id
    instanceId = _useContext.cache.instanceId, 
    container = _useContext.container;
  var _option$salt = option.salt,
    salt = _option$salt === void 0 ? '' : _option$salt,
    _option$override = option.override,
    override = _option$override === void 0 ? EMPTY_OVERRIDE : _option$override,
    formatToken = option.formatToken,
    compute = option.getComputedToken,
    cssVar = option.cssVar;
  // 将 tokens 数组合并为一个对象。  
  // Basic - We do basic cache here
  var mergedToken = memoResult(function () {
    return Object.assign.apply(Object, [{}].concat(_toConsumableArray(tokens)));
  }, tokens);
  // 将 mergedToken,overrideTokenStr,cssVarStr 扁平化为 string。
  var tokenStr = flattenToken(mergedToken);
  var overrideTokenStr = flattenToken(override);
  var cssVarStr = cssVar ? flattenToken(cssVar) : '';
  // 用来生成缓存,并通过副作用,更新和删除。
  var cachedToken = useGlobalCache(TOKEN_PREFIX, [salt, theme.id, tokenStr, overrideTokenStr, cssVarStr], function () { // 需要生成缓存时调用
    var _cssVar$key;
    // 如果没有在外部设置 option.getComputedToken,则调用当前类中的 getComputedToken 函数。
    // 合并最终 Token。
    var mergedDerivativeToken = compute ? compute(mergedToken, override, theme) : getComputedToken(mergedToken, override, theme, formatToken);

    // Replace token value with css variables
    var actualToken = _objectSpread({}, mergedDerivativeToken);
    var cssVarsStr = '';
    if (!!cssVar) {
      // 将 token 中的属性值转换为 css 变量
      // 即:{backGroundColor: #fff} -> {backGroundColor: var(--ant-back-ground-color)}
      var _transformToken = transformToken(mergedDerivativeToken, cssVar.key, {
        prefix: cssVar.prefix,
        ignore: cssVar.ignore,
        unitless: cssVar.unitless,
        preserve: cssVar.preserve
      });
      var _transformToken2 = _slicedToArray(_transformToken, 2);
      // 例:{backGroundColor: var(--ant-back-ground-color)}
      mergedDerivativeToken = _transformToken2[0];
      // 例:".xxxx{--ant-back-ground: #fff}"
      cssVarsStr = _transformToken2[1];
    }
    // 1. 调用 flattenToken 将 mergedDerivativeToken 扁平化为字符串
    // 2. .salt_作为前缀 如 "5.12.5-true_"
    // 3. 使用 emotion 的 hash 函数生成哈希。
    // Optimize for `useStyleRegister` performance
    var tokenKey = token2key(mergedDerivativeToken, salt);
    mergedDerivativeToken._tokenKey = tokenKey;
     // 如果使用 css 变量则 mergedDerivativeToken 与 actualToken 不一致
    // 所以再次生成 tokenKey,否则不会重新生成 tokenKey。
    actualToken._tokenKey = token2key(actualToken, salt);
    //  cssVar.key 不存在则使用 tokenKey 作为 themeKey
    var themeKey = (_cssVar$key = cssVar === null || cssVar === void 0 ? void 0 : cssVar.key) !== null && _cssVar$key !== void 0 ? _cssVar$key : tokenKey;
    mergedDerivativeToken._themeKey = themeKey;
    recordCleanToken(themeKey);
    // 将 tokenKey 进行哈希后,添加前缀,作为 hashId
    // hashPrefix 生产环境下为 'css' 其他环境下为 'css-dev-only-do-not-override'
    var hashId = "".concat(hashPrefix, "-").concat(hash(tokenKey));
    mergedDerivativeToken._hashId = hashId; // Not used

    return [mergedDerivativeToken, hashId, actualToken, cssVarsStr, (cssVar === null || cssVar === void 0 ? void 0 : cssVar.key) || ''];
  }, function (cache) { // 组件卸载时调用
     // 如果保存在 Map 中的 key 的 value 为 0
    // 则查找 style[data-css-hash=cache[0]._themeKey] 元素,
    // 删除 __cssinjs_instance__ 属性值为 instanceId 的元素。
    // Remove token will remove all related style
    cleanTokenStyle(cache[0]._themeKey, instanceId);
  }, function (_ref) { // 组件挂载时调用
    var _ref2 = _slicedToArray(_ref, 4),
      token = _ref2[0],
      cssVarsStr = _ref2[3];
    if (cssVar && cssVarsStr) { // 如果开启 css 变量,插入或更新,仅包含变量声明的类选择器。
      // 查找 'data-css-hash' 属性为指定值的 style 元素。
      // 更新或插入 css,并设置 'data-css-hash' 属性为指定值。
      var style = updateCSS(cssVarsStr, hash("css-variables-".concat(token._themeKey)), {
        mark: ATTR_MARK, // 'data-css-hash'
        prepend: 'queue',
        attachTo: container,
        priority: -999
      });
      style[CSS_IN_JS_INSTANCE] = instanceId;
      // 设置 style 元素的 'data-token-hash' 属性值为 token._themeKey  
      // Used for `useCacheToken` to remove on batch when token removed
      style.setAttribute(ATTR_TOKEN, token._themeKey);
    }
  });
  return cachedToken;
} 

生成 Alias Token

由于用户可以修改默认主题,所以这里根据主题生成 Token 之后,还需处理用户配置 Token。

大体过程:

  1. 根据主题生成派生 Token。
  2. 处理用户自定义的 Token(全局和组件)。用户配置会覆盖默认配置。
  3. 生成最终的 Alias Token。

最终生成的 Alias Token 类型:

{
   [key in keyof ComponentsToken]?: AliasToken
}  & AliasToken

参数 overrideToken

它包含了用户对主题和针对组件的修改。其中 override 属性对应的是针对主题的修改。

类型: node_modules/antd/es/theme/context.d.ts

export type ComponentsToken = {
    [key in keyof OverrideToken]?: OverrideToken[key] & {
        theme?: Theme<SeedToken, MapToken>;
    };
};
export interface DesignTokenProviderProps {
    token: Partial<AliasToken>;
    theme?: Theme<SeedToken, MapToken>;
    components?: ComponentsToken;
    /** Just merge `token` & `override` at top to save perf */
    override: {
        override: Partial<AliasToken>;
    } & ComponentsToken;
    ...
}

node_modules/antd/es/theme/interface/index.d.ts

export type OverrideToken = {
    [key in keyof ComponentTokenMap]: Partial<ComponentTokenMap[key]> & Partial<AliasToken>;
};

DesignTokenContext 默认初始化时,overrideToken 被初始化如下:

export declare const defaultConfig: {
    token: SeedToken;
    override: {
        override: SeedToken;
    };
    hashed: boolean;
};

以上面 useCacheToken 中在处理 Token 时,会使用 option.compute 或当前模块中的 compute 函数进行处理。这里以 option.compute 中指定的函数进行分析。

  1. 根据主题派生 token
  2. 调用 formatToken 生成 alias token
  3. 如果有配置组件 token,则接续处理,结果合并到 2 中的 alias token 。
  4. 返回 alias token。

node_modules/antd/es/theme/useToken.js

export const getComputedToken = (originToken, overrideToken, theme) => {
  // 生成派生 token
  const derivativeToken = theme.getDerivativeToken(originToken);
  const {
      override // AliasToken
    } = overrideToken,
    // 获取组件 Token。 
    components = __rest(overrideToken, ["override"]);
  // 将 override 与 derivativeToken 合并为一个新对象。
  // Merge with override
  let mergedDerivativeToken = Object.assign(Object.assign({}, derivativeToken), {
    override
  });
  // 生成一个 AliasToken。
  // Format if needed
  mergedDerivativeToken = formatToken(mergedDerivativeToken);
  if (components) { // 如果存在组件 Token
    
    Object.entries(components).forEach(_ref => {
      let [key, value] = _ref;
      const {
          theme: componentTheme // 组件Token 中扩展了 theme
        } = value,
        componentTokens = __rest(value, ["theme"]);
      let mergedComponentToken = componentTokens;
      // 如果属性值中函数 theme 属性,进行递归调用。
      if (componentTheme) { // 具有单独主题,则递归调用 getComputedToken 进行处理。
        mergedComponentToken = getComputedToken(Object.assign(Object.assign({}, mergedDerivativeToken), componentTokens), {
          override: componentTokens
        }, componentTheme);
      }
      // 添加到最终 Token 中。
      mergedDerivativeToken[key] = mergedComponentToken;
    });
  }
  return mergedDerivativeToken;
};

生成 alias token。
node_modules/antd/es/theme/util/alias.js

/**
 * Seed (designer) > Derivative (designer) > Alias (developer).
 *
 * Merge seed & derivative & override token and generate alias token for developer.
 */
export default function formatToken(derivativeToken) {
  const {
      override
    } = derivativeToken,
    restToken = __rest(derivativeToken, ["override"]);
  const overrideTokens = Object.assign({}, override);
  // overrideTokens 中不能包含 seedToken 中的属性。
  Object.keys(seedToken).forEach(token => {
    delete overrideTokens[token];
  });
  // 覆盖派生 token 中的属性。
  const mergedToken = Object.assign(Object.assign({}, restToken), overrideTokens);
  const screenXS = 480;
  const screenSM = 576;
  const screenMD = 768;
  const screenLG = 992;
  const screenXL = 1200;
  const screenXXL = 1600;
  // Motion
  if (mergedToken.motion === false) {
    const fastDuration = '0s';
    mergedToken.motionDurationFast = fastDuration;
    mergedToken.motionDurationMid = fastDuration;
    mergedToken.motionDurationSlow = fastDuration;
  }
  // Generate alias token
  const aliasToken = Object.assign(Object.assign(Object.assign({}, mergedToken), {
    // ============== Background ============== //
    colorFillContent: mergedToken.colorFillSecondary,
    colorFillContentHover: mergedToken.colorFill,
    colorFillAlter: mergedToken.colorFillQuaternary,
    colorBgContainerDisabled: mergedToken.colorFillTertiary,
   ...
  }), overrideTokens);
  return aliasToken;