@ant-design/cssinjs源码阅读笔记

345 阅读3分钟

创建主题(createTheme)

1、缓存主题

   创建缓存空间(Map) 缓存主题配置
   const cacheThemes = new ThemeCache();
   
   缓存主题
   cacheThemes.set(derivativeArr, new Theme(derivativeArr));
   
   new Theme(derivativeArr)
   实例主题,将主题方法设值到示例成员属性,并提供获取主题值get方法(getDerivativeToken)
   

2、使用缓存主题token

// 获取主题实例化 token 值, 获取hash key
const [token, hashId] = useCacheToken<DerivativeToken, DesignToken>(
    theme,
    [defaultDesignToken, rootDesignToken],
    {
      salt: typeof hashed === 'string' ? hashed : '',
    },
);

3、缓存优化渲染

  // 全局注册,内部会做缓存优化
  const wrapSSR = useStyleRegister(
    { theme, token, hashId, path: [prefixCls] },
    () => [
      genDefaultButtonStyle(defaultCls, token),
      genPrimaryButtonStyle(primaryCls, token),
      genGhostButtonStyle(ghostCls, token),
    ],
  );    
  ........
  // useStyleRegister 最后返回一个函数组件,包含单独的样式、被包裹的组件节点
  return (node: React.ReactElement) => {
    let styleNode: React.ReactElement;

    if (!ssrInline || isMergedClientSide || !defaultCache) {
      styleNode = <Empty />;
    } else {
      styleNode = (
        <style
          {...{
            [ATTR_TOKEN]: cachedTokenKey,
            [ATTR_MARK]: cachedStyleId,
          }}
          dangerouslySetInnerHTML={{ __html: cachedStyleStr }}
        />
      );
    }

    return (
      <>
        {styleNode}
        {node}
      </>
    );
  }; 
   

从DOM中获取样式

if (existPath(cachePath)) {
    //inlineCacheStyleStr 样式字符串
   const [inlineCacheStyleStr, styleHash] = getStyleAndHash(cachePath);
        if (inlineCacheStyleStr) {
          return [
            inlineCacheStyleStr,
            tokenKey,
            styleHash,
            {},
            clientOnly,
            order,
          ];
    }
 }

生成样式

// Generate style
   const styleObj = styleFn();
   const [parsedStyle, effectStyle] = parseStyle(styleObj, {
        hashId,
        hashPriority,
        layer,
        path: path.join('-'),
        transformers,
        linters,
   });

   const styleStr = normalizeStyle(parsedStyle);

