主题管理 #
什么是主题管理? #
主题管理是指对应用程序的外观样式进行统一管理和切换的能力,最常见的场景是深色模式和浅色模式切换。
text
┌─────────────────────────────────────────────────────────────┐
│ 主题管理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ 浅色主题 │ │ 深色主题 │ │
│ │ ┌─────────────────┐ │ │ ┌─────────────────┐ │ │
│ │ │ 背景: 白色 │ │ │ │ 背景: 深色 │ │ │
│ │ │ 文字: 黑色 │ │ │ │ 文字: 白色 │ │ │
│ │ │ 适合白天使用 │ │ │ │ 适合夜间使用 │ │ │
│ │ └─────────────────┘ │ │ └─────────────────┘ │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
基础函数 #
主题管理模块 #
javascript
const THEME_KEY = 'dools-theme';
export const getTheme = () => {
if (typeof window === 'undefined') return 'light';
const savedTheme = localStorage.getItem(THEME_KEY);
if (savedTheme && (savedTheme === 'light' || savedTheme === 'dark')) {
return savedTheme;
}
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
};
export const setTheme = (theme) => {
if (typeof window === 'undefined') return;
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem(THEME_KEY, theme);
};
export const toggleTheme = () => {
const currentTheme = getTheme();
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
return newTheme;
};
export const initTheme = () => {
const theme = getTheme();
setTheme(theme);
return theme;
};
export const watchSystemTheme = (callback) => {
if (typeof window === 'undefined') return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e) => {
const savedTheme = localStorage.getItem(THEME_KEY);
if (!savedTheme) {
const newTheme = e.matches ? 'dark' : 'light';
setTheme(newTheme);
callback && callback(newTheme);
}
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
};
基本使用 #
javascript
import { getTheme, setTheme, toggleTheme, initTheme } from './utils/theme';
initTheme();
const currentTheme = getTheme();
console.log(currentTheme);
setTheme('dark');
const newTheme = toggleTheme();
console.log(newTheme);
CSS 主题实现 #
使用 CSS 变量 #
css
:root {
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--text-primary: #333333;
--text-secondary: #666666;
--border-color: #e0e0e0;
--shadow-color: rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #ffffff;
--text-secondary: #b0b0b0;
--border-color: #404040;
--shadow-color: rgba(0, 0, 0, 0.3);
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
transition: background-color 0.3s, color 0.3s;
}
.card {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
box-shadow: 0 2px 8px var(--shadow-color);
}
使用 CSS 类名 #
css
.theme-light {
--bg-primary: #ffffff;
--text-primary: #333333;
}
.theme-dark {
--bg-primary: #1a1a1a;
--text-primary: #ffffff;
}
javascript
const setThemeByClass = (theme) => {
document.body.className = `theme-${theme}`;
localStorage.setItem(THEME_KEY, theme);
};
进阶用法 #
主题管理类 #
javascript
class ThemeManager {
constructor(options = {}) {
this.key = options.storageKey || 'app-theme';
this.defaultTheme = options.defaultTheme || 'light';
this.themes = options.themes || ['light', 'dark'];
this.listeners = [];
this.init();
}
init() {
const theme = this.get();
this.apply(theme);
this.watchSystem();
}
get() {
const saved = localStorage.getItem(this.key);
if (saved && this.themes.includes(saved)) {
return saved;
}
return this.getSystemPreference() || this.defaultTheme;
}
set(theme) {
if (!this.themes.includes(theme)) return;
this.apply(theme);
localStorage.setItem(this.key, theme);
this.notify(theme);
}
apply(theme) {
document.documentElement.setAttribute('data-theme', theme);
}
toggle() {
const current = this.get();
const currentIndex = this.themes.indexOf(current);
const nextIndex = (currentIndex + 1) % this.themes.length;
const nextTheme = this.themes[nextIndex];
this.set(nextTheme);
return nextTheme;
}
getSystemPreference() {
if (!window.matchMedia) return null;
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
return isDark ? 'dark' : 'light';
}
watchSystem() {
if (!window.matchMedia) return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', (e) => {
if (!localStorage.getItem(this.key)) {
this.set(e.matches ? 'dark' : 'light');
}
});
}
onChange(callback) {
this.listeners.push(callback);
return () => {
this.listeners = this.listeners.filter(cb => cb !== callback);
};
}
notify(theme) {
this.listeners.forEach(callback => callback(theme));
}
}
const themeManager = new ThemeManager({
storageKey: 'my-app-theme',
defaultTheme: 'light',
themes: ['light', 'dark']
});
themeManager.onChange((theme) => {
console.log('主题切换为:', theme);
});
多主题支持 #
javascript
class MultiThemeManager {
constructor() {
this.themes = {
light: {
name: '浅色',
colors: {
primary: '#3b82f6',
background: '#ffffff',
text: '#333333'
}
},
dark: {
name: '深色',
colors: {
primary: '#60a5fa',
background: '#1a1a1a',
text: '#ffffff'
}
},
ocean: {
name: '海洋',
colors: {
primary: '#0891b2',
background: '#ecfeff',
text: '#164e63'
}
},
forest: {
name: '森林',
colors: {
primary: '#059669',
background: '#ecfdf5',
text: '#064e3b'
}
}
};
}
applyTheme(themeName) {
const theme = this.themes[themeName];
if (!theme) return;
Object.entries(theme.colors).forEach(([key, value]) => {
document.documentElement.style.setProperty(`--color-${key}`, value);
});
localStorage.setItem('theme', themeName);
}
getThemeList() {
return Object.entries(this.themes).map(([key, value]) => ({
id: key,
name: value.name
}));
}
}
主题过渡动画 #
javascript
const setThemeWithTransition = (theme) => {
document.documentElement.classList.add('theme-transition');
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem(THEME_KEY, theme);
setTimeout(() => {
document.documentElement.classList.remove('theme-transition');
}, 300);
};
css
.theme-transition,
.theme-transition *,
.theme-transition *::before,
.theme-transition *::after {
transition: background-color 0.3s, color 0.3s, border-color 0.3s !important;
}
实际应用场景 #
1. React 主题 Hook #
javascript
import { useState, useEffect, useCallback } from 'react';
import { getTheme, setTheme, toggleTheme, watchSystemTheme } from './utils/theme';
export const useTheme = () => {
const [theme, setThemeState] = useState(getTheme);
useEffect(() => {
const cleanup = watchSystemTheme((newTheme) => {
setThemeState(newTheme);
});
return cleanup;
}, []);
const handleSetTheme = useCallback((newTheme) => {
setTheme(newTheme);
setThemeState(newTheme);
}, []);
const handleToggleTheme = useCallback(() => {
const newTheme = toggleTheme();
setThemeState(newTheme);
return newTheme;
}, []);
return {
theme,
setTheme: handleSetTheme,
toggleTheme: handleToggleTheme,
isDark: theme === 'dark'
};
};
const ThemeToggle = () => {
const { theme, toggleTheme, isDark } = useTheme();
return (
<button onClick={toggleTheme}>
{isDark ? '🌙 深色' : '☀️ 浅色'}
</button>
);
};
2. Vue 主题 Composable #
javascript
import { ref, watch, onMounted, onUnmounted } from 'vue';
import { getTheme, setTheme, toggleTheme, watchSystemTheme } from './utils/theme';
export const useTheme = () => {
const theme = ref(getTheme());
const handleSetTheme = (newTheme) => {
setTheme(newTheme);
theme.value = newTheme;
};
const handleToggleTheme = () => {
const newTheme = toggleTheme();
theme.value = newTheme;
};
let cleanup;
onMounted(() => {
cleanup = watchSystemTheme((newTheme) => {
theme.value = newTheme;
});
});
onUnmounted(() => {
cleanup && cleanup();
});
return {
theme,
setTheme: handleSetTheme,
toggleTheme: handleToggleTheme,
isDark: computed(() => theme.value === 'dark')
};
};
3. 主题切换按钮组件 #
javascript
class ThemeToggle {
constructor(options = {}) {
this.container = options.container || document.body;
this.onChange = options.onChange || (() => {});
this.render();
this.init();
}
render() {
this.element = document.createElement('button');
this.element.className = 'theme-toggle';
this.updateIcon();
this.container.appendChild(this.element);
this.element.addEventListener('click', () => {
const newTheme = toggleTheme();
this.updateIcon();
this.onChange(newTheme);
});
}
updateIcon() {
const theme = getTheme();
this.element.innerHTML = theme === 'dark' ? '☀️' : '🌙';
this.element.setAttribute('aria-label',
theme === 'dark' ? '切换到浅色模式' : '切换到深色模式'
);
}
init() {
initTheme();
watchSystemTheme(() => this.updateIcon());
}
}
new ThemeToggle({
container: document.getElementById('header'),
onChange: (theme) => console.log('主题切换:', theme)
});
最佳实践 #
1. 避免闪烁 #
html
<head>
<script>
(function() {
const theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
<link rel="stylesheet" href="styles.css">
</head>
2. 图片适配 #
css
.logo {
content: url('logo-light.png');
}
[data-theme="dark"] .logo {
content: url('logo-dark.png');
}
3. 图表适配 #
javascript
const getChartTheme = () => {
const isDark = getTheme() === 'dark';
return {
backgroundColor: isDark ? '#1a1a1a' : '#ffffff',
textColor: isDark ? '#ffffff' : '#333333',
gridColor: isDark ? '#404040' : '#e0e0e0'
};
};
下一步 #
现在你已经掌握了主题管理,接下来学习 加密哈希,了解 MD5 和 MD6 哈希算法!
最后更新:2026-04-04