diff --git a/components/_util/cssinjs/Cache.ts b/components/_util/cssinjs/Cache.ts index 007e4a867..010a0e3f6 100644 --- a/components/_util/cssinjs/Cache.ts +++ b/components/_util/cssinjs/Cache.ts @@ -1,16 +1,20 @@ export type KeyType = string | number; type ValueType = [number, any]; // [times, realValue] - +const SPLIT = '%'; class Entity { + instanceId: string; + constructor(instanceId: string) { + this.instanceId = instanceId; + } /** @private Internal cache map. Do not access this directly */ cache = new Map(); get(keys: KeyType[] | string): ValueType | null { - return this.cache.get(Array.isArray(keys) ? keys.join('%') : keys) || null; + return this.cache.get(Array.isArray(keys) ? keys.join(SPLIT) : keys) || null; } update(keys: KeyType[] | string, valueFn: (origin: ValueType | null) => ValueType | null) { - const path = Array.isArray(keys) ? keys.join('%') : keys; + const path = Array.isArray(keys) ? keys.join(SPLIT) : keys; const prevValue = this.cache.get(path)!; const nextValue = valueFn(prevValue); diff --git a/components/_util/cssinjs/StyleContext.tsx b/components/_util/cssinjs/StyleContext.tsx index eff876327..9e8200445 100644 --- a/components/_util/cssinjs/StyleContext.tsx +++ b/components/_util/cssinjs/StyleContext.tsx @@ -7,20 +7,22 @@ import { arrayType, booleanType, objectType, someType, stringType, withInstall } import initDefaultProps from '../props-util/initDefaultProps'; export const ATTR_TOKEN = 'data-token-hash'; export const ATTR_MARK = 'data-css-hash'; -export const ATTR_DEV_CACHE_PATH = 'data-dev-cache-path'; +export const ATTR_CACHE_PATH = 'data-cache-path'; // Mark css-in-js instance in style element export const CSS_IN_JS_INSTANCE = '__cssinjs_instance__'; -export const CSS_IN_JS_INSTANCE_ID = Math.random().toString(12).slice(2); 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] || CSS_IN_JS_INSTANCE_ID; + (style as any)[CSS_IN_JS_INSTANCE] = (style as any)[CSS_IN_JS_INSTANCE] || cssinjsInstanceId; // Not force move if no head document.head.insertBefore(style, firstChild); @@ -31,7 +33,7 @@ export function createCache() { 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] === CSS_IN_JS_INSTANCE_ID) { + if ((style as any)[CSS_IN_JS_INSTANCE] === cssinjsInstanceId) { style.parentNode?.removeChild(style); } } else { @@ -40,7 +42,7 @@ export function createCache() { }); } - return new CacheEntity(); + return new CacheEntity(cssinjsInstanceId); } export type HashPriority = 'low' | 'high'; diff --git a/components/_util/cssinjs/hooks/useCacheToken.tsx b/components/_util/cssinjs/hooks/useCacheToken.tsx index 1bee55d50..49365e4dc 100644 --- a/components/_util/cssinjs/hooks/useCacheToken.tsx +++ b/components/_util/cssinjs/hooks/useCacheToken.tsx @@ -1,5 +1,5 @@ import hash from '@emotion/hash'; -import { ATTR_TOKEN, CSS_IN_JS_INSTANCE, CSS_IN_JS_INSTANCE_ID } from '../StyleContext'; +import { ATTR_TOKEN, CSS_IN_JS_INSTANCE, useStyleInject } from '../StyleContext'; import type Theme from '../theme/Theme'; import useGlobalCache from './useGlobalCache'; import { flattenToken, token2key } from '../util'; @@ -12,7 +12,7 @@ const EMPTY_OVERRIDE = {}; // This helps developer not to do style override directly on the hash id. const hashPrefix = process.env.NODE_ENV !== 'production' ? 'css-dev-only-do-not-override' : 'css'; -export interface Option { +export interface Option { /** * Generate token with salt. * This is used to generate different hashId even same derivative token for different version. @@ -30,6 +30,18 @@ export interface Option { * It's ok to useMemo outside but this has better cache strategy. */ formatToken?: (mergedToken: any) => DerivativeToken; + /** + * Get final token with origin token, override token and theme. + * The parameters do not contain formatToken since it's passed by user. + * @param origin The original token. + * @param override Extra tokens to override. + * @param theme Theme instance. Could get derivative token by `theme.getDerivativeToken` + */ + getComputedToken?: ( + origin: DesignToken, + override: object, + theme: Theme, + ) => DerivativeToken; } const tokenKeys = new Map(); @@ -37,20 +49,22 @@ function recordCleanToken(tokenKey: string) { tokenKeys.set(tokenKey, (tokenKeys.get(tokenKey) || 0) + 1); } -function removeStyleTags(key: string) { +function removeStyleTags(key: string, instanceId: string) { if (typeof document !== 'undefined') { const styles = document.querySelectorAll(`style[${ATTR_TOKEN}="${key}"]`); styles.forEach(style => { - if ((style as any)[CSS_IN_JS_INSTANCE] === CSS_IN_JS_INSTANCE_ID) { + if ((style as any)[CSS_IN_JS_INSTANCE] === instanceId) { style.parentNode?.removeChild(style); } }); } } +const TOKEN_THRESHOLD = 0; + // Remove will check current keys first -function cleanTokenStyle(tokenKey: string) { +function cleanTokenStyle(tokenKey: string, instanceId: string) { tokenKeys.set(tokenKey, (tokenKeys.get(tokenKey) || 0) - 1); const tokenKeyList = Array.from(tokenKeys.keys()); @@ -60,14 +74,37 @@ function cleanTokenStyle(tokenKey: string) { return count <= 0; }); - if (cleanableKeyList.length < tokenKeyList.length) { + // Should keep tokens under threshold for not to insert style too often + if (tokenKeyList.length - cleanableKeyList.length > TOKEN_THRESHOLD) { cleanableKeyList.forEach(key => { - removeStyleTags(key); + removeStyleTags(key, instanceId); tokenKeys.delete(key); }); } } +export const getComputedToken = ( + originToken: DesignToken, + overrideToken: object, + theme: Theme, + format?: (token: DesignToken) => DerivativeToken, +) => { + const derivativeToken = theme.getDerivativeToken(originToken); + + // Merge with override + let mergedDerivativeToken = { + ...derivativeToken, + ...overrideToken, + }; + + // Format if needed + if (format) { + mergedDerivativeToken = format(mergedDerivativeToken); + } + + return mergedDerivativeToken; +}; + /** * Cache theme derivative token as global shared one * @param theme Theme entity @@ -78,8 +115,10 @@ function cleanTokenStyle(tokenKey: string) { export default function useCacheToken( theme: Ref>, tokens: Ref[]>, - option: Ref> = ref({}), + option: Ref> = ref({}), ) { + const style = useStyleInject(); + // Basic - We do basic cache here const mergedToken = computed(() => Object.assign({}, ...tokens.value)); const tokenStr = computed(() => flattenToken(mergedToken.value)); @@ -94,19 +133,15 @@ export default function useCacheToken { - const { salt = '', override = EMPTY_OVERRIDE, formatToken } = option.value; - const derivativeToken = theme.value.getDerivativeToken(mergedToken.value); - - // Merge with override - let mergedDerivativeToken = { - ...derivativeToken, - ...override, - }; - - // Format if needed - if (formatToken) { - mergedDerivativeToken = formatToken(mergedDerivativeToken); - } + const { + salt = '', + override = EMPTY_OVERRIDE, + formatToken, + getComputedToken: compute, + } = option.value; + const mergedDerivativeToken = compute + ? compute(mergedToken.value, override, theme.value) + : getComputedToken(mergedToken.value, override, theme.value, formatToken); // Optimize for `useStyleRegister` performance const tokenKey = token2key(mergedDerivativeToken, salt); @@ -120,7 +155,7 @@ export default function useCacheToken { // Remove token will remove all related style - cleanTokenStyle(cache[0]._tokenKey); + cleanTokenStyle(cache[0]._tokenKey, style.value?.cache.instanceId); }, ); diff --git a/components/_util/cssinjs/hooks/useHMR.ts b/components/_util/cssinjs/hooks/useHMR.ts index 4fc7ab7ed..ff54518b8 100644 --- a/components/_util/cssinjs/hooks/useHMR.ts +++ b/components/_util/cssinjs/hooks/useHMR.ts @@ -16,7 +16,8 @@ if ( process.env.NODE_ENV !== 'production' && typeof module !== 'undefined' && module && - (module as any).hot + (module as any).hot && + typeof window !== 'undefined' ) { const win = window as any; if (typeof win.webpackHotUpdate === 'function') { diff --git a/components/_util/cssinjs/hooks/useStyleRegister/cacheMapUtil.ts b/components/_util/cssinjs/hooks/useStyleRegister/cacheMapUtil.ts new file mode 100644 index 000000000..69a57c933 --- /dev/null +++ b/components/_util/cssinjs/hooks/useStyleRegister/cacheMapUtil.ts @@ -0,0 +1,91 @@ +import canUseDom from '../../../../_util/canUseDom'; +import { ATTR_MARK } from '../../StyleContext'; + +export const ATTR_CACHE_MAP = 'data-ant-cssinjs-cache-path'; + +/** + * This marks style from the css file. + * Which means not exist in ``; - }); + const attrStr = Object.keys(attrs) + .map(attr => { + const val = attrs[attr]; + return val ? `${attr}="${val}"` : null; + }) + .filter(v => v) + .join(' '); + + return plain ? style : ``; + } + + // ====================== Fill Style ====================== + type OrderStyle = [order: number, style: string]; + + const orderStyles: OrderStyle[] = styleKeys + .map(key => { + const cachePath = key.slice(matchPrefix.length).replace(/%/g, '|'); + + const [styleStr, tokenKey, styleId, effectStyle, clientOnly, order]: [ + string, + string, + string, + Record, + boolean, + number, + ] = cache.cache.get(key)![1]; + + // Skip client only style + if (clientOnly) { + return null! as OrderStyle; + } + + // ====================== Style ====================== + // Used for vc-util + const sharedAttrs = { + 'data-vc-order': 'prependQueue', + 'data-vc-priority': `${order}`, + }; + + let keyStyleText = toStyleStr(styleStr, tokenKey, styleId, sharedAttrs); + + // Save cache path with hash mapping + cachePathMap[cachePath] = styleId; + + // =============== Create effect style =============== + if (effectStyle) { + Object.keys(effectStyle).forEach(effectKey => { + // Effect style can be reused + if (!effectStyles[effectKey]) { + effectStyles[effectKey] = true; + keyStyleText += toStyleStr( + normalizeStyle(effectStyle[effectKey]), + tokenKey, + `_effect-${effectKey}`, + sharedAttrs, + ); + } + }); + } + + const ret: OrderStyle = [order, keyStyleText]; + + return ret; + }) + .filter(o => o); + + orderStyles + .sort((o1, o2) => o1[0] - o2[0]) + .forEach(([, style]) => { + styleText += style; + }); + + // ==================== Fill Cache Path ==================== + styleText += toStyleStr( + `.${ATTR_CACHE_MAP}{content:"${serializeCacheMap(cachePathMap)}";}`, + undefined, + undefined, + { + [ATTR_CACHE_MAP]: ATTR_CACHE_MAP, + }, + ); return styleText; } diff --git a/components/_util/cssinjs/index.ts b/components/_util/cssinjs/index.ts index a22943787..511045180 100644 --- a/components/_util/cssinjs/index.ts +++ b/components/_util/cssinjs/index.ts @@ -11,6 +11,8 @@ import { createTheme, Theme } from './theme'; import type { Transformer } from './transformers/interface'; import legacyLogicalPropertiesTransformer from './transformers/legacyLogicalProperties'; import px2remTransformer from './transformers/px2rem'; +import { supportLogicProps, supportWhere } from './util'; + const cssinjs = { Theme, createTheme, @@ -68,4 +70,8 @@ export type { StyleProviderProps, }; +export const _experimental = { + supportModernCSS: () => supportWhere() && supportLogicProps(), +}; + export default cssinjs; diff --git a/components/_util/cssinjs/util.ts b/components/_util/cssinjs/util.ts index b4115a37d..f22b226d8 100644 --- a/components/_util/cssinjs/util.ts +++ b/components/_util/cssinjs/util.ts @@ -2,17 +2,30 @@ import hash from '@emotion/hash'; import { removeCSS, updateCSS } from '../../vc-util/Dom/dynamicCSS'; import canUseDom from '../canUseDom'; +import { Theme } from './theme'; + +// Create a cache here to avoid always loop generate +const flattenTokenCache = new WeakMap(); + export function flattenToken(token: any) { - let str = ''; - Object.keys(token).forEach(key => { - const value = token[key]; - str += key; - if (value && typeof value === 'object') { - str += flattenToken(value); - } else { - str += value; - } - }); + let str = flattenTokenCache.get(token) || ''; + + if (!str) { + Object.keys(token).forEach(key => { + const value = token[key]; + str += key; + if (value instanceof Theme) { + str += value.id; + } else if (value && typeof value === 'object') { + str += flattenToken(value); + } else { + str += value; + } + }); + + // Put in cache + flattenTokenCache.set(token, str); + } return str; } @@ -23,12 +36,18 @@ export function token2key(token: any, salt: string): string { return hash(`${salt}_${flattenToken(token)}`); } -const layerKey = `layer-${Date.now()}-${Math.random()}`.replace(/\./g, ''); -const layerWidth = '903px'; +const randomSelectorKey = `random-${Date.now()}-${Math.random()}`.replace(/\./g, ''); -function supportSelector(styleStr: string, handleElement?: (ele: HTMLElement) => void): boolean { +// Magic `content` for detect selector support +const checkContent = '_bAmBoO_'; + +function supportSelector( + styleStr: string, + handleElement: (ele: HTMLElement) => void, + supportCheck?: (ele: HTMLElement) => boolean, +): boolean { if (canUseDom()) { - updateCSS(styleStr, layerKey); + updateCSS(styleStr, randomSelectorKey); const ele = document.createElement('div'); ele.style.position = 'fixed'; @@ -42,10 +61,12 @@ function supportSelector(styleStr: string, handleElement?: (ele: HTMLElement) => ele.style.zIndex = '9999999'; } - const support = getComputedStyle(ele).width === layerWidth; + const support = supportCheck + ? supportCheck(ele) + : getComputedStyle(ele).content?.includes(checkContent); ele.parentNode?.removeChild(ele); - removeCSS(layerKey); + removeCSS(randomSelectorKey); return support; } @@ -57,12 +78,41 @@ let canLayer: boolean | undefined = undefined; export function supportLayer(): boolean { if (canLayer === undefined) { canLayer = supportSelector( - `@layer ${layerKey} { .${layerKey} { width: ${layerWidth}!important; } }`, + `@layer ${randomSelectorKey} { .${randomSelectorKey} { content: "${checkContent}"!important; } }`, ele => { - ele.className = layerKey; + ele.className = randomSelectorKey; }, ); } return canLayer!; } + +let canWhere: boolean | undefined = undefined; +export function supportWhere(): boolean { + if (canWhere === undefined) { + canWhere = supportSelector( + `:where(.${randomSelectorKey}) { content: "${checkContent}"!important; }`, + ele => { + ele.className = randomSelectorKey; + }, + ); + } + + return canWhere!; +} + +let canLogic: boolean | undefined = undefined; +export function supportLogicProps(): boolean { + if (canLogic === undefined) { + canLogic = supportSelector( + `.${randomSelectorKey} { inset-block: 93px !important; }`, + ele => { + ele.className = randomSelectorKey; + }, + ele => getComputedStyle(ele).bottom === '93px', + ); + } + + return canLogic!; +} diff --git a/components/vc-util/Dom/dynamicCSS.ts b/components/vc-util/Dom/dynamicCSS.ts index 1b429c4c9..ac16baf62 100644 --- a/components/vc-util/Dom/dynamicCSS.ts +++ b/components/vc-util/Dom/dynamicCSS.ts @@ -15,6 +15,7 @@ interface Options { csp?: { nonce?: string }; prepend?: Prepend; mark?: string; + priority?: number; } function getMark({ mark }: Options = {}) {