diff --git a/packages/components/config/src/defaultConfig.ts b/packages/components/config/src/defaultConfig.ts index 547234901..bb03c25e8 100644 --- a/packages/components/config/src/defaultConfig.ts +++ b/packages/components/config/src/defaultConfig.ts @@ -27,6 +27,7 @@ export const defaultConfig: GlobalConfig = { }, locale: zhCN, theme: { + injectThemeStyle: true, presetTheme: 'default', hashed: true, }, diff --git a/packages/components/config/src/types.ts b/packages/components/config/src/types.ts index 14a564d55..8a0db8d60 100644 --- a/packages/components/config/src/types.ts +++ b/packages/components/config/src/types.ts @@ -125,6 +125,7 @@ export interface CommonConfig { } export interface ThemeConfig extends DeepPartialThemeTokens { presetTheme: PresetTheme + injectThemeStyle: boolean hashed: boolean attachTo?: ThemeProviderAttachTo } diff --git a/packages/components/theme/docs/Api.zh.md b/packages/components/theme/docs/Api.zh.md index 47338160d..822e208b6 100644 --- a/packages/components/theme/docs/Api.zh.md +++ b/packages/components/theme/docs/Api.zh.md @@ -5,6 +5,7 @@ | 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | | --- | --- | --- | --- | --- | --- | | `presetTheme` | 预设的主题 | `PresetTheme` | `'default'` | ✅ | | +| `injectThemeStyle` | 是否注入主题变量样式 | `boolean` | `true` | ✅ | | | `hashed` | 是否开始 `hash` 功能 | `boolean` | `true` | ✅ | | | `tag` | 配置 `IxThemeProvider` 渲染时使用的标签 | `string` | - | - | - | | `inherit` | 是否继承上层Provider的token和配置 | `boolean \| 'all'` | `true` | - | 配置为true仅继承,配置为`'all'`则必须有上层的provider才会启用主题功能,用于组件封装时覆盖变量的场景 | diff --git a/packages/components/theme/src/ThemeProvider.tsx b/packages/components/theme/src/ThemeProvider.tsx index 6165ff07c..7d93591e0 100644 --- a/packages/components/theme/src/ThemeProvider.tsx +++ b/packages/components/theme/src/ThemeProvider.tsx @@ -5,135 +5,33 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { computed, defineComponent, inject, provide, watch } from 'vue' +import { defineComponent, inject, provide } from 'vue' -import { isFunction } from 'lodash-es' +import { Logger } from '@idux/cdk/utils' -import { useGlobalConfig } from '@idux/components/config' - -import { useTokenMerge } from './composables/useTokenMerge' -import { useTokenRegister } from './composables/useTokenRegister' -import { getResetTokens } from './themeTokens' +import { createThemeProviderContext, useSharedThemeProvider } from './composables/useThemeProvider' import { THEME_PROVIDER_TOKEN } from './token' -import { - type ThemeKeys, - type UsetThemeProviderStates, - globalTokenKey, - resetTokenKey, - themeProviderProps, -} from './types' +import { themeProviderProps } from './types' export default defineComponent({ name: 'IxThemeProvider', props: themeProviderProps, setup(props, { slots, attrs }) { - const supperContext = inject(THEME_PROVIDER_TOKEN, null) - - if (props.inherit != 'all' || supperContext) { - const themeConfig = useGlobalConfig('theme') - const mergedPresetTheme = computed( - () => - (props.inherit && !props.presetTheme ? supperContext?.presetTheme.value : props.presetTheme) ?? - themeConfig.presetTheme, - ) - const mergedHashed = computed( - () => (props.inherit ? supperContext?.hashed.value : undefined) ?? props.hashed ?? themeConfig.hashed, - ) - - const mergedAttachTo = computed(() => { - const attachTo = - (props.inherit ? supperContext?.attachTo.value : undefined) ?? props.attachTo ?? themeConfig.attachTo - if (!attachTo) { - return - } - - if (attachTo instanceof Element) { - return attachTo - } - - if (isFunction(attachTo)) { - return attachTo() - } - - return document.querySelector(attachTo) ?? undefined - }) - - const { mergedAlgorithms, mergedTokens, getMergedTokens } = useTokenMerge( - props, - themeConfig, - supperContext, - mergedPresetTheme, - ) - const { globalHashId, registerToken, updateToken, getThemeTokens, getThemeHashId, isTokensRegistered } = - useTokenRegister(mergedPresetTheme, mergedAlgorithms, mergedAttachTo, mergedHashed, getMergedTokens) - - watch( - () => mergedTokens.value.global, - () => { - const useSupper = props.inherit && !props.tokens?.global && !!supperContext - if (!isTokensRegistered(globalTokenKey)) { - registerToken( - globalTokenKey, - () => (useSupper ? supperContext.getThemeTokens(globalTokenKey) : mergedTokens.value.global), - undefined, - undefined, - useSupper ? supperContext!.getThemeHashId(globalTokenKey) : undefined, - ) - } else { - updateToken(globalTokenKey, useSupper ? supperContext!.getThemeHashId(globalTokenKey) : undefined) - } - - // sub providers don't register reset styles - if (props.inherit && !!supperContext) { - return - } - - if (!isTokensRegistered(resetTokenKey)) { - registerToken( - resetTokenKey, - globalTokens => (useSupper ? supperContext!.getThemeTokens(resetTokenKey) : getResetTokens(globalTokens)), - undefined, - false, - useSupper ? supperContext.getThemeHashId(resetTokenKey) : undefined, - ) - } else { - updateToken(resetTokenKey, useSupper ? supperContext.getThemeHashId(resetTokenKey) : undefined) - } - }, - { - immediate: true, - deep: true, - }, - ) - watch( - () => mergedTokens.value.components, - components => { - Object.keys(components).forEach(key => { - updateToken(key) - }) - }, - { - deep: true, - }, - ) - - const useThemeTokenContextMap = new Map() - - provide(THEME_PROVIDER_TOKEN, { - globalHashId, - hashed: mergedHashed, - presetTheme: mergedPresetTheme, - attachTo: mergedAttachTo, - mergedTokens, - useThemeTokenContextMap, - getThemeHashId, - registerToken, - updateToken, - getThemeTokens, - isTokensRegistered, - }) + let supperContext = inject(THEME_PROVIDER_TOKEN, null) + + if (props.inherit === 'all' && !supperContext) { + if (__DEV__) { + Logger.warn( + 'components/theme', + `parent IxThemeProvider not found when using inherit 'all', this may cause unexpected theme errors`, + ) + } + supperContext = useSharedThemeProvider() } + const context = createThemeProviderContext(supperContext, props) + provide(THEME_PROVIDER_TOKEN, context) + return () => (props.tag ? {slots.default?.()} : <>{slots.default?.()}) }, }) diff --git a/packages/components/theme/src/composables/useThemeProvider.ts b/packages/components/theme/src/composables/useThemeProvider.ts new file mode 100644 index 000000000..ce11b2d0d --- /dev/null +++ b/packages/components/theme/src/composables/useThemeProvider.ts @@ -0,0 +1,150 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import { computed, watch } from 'vue' + +import { isFunction } from 'lodash-es' + +import { createSharedComposable } from '@idux/cdk/utils' +import { useGlobalConfig } from '@idux/components/config' + +import { useTokenMerge } from './useTokenMerge' +import { useTokenRegister } from './useTokenRegister' +import { getResetTokens } from '../themeTokens' +import { type ThemeProviderContext } from '../token' +import { + type ThemeKeys, + type ThemeProviderProps, + type UsetThemeProviderStates, + globalTokenKey, + resetTokenKey, +} from '../types' + +export function createThemeProviderContext( + supperContext: ThemeProviderContext | null, + props?: ThemeProviderProps, +): ThemeProviderContext { + const themeConfig = useGlobalConfig('theme') + const injectThemeStyle = computed(() => props?.injectThemeStyle ?? themeConfig.injectThemeStyle) + const mergedPresetTheme = computed( + () => + (props?.inherit && !props.presetTheme ? supperContext?.presetTheme.value : props?.presetTheme) ?? + themeConfig.presetTheme, + ) + const mergedHashed = computed( + () => (props?.inherit ? supperContext?.hashed.value : undefined) ?? props?.hashed ?? themeConfig.hashed, + ) + + const mergedAttachTo = computed(() => { + const attachTo = + (props?.inherit ? supperContext?.attachTo.value : undefined) ?? props?.attachTo ?? themeConfig.attachTo + if (!attachTo) { + return + } + + if (attachTo instanceof Element) { + return attachTo + } + + if (isFunction(attachTo)) { + return attachTo() + } + + return document.querySelector(attachTo) ?? undefined + }) + + const { mergedAlgorithms, mergedTokens, getMergedTokens } = useTokenMerge( + props, + themeConfig, + supperContext, + mergedPresetTheme, + ) + const { globalHashId, registerToken, updateToken, getThemeTokens, getThemeHashId, isTokensRegistered } = + useTokenRegister( + injectThemeStyle, + mergedPresetTheme, + mergedAlgorithms, + mergedAttachTo, + mergedHashed, + getMergedTokens, + ) + + watch( + () => mergedTokens.value.global, + () => { + const useSupper = props?.inherit && !props.tokens?.global && !!supperContext + if (!isTokensRegistered(globalTokenKey)) { + registerToken( + globalTokenKey, + () => (useSupper ? supperContext!.getThemeTokens(globalTokenKey) : mergedTokens.value.global), + undefined, + undefined, + useSupper ? supperContext!.getThemeHashId(globalTokenKey) : undefined, + ) + } else { + updateToken(globalTokenKey, useSupper ? supperContext!.getThemeHashId(globalTokenKey) : undefined) + } + + // sub providers don't register reset styles + if (props?.inherit && !!supperContext) { + return + } + + if (!isTokensRegistered(resetTokenKey)) { + registerToken( + resetTokenKey, + globalTokens => (useSupper ? supperContext!.getThemeTokens(resetTokenKey) : getResetTokens(globalTokens)), + undefined, + false, + useSupper ? supperContext!.getThemeHashId(resetTokenKey) : undefined, + ) + } else { + updateToken(resetTokenKey, useSupper ? supperContext!.getThemeHashId(resetTokenKey) : undefined) + } + }, + { + immediate: true, + deep: true, + }, + ) + watch( + () => mergedTokens.value.components, + components => { + Object.keys(components).forEach(key => { + updateToken(key) + }) + }, + { + deep: true, + }, + ) + + const useThemeTokenContextMap = new Map() + + return { + globalHashId, + hashed: mergedHashed, + presetTheme: mergedPresetTheme, + attachTo: mergedAttachTo, + mergedTokens, + useThemeTokenContextMap, + getThemeHashId, + registerToken, + updateToken, + getThemeTokens, + isTokensRegistered, + } +} + +// export function useThemeProvider(props?: ThemeProviderProps) { +// let supperContext = inject(THEME_PROVIDER_TOKEN, null) +// supperContext = supperContext ?? useSharedThemeProvider() + +// return createThemeProviderContext(supperContext, props) +// } + +export const useSharedThemeProvider = createSharedComposable(() => createThemeProviderContext(null)) diff --git a/packages/components/theme/src/composables/useTokenMerge.ts b/packages/components/theme/src/composables/useTokenMerge.ts index 856046285..c58a8932d 100644 --- a/packages/components/theme/src/composables/useTokenMerge.ts +++ b/packages/components/theme/src/composables/useTokenMerge.ts @@ -32,14 +32,14 @@ export interface TokenMergeContext { } export function useTokenMerge( - props: ThemeProviderProps, + props: ThemeProviderProps | undefined, config: ThemeConfig, supperContext: ThemeProviderContext | null, mergedPresetTheme: ComputedRef, ): TokenMergeContext { const mergedAlgorithms = computed(() => { const presetAlgorithms = getPresetAlgorithms(mergedPresetTheme.value) - const { getBaseColors, getColorPalette, getGreyColors } = props.algorithm ?? {} + const { getBaseColors, getColorPalette, getGreyColors } = props?.algorithm ?? {} return { getBaseColors: getBaseColors ?? presetAlgorithms.getBaseColors, @@ -53,17 +53,17 @@ export function useTokenMerge( const configComponentTokens = config.components const overwrittenTokens = merge( - { ...(props.inherit && !props.presetTheme ? supperContext?.mergedTokens.value.global ?? {} : {}) }, + { ...(props?.inherit && !props.presetTheme ? supperContext?.mergedTokens.value.global ?? {} : {}) }, { ...configGlobalTokens }, - props.tokens?.global, + props?.tokens?.global, ) as GlobalThemeTokens const mergedGlobalTokens = getThemeTokens(mergedPresetTheme.value, overwrittenTokens, mergedAlgorithms.value) const mergedComponentTokens = merge( - { ...(props.inherit ? supperContext?.mergedTokens.value.components ?? {} : {}) }, + { ...(props?.inherit ? supperContext?.mergedTokens.value.components ?? {} : {}) }, { ...configComponentTokens }, - props.tokens?.components, + props?.tokens?.components, ) return { diff --git a/packages/components/theme/src/composables/useTokenRegister.ts b/packages/components/theme/src/composables/useTokenRegister.ts index c65fb6abc..b50197b39 100644 --- a/packages/components/theme/src/composables/useTokenRegister.ts +++ b/packages/components/theme/src/composables/useTokenRegister.ts @@ -56,6 +56,7 @@ export interface TokenRegisterContext { } export function useTokenRegister( + injectThemeStyle: ComputedRef, mergedPresetTheme: ComputedRef, mergedAlgorithms: ComputedRef, mergedAttachTo: ComputedRef, @@ -121,7 +122,7 @@ export function useTokenRegister( tokenHashedMap.set(key, hashed) // if hashId is already provided, we consider the style injected already, no need to inject it again - if (!existedHashId) { + if (injectThemeStyle.value && !existedHashId) { const cssContent = tokenToCss( { ...record, hashId: hashed ?? mergedHashed.value ? record.hashId : '' } as TokenRecord, transforms, diff --git a/packages/components/theme/src/types/themeProvider.ts b/packages/components/theme/src/types/themeProvider.ts index 8ad6a8476..45d6874d4 100644 --- a/packages/components/theme/src/types/themeProvider.ts +++ b/packages/components/theme/src/types/themeProvider.ts @@ -25,6 +25,10 @@ export const themeProviderProps = { type: String as PropType, default: undefined, }, + injectThemeStyle: { + type: Boolean, + default: undefined, + }, hashed: { type: Boolean, default: undefined, diff --git a/packages/components/theme/src/useThemeToken.ts b/packages/components/theme/src/useThemeToken.ts index 45fbb65cb..b4dce7c07 100644 --- a/packages/components/theme/src/useThemeToken.ts +++ b/packages/components/theme/src/useThemeToken.ts @@ -9,8 +9,9 @@ import type { TokenGetter } from './composables/useTokenRegister' import { type ComputedRef, computed, effectScope, inject } from 'vue' -import { Logger, tryOnScopeDispose } from '@idux/cdk/utils' +import { tryOnScopeDispose } from '@idux/cdk/utils' +import { useSharedThemeProvider } from './composables/useThemeProvider' import { THEME_PROVIDER_TOKEN, type ThemeProviderContext } from './token' import { type CertainThemeTokens, @@ -48,8 +49,6 @@ export type UseThemeTokenContext< ? ScopedUseThemeTokenContext : never -let emptyContext: UseThemeTokenContext - export function useThemeToken(): GlobalUseThemeTokenContext export function useThemeToken( key: K, @@ -57,22 +56,10 @@ export function useThemeToken( key?: K, ): UseThemeTokenContext { - const themeProviderContext = inject(THEME_PROVIDER_TOKEN, null) + let themeProviderContext = inject(THEME_PROVIDER_TOKEN, null) if (!themeProviderContext) { - Logger.warn('components/theme', ' not found.') - - if (!emptyContext) { - emptyContext = { - globalHashId: computed(() => ''), - hashId: computed(() => ''), - themeTokens: computed(() => ({})), - presetTheme: computed(() => 'default'), - registerToken: (() => {}) as unknown as UseThemeTokenContext['registerToken'], - } as UseThemeTokenContext - } - - return emptyContext as unknown as UseThemeTokenContext + themeProviderContext = useSharedThemeProvider() } const { diff --git a/packages/components/theme/src/utils/createTokensHash.ts b/packages/components/theme/src/utils/createTokensHash.ts index c914235c6..1f0ac3ab1 100644 --- a/packages/components/theme/src/utils/createTokensHash.ts +++ b/packages/components/theme/src/utils/createTokensHash.ts @@ -9,16 +9,30 @@ import hash from '@emotion/hash' import { themeTokenPrefix } from '../types' +const sequenceCache = new Map() export function createTokensHash(key: string, tokens: Record): string { - return `${themeTokenPrefix}-${key}-${hash(flattenTokens(tokens))}` + let sequence = sequenceCache.get(key) + + if (!sequence) { + sequence = getTokenSequence(tokens) + sequenceCache.set(key, sequence) + } + + return `${themeTokenPrefix}-${key}-${hash(flattenTokens(tokens, sequence))}` } -function flattenTokens(tokens: Record) { +function flattenTokens(tokens: Record, sequence: string[]): string { let str = '' + sequence - Object.entries(tokens).forEach(([key, value]) => { - str += `${key}${value}` + sequence.forEach(key => { + const value = tokens[key] + str += `${key}${value || ''}` }) return str } + +function getTokenSequence(tokens: Record): string[] { + return Object.keys(tokens).sort() +} diff --git a/packages/site/src/docs/CustomizeTheme.zh.md b/packages/site/src/docs/CustomizeTheme.zh.md index 90878bede..2c3f00b7d 100644 --- a/packages/site/src/docs/CustomizeTheme.zh.md +++ b/packages/site/src/docs/CustomizeTheme.zh.md @@ -103,6 +103,12 @@ import { IxThemeProvider } from '@idux/components/theme' > 注:即使不使用 `hash`,组件库内部仍然会计算 `hash`,通过比对两次的计算结果来判断是否有组件或全局的主题变更 +### 是否注入主题变量样式 + +在某些场景下,如果希望通过引入组件的全量预生成的css变量文件而不希望动态注入样式,可以配置 `injectThemeStyle` 为 `false` 来关闭动态注入。 + +> 注:关闭之后将不再注入样式,但仍然会计算`hash`和注入主题js变量。 + ### 主题嵌套 可以通过 `IxThemeProvider` 的嵌套使用来实现主题嵌套 @@ -573,9 +579,11 @@ token `getter` 只会在第一次注册成功的时候实际执行,因此不 ### 可不可以不使用 IxThemeProvider? -如果由于某些限制无法使用 IxThemeProvider,我们在打包产物中增加了不同主题下的全部 `css` 变量,可以直接在项目中引入这些变量并针对性覆盖。 +如果由于某些限制无法使用 `IxThemeProvider`,我们在打包产物中增加了不同主题下的全部 `css` 变量,可以直接在项目中引入这些变量并针对性覆盖。 + +首先,通过全局配置或者 `IxThemeProvider` 的 `props` 配置 `injectThemeStyle` 为 `false` -可以将全量的变量直接引入到项目中: +之后,可以将全量的变量直接引入到项目中: ```ts // 引入默认主题全量变量 diff --git a/packages/site/src/docs/GettingStarted.zh.md b/packages/site/src/docs/GettingStarted.zh.md index 95d615d81..4eeacb648 100755 --- a/packages/site/src/docs/GettingStarted.zh.md +++ b/packages/site/src/docs/GettingStarted.zh.md @@ -125,7 +125,7 @@ export default defineConfig({ #### IxThemeProvider -在 Idux v2 版本中,我们增加了 `IxThemeProvider` 来管理主题配置并动态插入主题css变量,因此需要在 `vue` 应用的最外围包裹使用 `IxThemeProvider`。 +在 Idux v2 版本中,我们增加了 `IxThemeProvider` 来管理主题配置并动态插入主题css变量,如果需要使用动态主题或者主题覆盖功能,要在 `vue` 应用的最外围包裹使用 `IxThemeProvider`。 例如,可以在 App.vue 中这样写: diff --git a/tests/setup.ts b/tests/setup.ts index 7d6ff2a49..7106727be 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -1,8 +1,11 @@ import { ResizeObserver } from '@juggle/resize-observer' +import { defaultConfig } from '@idux/components/config' import { addIconDefinitions } from '@idux/components/icon' import * as allIcon from '@idux/components/icon/src/definitions' +defaultConfig.theme.hashed = false + addIconDefinitions(Object.values(allIcon)) global.ResizeObserver = ResizeObserver