# 案例 2：多主题切换引擎

URL: https://caijiao.org/zustand/practice/theme-engine
Source: docs/zustand/practice/theme-engine.md
Description: 学习如何使用 Zustand 实现一个功能完整的多主题切换引擎，支持亮色/暗色主题切换、自定义主题配置、主题持久化存储、实时主题切换以及嵌套主题覆盖等功能。

在现代 Web 应用中，提供多主题切换功能已成为提升用户体验的重要特性。本案例将使用 [Zustand](../) 实现一个功能完整的多主题切换引擎，支持亮色/暗色主题切换、自定义主题配置以及主题持久化。

## 1. 主题引擎设计思路

### 1.1 核心功能需求

- 支持亮色、暗色两种默认主题
- 允许用户自定义主题颜色
- 主题配置持久化存储
- 实时主题切换，无需页面刷新
- 支持嵌套主题和局部主题覆盖

### 1.2 Store 设计方案

使用 Zustand 创建主题管理 Store，集成 [Persist 中间件](../guide/middleware-persist)实现主题配置持久化，使用 [Immer](../guide/middleware-immer) 简化状态更新逻辑。

## 2. 实现步骤

### 2.1 安装必要依赖

```bash
npm install zustand immer zustand/middleware
```

### 2.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 ThemeSwitcher
```

### 2.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 ThemeEditor
```

### 2.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 (
    
  )
}

export default App
```

## 3. 功能特性与最佳实践

### 3.1 核心功能特性

- **主题持久化**: 使用 Zustand 的 Persist 中间件将主题配置保存到 localStorage
- **实时切换**: 主题切换实时生效，无需页面刷新
- **自定义主题**: 支持用户创建和编辑自定义主题
- **响应式设计**: 主题系统与 CSS 变量无缝集成，支持响应式设计
- **类型安全**: 如果使用 TypeScript，可以为主题配置添加完整的类型定义

### 3.2 最佳实践

1. **使用 CSS 变量**: 将主题颜色定义为 CSS 变量，便于全局应用和动态更新
2. **主题继承**: 自定义主题可以继承默认主题的颜色，减少重复配置
3. **性能优化**: 使用 Zustand 的 selector 模式只订阅必要的主题状态
4. **可扩展性**: 设计时考虑未来可能的主题扩展，如字体大小、间距等
5. **无障碍支持**: 确保所有主题都符合无障碍标准，特别是颜色对比度

### 3.3 扩展建议

- 添加主题导入/导出功能
- 实现主题预览功能
- 支持基于系统主题自动切换
- 添加主题历史记录功能
- 实现主题分享功能

## 4. 总结

本案例实现了一个功能完整的多主题切换引擎，使用 Zustand 管理主题状态，集成了 Persist 和 Immer 中间件。通过 CSS 变量和 React 上下文提供者，实现了主题的全局应用和实时切换。该方案具有良好的可扩展性和维护性，可以轻松应用到各种 React 项目中，为用户提供个性化的主题体验。
