案例 2:多主题切换引擎
在现代 Web 应用中,提供多主题切换功能已成为提升用户体验的重要特性。本案例将使用 Zustand 实现一个功能完整的多主题切换引擎,支持亮色/暗色主题切换、自定义主题配置以及主题持久化。
1. 主题引擎设计思路
1.1 核心功能需求
- 支持亮色、暗色两种默认主题
- 允许用户自定义主题颜色
- 主题配置持久化存储
- 实时主题切换,无需页面刷新
- 支持嵌套主题和局部主题覆盖
1.2 Store 设计方案
使用 Zustand 创建主题管理 Store,集成 Persist 中间件实现主题配置持久化,使用 Immer 简化状态更新逻辑。
2. 实现步骤
2.1 安装必要依赖
bash
npm install zustand immer zustand/middleware2.2 创建主题 Store
javascript
// src/stores/themeStore.js
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'
// 定义默认主题
const defaultThemes = {
light: {
name: 'light',
colors: {
primary: '#3b82f6',
background: '#ffffff',
surface: '#f3f4f6',
text: '#1f2937',
textSecondary: '#6b7280',
border: '#e5e7eb',
error: '#ef4444'
}
},
dark: {
name: 'dark',
colors: {
primary: '#60a5fa',
background: '#111827',
surface: '#1f2937',
text: '#f9fafb',
textSecondary: '#d1d5db',
border: '#374151',
error: '#f87171'
}
}
}
// 创建主题 Store
export const useThemeStore = create(
persist(
immer((set, get) => ({
// 当前主题
currentTheme: 'light',
// 主题配置
themes: { ...defaultThemes },
// 自定义主题
customThemes: {},
// 切换主题
toggleTheme: () => {
set(state => {
state.currentTheme = state.currentTheme === 'light' ? 'dark' : 'light'
})
},
// 切换到指定主题
setTheme: (themeName) => {
set(state => {
if (state.themes[themeName] || state.customThemes[themeName]) {
state.currentTheme = themeName
}
})
},
// 更新主题颜色
updateThemeColors: (themeName, colors) => {
set(state => {
const targetTheme = state.themes[themeName] || state.customThemes[themeName]
if (targetTheme) {
Object.assign(targetTheme.colors, colors)
}
})
},
// 创建自定义主题
createCustomTheme: (themeName, colors) => {
set(state => {
state.customThemes[themeName] = {
name: themeName,
colors: { ...defaultThemes.light.colors, ...colors }
}
})
},
// 删除自定义主题
deleteCustomTheme: (themeName) => {
set(state => {
delete state.customThemes[themeName]
// 如果当前正在使用被删除的主题,切换到默认主题
if (state.currentTheme === themeName) {
state.currentTheme = 'light'
}
})
},
// 获取当前主题配置
getCurrentThemeConfig: () => {
const { currentTheme, themes, customThemes } = get()
return themes[currentTheme] || customThemes[currentTheme]
},
// 获取所有可用主题名称
getAllThemeNames: () => {
const { themes, customThemes } = get()
return [...Object.keys(themes), ...Object.keys(customThemes)]
}
})),
{
name: 'theme-storage',
storage: createJSONStorage(() => localStorage)
}
)
)2.3 创建主题上下文提供者
javascript
// src/components/ThemeProvider.jsx
import React, { useEffect } from 'react'
import { useThemeStore } from '../stores/themeStore'
export const ThemeProvider = ({ children }) => {
const currentThemeConfig = useThemeStore(state => state.getCurrentThemeConfig())
useEffect(() => {
// 应用主题到根元素
const root = document.documentElement
// 清除旧的主题变量
root.style.setProperty('--color-primary', '')
root.style.setProperty('--color-background', '')
root.style.setProperty('--color-surface', '')
root.style.setProperty('--color-text', '')
root.style.setProperty('--color-text-secondary', '')
root.style.setProperty('--color-border', '')
root.style.setProperty('--color-error', '')
// 设置新的主题变量
root.style.setProperty('--color-primary', currentThemeConfig.colors.primary)
root.style.setProperty('--color-background', currentThemeConfig.colors.background)
root.style.setProperty('--color-surface', currentThemeConfig.colors.surface)
root.style.setProperty('--color-text', currentThemeConfig.colors.text)
root.style.setProperty('--color-text-secondary', currentThemeConfig.colors.textSecondary)
root.style.setProperty('--color-border', currentThemeConfig.colors.border)
root.style.setProperty('--color-error', currentThemeConfig.colors.error)
// 更新根元素的主题类名
root.className = currentThemeConfig.name
}, [currentThemeConfig])
return <>{children}</>
}2.4 创建主题切换组件
javascript
// src/components/ThemeSwitcher.jsx
import React from 'react'
import { useThemeStore } from '../stores/themeStore'
const ThemeSwitcher = () => {
const {
currentTheme,
toggleTheme,
setTheme,
getAllThemeNames
} = useThemeStore()
const themeNames = getAllThemeNames()
return (
<div className="theme-switcher">
<button
onClick={toggleTheme}
className="theme-toggle-btn"
aria-label="切换主题"
>
{currentTheme === 'light' ? '🌙' : '☀️'}
</button>
<select
value={currentTheme}
onChange={(e) => setTheme(e.target.value)}
className="theme-select"
>
{themeNames.map(themeName => (
<option key={themeName} value={themeName}>
{themeName === 'light' ? '亮色主题' : themeName === 'dark' ? '暗色主题' : themeName}
</option>
))}
</select>
</div>
)
}
export default ThemeSwitcher2.5 创建自定义主题编辑器
javascript
// src/components/ThemeEditor.jsx
import React, { useState } from 'react'
import { useThemeStore } from '../stores/themeStore'
const ThemeEditor = () => {
const { createCustomTheme, updateThemeColors } = useThemeStore()
const [themeName, setThemeName] = useState('')
const [colors, setColors] = useState({
primary: '#3b82f6',
background: '#ffffff',
surface: '#f3f4f6',
text: '#1f2937'
})
const handleColorChange = (colorName, value) => {
setColors(prev => ({ ...prev, [colorName]: value }))
}
const handleCreateTheme = () => {
if (themeName.trim()) {
createCustomTheme(themeName, colors)
setThemeName('')
// 重置为默认颜色
setColors({
primary: '#3b82f6',
background: '#ffffff',
surface: '#f3f4f6',
text: '#1f2937'
})
}
}
return (
<div className="theme-editor">
<h3>自定义主题</h3>
<input
type="text"
placeholder="主题名称"
value={themeName}
onChange={(e) => setThemeName(e.target.value)}
className="theme-name-input"
/>
<div className="color-pickers">
<div className="color-picker-group">
<label>主色调:</label>
<input
type="color"
value={colors.primary}
onChange={(e) => handleColorChange('primary', e.target.value)}
/>
</div>
<div className="color-picker-group">
<label>背景色:</label>
<input
type="color"
value={colors.background}
onChange={(e) => handleColorChange('background', e.target.value)}
/>
</div>
<div className="color-picker-group">
<label>表面色:</label>
<input
type="color"
value={colors.surface}
onChange={(e) => handleColorChange('surface', e.target.value)}
/>
</div>
<div className="color-picker-group">
<label>文字色:</label>
<input
type="color"
value={colors.text}
onChange={(e) => handleColorChange('text', e.target.value)}
/>
</div>
</div>
<button
onClick={handleCreateTheme}
disabled={!themeName.trim()}
className="create-theme-btn"
>
创建主题
</button>
</div>
)
}
export default ThemeEditor2.6 创建样式文件
css
/* src/styles/theme.css */
:root {
/* 默认亮色主题变量 */
--color-primary: #3b82f6;
--color-background: #ffffff;
--color-surface: #f3f4f6;
--color-text: #1f2937;
--color-text-secondary: #6b7280;
--color-border: #e5e7eb;
--color-error: #ef4444;
}
/* 暗色主题类 */
.dark {
color-scheme: dark;
}
/* 主题切换器样式 */
.theme-switcher {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background-color: var(--color-surface);
border-radius: 8px;
border: 1px solid var(--color-border);
}
.theme-toggle-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
padding: 0.5rem;
border-radius: 4px;
transition: background-color 0.2s;
}
.theme-toggle-btn:hover {
background-color: var(--color-border);
}
.theme-select {
padding: 0.5rem;
border: 1px solid var(--color-border);
border-radius: 4px;
background-color: var(--color-background);
color: var(--color-text);
cursor: pointer;
}
/* 主题编辑器样式 */
.theme-editor {
padding: 1.5rem;
background-color: var(--color-surface);
border-radius: 8px;
border: 1px solid var(--color-border);
max-width: 500px;
margin: 1rem 0;
}
.theme-editor h3 {
margin-top: 0;
margin-bottom: 1rem;
color: var(--color-text);
}
.theme-name-input {
width: 100%;
padding: 0.75rem;
margin-bottom: 1rem;
border: 1px solid var(--color-border);
border-radius: 4px;
background-color: var(--color-background);
color: var(--color-text);
}
.color-pickers {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.color-picker-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.color-picker-group label {
color: var(--color-text-secondary);
font-size: 0.875rem;
}
.color-picker-group input[type="color"] {
width: 100%;
height: 40px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.create-theme-btn {
width: 100%;
padding: 0.75rem;
background-color: var(--color-primary);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
}
.create-theme-btn:hover:not(:disabled) {
opacity: 0.9;
}
.create-theme-btn:disabled {
background-color: var(--color-border);
cursor: not-allowed;
opacity: 0.7;
}
/* 主题预览样式 */
.theme-preview {
margin-top: 2rem;
padding: 1.5rem;
background-color: var(--color-surface);
border-radius: 8px;
border: 1px solid var(--color-border);
}
.theme-preview h3 {
color: var(--color-text);
margin-top: 0;
margin-bottom: 1rem;
}
.preview-card {
background-color: var(--color-background);
padding: 1.5rem;
border-radius: 8px;
border: 1px solid var(--color-border);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.preview-card h4 {
color: var(--color-text);
margin-top: 0;
margin-bottom: 0.5rem;
}
.preview-card p {
color: var(--color-text-secondary);
margin: 0;
}2.7 在应用中集成主题系统
javascript
// src/App.jsx
import React from 'react'
import { ThemeProvider } from './components/ThemeProvider'
import ThemeSwitcher from './components/ThemeSwitcher'
import ThemeEditor from './components/ThemeEditor'
import './styles/theme.css'
function App() {
return (
<ThemeProvider>
<div className="app">
<header className="app-header">
<h1>多主题切换引擎</h1>
<ThemeSwitcher />
</header>
<main className="app-main">
<ThemeEditor />
<div className="theme-preview">
<h3>主题预览</h3>
<div className="preview-card">
<h4>示例卡片</h4>
<p>这是一个使用当前主题样式的示例卡片。</p>
<button style={{
marginTop: '1rem',
padding: '0.5rem 1rem',
backgroundColor: 'var(--color-primary)',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}>
主按钮
</button>
</div>
</div>
</main>
</div>
</ThemeProvider>
)
}
export default App3. 功能特性与最佳实践
3.1 核心功能特性
- 主题持久化: 使用 Zustand 的 Persist 中间件将主题配置保存到 localStorage
- 实时切换: 主题切换实时生效,无需页面刷新
- 自定义主题: 支持用户创建和编辑自定义主题
- 响应式设计: 主题系统与 CSS 变量无缝集成,支持响应式设计
- 类型安全: 如果使用 TypeScript,可以为主题配置添加完整的类型定义
3.2 最佳实践
- 使用 CSS 变量: 将主题颜色定义为 CSS 变量,便于全局应用和动态更新
- 主题继承: 自定义主题可以继承默认主题的颜色,减少重复配置
- 性能优化: 使用 Zustand 的 selector 模式只订阅必要的主题状态
- 可扩展性: 设计时考虑未来可能的主题扩展,如字体大小、间距等
- 无障碍支持: 确保所有主题都符合无障碍标准,特别是颜色对比度
3.3 扩展建议
- 添加主题导入/导出功能
- 实现主题预览功能
- 支持基于系统主题自动切换
- 添加主题历史记录功能
- 实现主题分享功能
4. 总结
本案例实现了一个功能完整的多主题切换引擎,使用 Zustand 管理主题状态,集成了 Persist 和 Immer 中间件。通过 CSS 变量和 React 上下文提供者,实现了主题的全局应用和实时切换。该方案具有良好的可扩展性和维护性,可以轻松应用到各种 React 项目中,为用户提供个性化的主题体验。