一、说明antd4和antd5主题区别:
核心差异是 AntD4 是「静态 Less 变量」,AntD5 是「动态 CSS 变量」,这直接导致了主题切换的实现方式天差地别。
| 类型 | 作用时机 | 能否动态修改 | 典型场景 |
|---|---|---|---|
| Less 变量 | 编译时(打包阶段) | ❌ 不能(编译后变成固定 CSS 值) | AntD4 的主题定制 |
| CSS 变量 | 运行时(浏览器中) | ✅ 可以(直接修改变量值,样式实时变化) | AntD5 的主题切换 |
1、AntD4
AntD4 的样式是 基于 Less 静态编译的 ------ 所有主题变量(比如 @primary-color)在打包时就被编译成了固定的 CSS 值,举个例子:
AntD4 源码中的 Less 代码:
css
// AntD4 按钮样式(Less)
.ant-btn {
background: @primary-color; // Less 变量
color: #fff;
}
打包编译后,@primary-color 会被替换成固定值(比如 #006bff),最终输出的 CSS 是:
css
/* 编译后的 AntD4 CSS */
.ant-btn {
background: #006bff; /* 固定值,无法动态修改 */
color: #fff;
}
所以 AntD4 要切换主题,必须重新编译 Less 文件 (把 @primary-color 换成新值,再生成新的 CSS),然后替换页面中的样式。
2、AntD5
AntD5 把所有 Less 变量改成了 CSS 变量 (比如 --ant-primary-color),打包后输出的 CSS 是「变量引用形式」,而不是固定值:
AntD5 源码中的 Less 代码(最终编译成 CSS 变量):
css
// AntD5 按钮样式(Less 编译为 CSS 变量)
.ant-btn {
background: var(--ant-primary-color); // 引用 CSS 变量
color: #fff;
}
打包后输出的 CSS 是:
css
/* 编译后的 AntD5 CSS */
.ant-btn {
background: var(--ant-primary-color); /* 保留变量引用,不替换成固定值 */
color: #fff;
}
同时,AntD5 会在页面根节点(比如 <html>)注入默认的 CSS 变量:
css
/* AntD5 自动注入的默认变量 */
:root {
--ant-primary-color: #006bff;
--ant-body-background: #fff;
/* ...其他变量 */
}
所以 AntD5 切换主题,只需要修改根节点上的 CSS 变量值 (比如把 --ant-primary-color 改成 #f00),样式会实时变化 ------ 这就是为什么 AntD5 用 ConfigProvider 就能轻松切换主题,不需要编译。
| 维度 | AntD4 | AntD5 |
|---|---|---|
| 变量类型 | Less 静态变量 | CSS 动态变量 |
| 样式编译时机 | 打包时(固定值) | 打包时(保留变量引用) |
| 主题切换方式 | 动态编译 Less → 替换 CSS 文件 | 修改 CSS 变量值 → 实时生效 |
| 依赖 | 需要 less 库编译 |
无额外依赖(原生 CSS 支持) |
| 切换性能 | 较慢(编译 + 替换样式) | 极快(仅修改变量) |
二、实现主题切换
1、antd4实现
1)通过less自己实现
css
//1、style/theme/dark.less
@import 'antd/es/style/themes/default.less';
@import './variables.less'; //公共less主题变量
// 深色主题专属变量
@primary-color: #006bff; // 你的主题色
@body-background: #141414;
@component-background: #222222;
@text-color: rgba(255, 255, 255, 0.85);
@text-color-secondary: rgba(255, 255, 255, 0.65);
//2、style/theme/light.less
@import 'antd/es/style/themes/default.less';
@import './variables.less';
// 浅色主题专属变量
@primary-color: #006bff;
@body-background: #ffffff;
@component-background: #ffffff;
@text-color: rgba(0, 0, 0, 0.85);
@text-color-secondary: rgba(0, 0, 0, 0.65);
css
//需要安装less:npm install less --save
//主题切换工具函数:theme-utils.ts
// @renderer/utils/theme-utils.ts
import less from 'less';
// 主题类型
export type ThemeType = 'dark' | 'light';
// 全局样式标签 ID(用于替换样式)
const THEME_STYLE_ID = 'antd-theme-style';
// 加载对应主题的 Less 文件(注意路径对应你项目的实际结构)
const loadThemeLess = async (theme: ThemeType): Promise<string> => {
// 开发环境:直接加载本地 Less 文件
if (import.meta.env.DEV) {
const response = await fetch(`/src/style/theme/${theme}.less`);
return response.text();
}
// 生产环境:需提前将主题 Less 打包为静态资源(或内联)
// 示例:假设生产环境主题文件放在 dist/theme 下
const response = await fetch(`/theme/${theme}.less`);
return response.text();
};
// 编译并注入主题样式
export const switchTheme = async (theme: ThemeType) => {
try {
// 1. 加载主题 Less 代码
const lessCode = await loadThemeLess(theme);
// 2. 编译 Less(指定 AntD 前缀等配置)
const { css } = await less.render(lessCode, {
javascriptEnabled: true, // 允许 Less 中使用 JS
modifyVars: {
'@ant-prefix': 'ant', // 和你配置的前缀保持一致
},
});
// 3. 替换页面中的主题样式
let styleElement = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement;
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = THEME_STYLE_ID;
document.head.appendChild(styleElement);
}
styleElement.innerHTML = css;
// 4. 保存主题到本地存储(刷新后恢复)
localStorage.setItem('antd-theme', theme);
} catch (error) {
console.error('切换主题失败:', error);
}
};
// 初始化主题(页面加载时调用)
export const initTheme = () => {
const savedTheme = localStorage.getItem('antd-theme') as ThemeType || 'light';
switchTheme(savedTheme);
};
主题初始化:
javascript
// @renderer/App.tsx
import { useEffect } from 'react';
import { initTheme } from './utils/theme-utils';
const App = () => {
useEffect(() => {
// 页面加载时初始化主题
initTheme();
}, []);
return <div>你的应用内容</div>;
};
export default App;
切换主题:
css
// @renderer/components/ThemeSwitch.tsx
import { Button } from 'antd';
import { switchTheme, ThemeType } from '../utils/theme-utils';
const ThemeSwitch = () => {
const toggleTheme = async () => {
const currentTheme = localStorage.getItem('antd-theme') as ThemeType || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
await switchTheme(newTheme);
};
return (
<Button onClick={toggleTheme} type="primary">
切换主题
</Button>
);
};
export default ThemeSwitch;
注意:开发环境可以直接通过 fetch 加载本地 Less 文件,但生产环境需要将主题 Less 文件打包为静态资源 :
vite配置:
css
import { copyFileSync, mkdirSync, existsSync } from 'fs';
import { join } from 'path';
// 复制主题文件到 dist
const copyThemeFiles = () => {
const srcDir = join(__dirname, 'src/style/theme');
const distDir = join(__dirname, 'dist/renderer/theme');
if (!existsSync(distDir)) mkdirSync(distDir, { recursive: true });
['dark.less', 'light.less', 'variables.less'].forEach(file => {
copyFileSync(join(srcDir, file), join(distDir, file));
});
};
export default defineConfig({
renderer: {
// 其他配置...
build: {
// 打包完成后复制主题文件
onEnd: () => copyThemeFiles(),
},
},
});
2)通过插件实现:如@zougt/vite-plugin-theme-preprocessor
主题变量配置不变,即style文件夹下的不变
css
//需要安装:npm install @zougt/vite-plugin-theme-preprocessor -D
import { themePreprocessorPlugin } from '@zougt/vite-plugin-theme-preprocessor'
{
plugins:[
// ...其他配置
themePreprocessorPlugin({
less: {
// 是否启用任意主题色模式,这里不启用
arbitraryMode: false,
// 在生产模式是否抽取独立的主题css文件,extract为true以下属性有效
extract: true,
// 默认主题
defaultScopeName: 'theme-dark',
// 提供多组变量文件
multipleScopeVars: [
{
scopeName: 'theme-dark',
path: resolve('src/renderer/src/style/theme/dark.less')
},
{
scopeName: 'theme-light',
path: resolve('src/renderer/src/style/theme/light.less')
}
]
}
}),
// ...其他配置
]
}
构建的产物:
css
.theme-light .ant-btn {
background-color: #1677ff;
}
.theme-dark .ant-btn {
background-color: #177ddc;
}
运行时切换css:document.body.className = 'theme-dark'
话可以通过属性data-theme:
css
//style/index.less
[data-theme='light'] {
@import './theme/light.less';
}
[data-theme='dark'] {
@import './theme/dark.less';
}
//生成的css为:
//[data-theme='light'] { ... }
//[data-theme='dark'] { ... }
//初始化
const theme = localStorage.getItem('theme') ?? 'light'
document.documentElement.setAttribute('data-theme', theme)
//切换
export function setTheme(theme: 'light' | 'dark') {
document.documentElement.setAttribute('data-theme', theme)
localStorage.setItem('theme', theme)
}
只是更改部分变量:使用less的modifyVars
javascript
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import less from 'less';
export default defineConfig({
plugins: [react()],
css: {
preprocessorOptions: {
less: {
modifyVars: {
'@primary-color': '#1890ff',
'@link-color': '#1890ff',
},
javascriptEnabled: true,
},
},
},
});
通过antd的cdn动态切换
javascript
const changeTheme = (theme) => {
const linkId = 'antd-theme-link';
let link = document.getElementById(linkId);
if (!link) {
link = document.createElement('link');
link.rel = 'stylesheet';
link.id = linkId;
document.head.appendChild(link);
}
if (theme === 'dark') {
link.href = 'https://cdn.jsdelivr.net/npm/antd@4/dist/antd.dark.css';
} else {
link.href = 'https://cdn.jsdelivr.net/npm/antd@4/dist/antd.css';
}
};
2、antd5
AntD v5 的主题由三层组成:Theme = Algorithm + Token + Component Token
Algorithm(主题算法)
-
defaultAlgorithm(亮色) -
darkAlgorithm(暗色) -
compactAlgorithm
决定:整体色板生成规则
global Token(全局设计变量):类似但 不等价于 v4 的 Less 变量:
css
{
colorPrimary,
colorBgBase,
colorTextBase,
borderRadius,
}
Component Token(组件级定制)
css
{
Button: {
colorPrimary,
borderRadius,
}
}
1、自定义主题变量
css
// theme/index.ts
import { theme } from 'antd'
export type ThemeMode = 'light' | 'dark'
export const themeConfigMap = {
light: {
algorithm: theme.defaultAlgorithm,
token: {
colorPrimary: '#1677ff',
colorBgBase: '#ffffff',
colorTextBase: '#000000',
borderRadius: 6,
},
},
dark: {
algorithm: theme.darkAlgorithm,
token: {
colorPrimary: '#177ddc',
colorBgBase: '#141414',
colorTextBase: '#ffffff',
borderRadius: 6,
},
},
}
css
import { ConfigProvider } from 'antd'
import { themeConfigMap, ThemeMode } from './theme'
interface Props {
themeMode: ThemeMode
}
export function ThemeProvider({ themeMode, children }: React.PropsWithChildren<Props>) {
return (
<ConfigProvider theme={themeConfigMap[themeMode]}>
{children}
</ConfigProvider>
)
}
使用:
css
const App = () => {
const [theme, setTheme] = useState<ThemeMode>('light')
return (
<ThemeProvider themeMode={theme}>
<Button type="primary">Primary</Button>
<Switch
checked={theme === 'dark'}
onChange={(v) => setTheme(v ? 'dark' : 'light')}
/>
</ThemeProvider>
)
}
js中读取token:
javascript
import { theme } from 'antd'
const { useToken } = theme
const MyComponent = () => {
const { token } = useToken()
return <div style={{ color: token.colorPrimary }} />
}
antd5就不适合在独立搞一套和主题有关系的自定义变量,最好自定义antd的主题样式,但是一定要再单独搞一套业务的样式,可以如下:
javascript
// 自定义主题变量
export const customThemeMap: Record<ThemeMode, CustomThemeTokens> = {
light: {
chartBg: '#ffffff',
chartGrid: '#e5e5e5',
orderBuy: '#2ecc71',
orderSell: '#e74c3c',
pnlPositive: '#16a085',
pnlNegative: '#c0392b',
},
dark: {
chartBg: '#0f0f0f',
chartGrid: '#2a2a2a',
orderBuy: '#2ecc71',
orderSell: '#e74c3c',
pnlPositive: '#1abc9c',
pnlNegative: '#e74c3c',
},
}
//使用context向下提供,之后通过style使用
import React from 'react'
export const CustomThemeContext =
React.createContext<CustomThemeTokens | null>(null)
export const CustomThemeProvider: React.FC<{
themeMode: ThemeMode
}> = ({ themeMode, children }) => {
return (
<CustomThemeContext.Provider value={customThemeMap[themeMode]}>
{children}
</CustomThemeContext.Provider>
)
}
//通过style使用
const chartTheme = useContext(CustomThemeContext)
<div style={{ background: chartTheme.chartBg }} />
//和antd总体使用
<ConfigProvider theme={antdThemeMap[themeMode]}>
<CustomThemeProvider themeMode={themeMode}>
<App />
</CustomThemeProvider>
</ConfigProvider>
若是自定义的变量有点多:
css
/* @renderer/style/global.css */
/* 1. 基础变量(公共 fallback 值) */
:root {
/* 主题 1:浅色主题(默认) */
--primary-color: #006bff;
--body-bg: #ffffff;
--text-color: rgba(0, 0, 0, 0.85);
--border-color: #d9d9d9;
/* 可扩展更多变量:按钮、卡片、表单等 */
--btn-primary-bg: var(--primary-color);
--card-bg: #ffffff;
}
/* 2. 主题 2:深色主题(通过 .dark 类触发) */
:root.dark {
--primary-color: #006bff; /* 可自定义深色主题的主色 */
--body-bg: #141414;
--text-color: rgba(255, 255, 255, 0.85);
--border-color: #333333;
--btn-primary-bg: var(--primary-color);
--card-bg: #222222;
}
/* 3. 全局样式使用 CSS 变量(替代固定值) */
body {
background-color: var(--body-bg);
color: var(--text-color);
margin: 0;
padding: 0;
transition: background-color 0.3s ease; /* 主题切换过渡动画 */
}
/* 示例:按钮样式使用变量 */
.btn-primary {
background-color: var(--btn-primary-bg);
color: #fff;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
}
切换主题:
javascript
// @renderer/utils/theme-utils.ts
export type ThemeType = 'light' | 'dark';
// 初始化主题(页面加载时执行)
export const initTheme = () => {
// 优先读取本地存储的主题,默认浅色
const savedTheme = localStorage.getItem('app-theme') as ThemeType || 'light';
setTheme(savedTheme);
};
// 设置主题(核心函数)
export const setTheme = (theme: ThemeType) => {
const root = document.documentElement; // 获取 :root 对应的 html 元素
// 移除所有主题类名,避免冲突
root.classList.remove('light', 'dark');
// 添加当前主题类名
root.classList.add(theme);
// 持久化到本地存储
localStorage.setItem('app-theme', theme);
};
// 切换主题(快捷函数)
export const toggleTheme = () => {
const currentTheme = localStorage.getItem('app-theme') as ThemeType || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
};
或者通过js动态设置:
javascript
useEffect(() => {
applyCustomTheme(customThemeMap[themeMode])
}, [themeMode])
function applyCustomTheme(theme: CustomThemeTokens) {
const root = document.documentElement
Object.entries(theme).forEach(([key, value]) => {
root.style.setProperty(`--custom-${key}`, value)
})
}
// 实际样式
.chart {
background: var(--custom-chartBg);
}