Skip to content

Commit

Permalink
chore: update cssinjs
Browse files Browse the repository at this point in the history
  • Loading branch information
tangjinzhou committed Sep 9, 2023
1 parent 2b8f2ad commit d0f7c34
Show file tree
Hide file tree
Showing 9 changed files with 404 additions and 81 deletions.
10 changes: 7 additions & 3 deletions components/_util/cssinjs/Cache.ts
Original file line number Diff line number Diff line change
@@ -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<string, ValueType>();

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);

Expand Down
14 changes: 8 additions & 6 deletions components/_util/cssinjs/StyleContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 {
Expand All @@ -40,7 +42,7 @@ export function createCache() {
});
}

return new CacheEntity();
return new CacheEntity(cssinjsInstanceId);
}

export type HashPriority = 'low' | 'high';
Expand Down
79 changes: 57 additions & 22 deletions components/_util/cssinjs/hooks/useCacheToken.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<DerivativeToken> {
export interface Option<DerivativeToken, DesignToken> {
/**
* Generate token with salt.
* This is used to generate different hashId even same derivative token for different version.
Expand All @@ -30,27 +30,41 @@ export interface Option<DerivativeToken> {
* 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<any, any>,
) => DerivativeToken;
}

const tokenKeys = new Map<string, number>();
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());
Expand All @@ -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 = <DerivativeToken = object, DesignToken = DerivativeToken>(
originToken: DesignToken,
overrideToken: object,
theme: Theme<any, any>,
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
Expand All @@ -78,8 +115,10 @@ function cleanTokenStyle(tokenKey: string) {
export default function useCacheToken<DerivativeToken = object, DesignToken = DerivativeToken>(
theme: Ref<Theme<any, any>>,
tokens: Ref<Partial<DesignToken>[]>,
option: Ref<Option<DerivativeToken>> = ref({}),
option: Ref<Option<DerivativeToken, DesignToken>> = ref({}),
) {
const style = useStyleInject();

// Basic - We do basic cache here
const mergedToken = computed(() => Object.assign({}, ...tokens.value));
const tokenStr = computed(() => flattenToken(mergedToken.value));
Expand All @@ -94,19 +133,15 @@ export default function useCacheToken<DerivativeToken = object, DesignToken = De
overrideTokenStr.value,
]),
() => {
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);
Expand All @@ -120,7 +155,7 @@ export default function useCacheToken<DerivativeToken = object, DesignToken = De
},
cache => {
// Remove token will remove all related style
cleanTokenStyle(cache[0]._tokenKey);
cleanTokenStyle(cache[0]._tokenKey, style.value?.cache.instanceId);
},
);

Expand Down
3 changes: 2 additions & 1 deletion components/_util/cssinjs/hooks/useHMR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
91 changes: 91 additions & 0 deletions components/_util/cssinjs/hooks/useStyleRegister/cacheMapUtil.ts
Original file line number Diff line number Diff line change
@@ -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 `<style />` tag.
*/
export const CSS_FILE_STYLE = '_FILE_STYLE__';

export function serialize(cachePathMap: Record<string, string>) {
return Object.keys(cachePathMap)
.map(path => {
const hash = cachePathMap[path];
return `${path}:${hash}`;
})
.join(';');
}

let cachePathMap: Record<string, string>;
let fromCSSFile = true;

/**
* @private Test usage only. Can save remove if no need.
*/
export function reset(mockCache?: Record<string, string>, fromFile = true) {
cachePathMap = mockCache!;
fromCSSFile = fromFile;
}

export function prepare() {
if (!cachePathMap) {
cachePathMap = {};

if (canUseDom()) {
const div = document.createElement('div');
div.className = ATTR_CACHE_MAP;
div.style.position = 'fixed';
div.style.visibility = 'hidden';
div.style.top = '-9999px';
document.body.appendChild(div);

let content = getComputedStyle(div).content || '';
content = content.replace(/^"/, '').replace(/"$/, '');

// Fill data
content.split(';').forEach(item => {
const [path, hash] = item.split(':');
cachePathMap[path] = hash;
});

// Remove inline record style
const inlineMapStyle = document.querySelector(`style[${ATTR_CACHE_MAP}]`);
if (inlineMapStyle) {
fromCSSFile = false;
inlineMapStyle.parentNode?.removeChild(inlineMapStyle);
}

document.body.removeChild(div);
}
}
}

export function existPath(path: string) {
prepare();

return !!cachePathMap[path];
}

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];
}
Loading

0 comments on commit d0f7c34

Please sign in to comment.