回调调用获取样式(注意:此代码为调用cssinjs的api方法代码,非@ant-design/cssinjs源码

// 全局注册,内部会做缓存优化
  const wrapSSR = useStyleRegister(
    { theme, token, hashId, path: [prefixCls] },
    // 调用该方法获取样式
    () => [
      genDefaultButtonStyle(defaultCls, token),
      genPrimaryButtonStyle(primaryCls, token),
      genGhostButtonStyle(ghostCls, token),
    ],
  );
export function getStyleAndHash(
  path: string,
): [style: string | null, hash: string] {
  const hash = cachePathMap[path];
  let styleStr: string | null = null;

  if (hash && canUseDom()) {
    if (fromCSSFile) {
      styleStr = CSS_FILE_STYLE;
    } else {
      const style = document.querySelector(
        `style[${ATTR_MARK}="${cachePathMap[path]}"]`,
      );

      if (style) {
        styleStr = style.innerHTML;
      } else {
        // Clean up since not exist anymore
        delete cachePathMap[path];
      }
    }
  }

  return [styleStr, hash];
}

//提供缓存上下文
const StyleContext = React.createContext<StyleContextProps>({
  hashPriority: 'low',
  cache: createCache(),
  defaultCache: true,
});
//处理document.body里面的style标签,将style 放到head标签中,并删除body重复的style标签
//返回缓存值实体
export function createCache() {
  const cssinjsInstanceId = Math.random().toString(12).slice(2);

  // Tricky SSR: Move all inline style to the head.
  // PS: We do not recommend tricky mode.
  if (typeof document !== 'undefined' && document.head && document.body) {
    const styles = document.body.querySelectorAll(`style[${ATTR_MARK}]`) || [];
    const { firstChild } = document.head;

    Array.from(styles).forEach((style) => {
      (style as any)[CSS_IN_JS_INSTANCE] =
        (style as any)[CSS_IN_JS_INSTANCE] || cssinjsInstanceId;

      // Not force move if no head
      if ((style as any)[CSS_IN_JS_INSTANCE] === cssinjsInstanceId) {
        document.head.insertBefore(style, firstChild);
      }
    });

    // Deduplicate of moved styles
    const styleHash: Record<string, boolean> = {};
    Array.from(document.querySelectorAll(`style[${ATTR_MARK}]`)).forEach(
      (style) => {
        const hash = style.getAttribute(ATTR_MARK)!;
        if (styleHash[hash]) {
          if ((style as any)[CSS_IN_JS_INSTANCE] === cssinjsInstanceId) {
            style.parentNode?.removeChild(style);
          }
        } else {
          styleHash[hash] = true;
        }
      },
    );
  }

  return new CacheEntity(cssinjsInstanceId);
}

缓存核心

// cachedStyleStr css 样式字符串
const [cachedStyleStr, cachedTokenKey, cachedStyleId] = useGlobalCache<CacheType>(
  prefix: string,
  keyPath: KeyType[],
  cacheFn: () => CacheType,
  onCacheRemove?: (cache: CacheType, fromHMR: boolean) => void,
  // Add additional effect trigger by `useInsertionEffect`
  onCacheEffect?: (cachedValue: CacheType) => void,
)
export default function useGlobalCache<CacheType>(
  prefix: string,
  keyPath: KeyType[],
  cacheFn: () => CacheType,
  onCacheRemove?: (cache: CacheType, fromHMR: boolean) => void,
  // Add additional effect trigger by `useInsertionEffect`
  onCacheEffect?: (cachedValue: CacheType) => void,
): CacheType {
  const { cache: globalCache } = React.useContext(StyleContext);
  const fullPath = [prefix, ...keyPath];
  const deps = fullPath.join('_');
  
  const register = useEffectCleanupRegister([deps]);

  const HMRUpdate = useHMR();

  type UpdaterArgs = [times: number, cache: CacheType];
  
  const buildCache = (updater?: (data: UpdaterArgs) => UpdaterArgs) => {
    globalCache.update(fullPath, (prevCache) => {
      const [times = 0, cache] = prevCache || [];

      // HMR should always ignore cache since developer may change it
      let tmpCache = cache;
      if (process.env.NODE_ENV !== 'production' && cache && HMRUpdate) {
        onCacheRemove?.(tmpCache, HMRUpdate);
        tmpCache = null;
      }

      const mergedCache = tmpCache || cacheFn();

      const data: UpdaterArgs = [times, mergedCache];

      // Call updater if need additional logic
      return updater ? updater(data) : data;
    });
  };

  // Create cache
  React.useMemo(
    () => {
      buildCache();
    },
    /* eslint-disable react-hooks/exhaustive-deps */
    [deps],
    /* eslint-enable */
  );

  let cacheEntity = globalCache.get(fullPath);

  // HMR clean the cache but not trigger `useMemo` again
  // Let's fallback of this
  // ref https://github.com/ant-design/cssinjs/issues/127
  if (process.env.NODE_ENV !== 'production' && !cacheEntity) {
    buildCache();
    cacheEntity = globalCache.get(fullPath);
  }

  const cacheContent = cacheEntity![1];

  // Remove if no need anymore
  useCompatibleInsertionEffect(
    () => {
      onCacheEffect?.(cacheContent);
    },
    (polyfill) => {
      // It's bad to call build again in effect.
      // But we have to do this since StrictMode will call effect twice
      // which will clear cache on the first time.
      buildCache(([times, cache]) => {
        if (polyfill && times === 0) {
          onCacheEffect?.(cacheContent);
        }
        return [times + 1, cache];
      });

      return () => {
        globalCache.update(fullPath, (prevCache) => {
          const [times = 0, cache] = prevCache || [];
          const nextCount = times - 1;

          if (nextCount === 0) {
            // Always remove styles in useEffect callback
            register(() => onCacheRemove?.(cache, false));
            return null;
          }

          return [times - 1, cache];
        });
      };
    },
    [deps],
  );

  return cacheContent;
}

如何打包出 css in js 单独样式?

1、动态生成 style 标签

2、与包裹组件一起渲染

3、操作DOM 把body中的style 标签插入到head中

如何生成样式字符串?

1、在一次的组件缓存中调用了两次 useGlobalCache,第一次缓存当前主题token,第二次缓存组件样式

2、首次生成样式 调用 cacheFn 回调方法,获取样式字符串

3、cacheFn 调用, 命中缓存 调用getStyleAndHash获取DOM中标签的样式,否则调用styleFn获取样式对象,通过parseStyle、normalizeStyle 解析生成样式字符串

何时注入主题样式?

在调用 styleFn 回调函数时,会把主题token传递到样式函数,在业务侧实现主题样式修改。

const wrapSSR = useStyleRegister(
    { theme, token, hashId, path: [prefixCls] },
    () => [
      genDefaultButtonStyle(defaultCls, token),
      genPrimaryButtonStyle(primaryCls, token),
      genGhostButtonStyle(ghostCls, token),
    ],
  );
const genDefaultButtonStyle = (
  prefixCls: string,
  token: DerivativeToken,
): CSSInterpolation =>
  genSolidButtonStyle(prefixCls, token, () => ({
    backgroundColor: token.componentBackgroundColor,
    color: token.textColor,

    '&:hover': {
      borderColor: token.primaryColor,
      color: token.primaryColor,
    },
  }));

总结脑图

未命名文件-导出 (1).png