目录
前言
在现代 Web 应用中,深色模式已经成为了标配功能。一个优秀的深色模式实现不仅要提供良好的视觉体验,还需要解决页面闪烁、状态持久化、系统主题跟随等技术难题。本文将深入剖析 DjangoBlog 项目的深色模式实现方案,展示如何打造一个无闪烁、高性能、用户体验优秀的主题切换系统。
整体架构
DjangoBlog 采用了前端主导、后端辅助的架构设计,主题切换完全在前端实现,后端只负责配色方案的配置。整个系统可以分为三个层次:
- 前端逻辑层:使用 Alpine.js + 原生 JavaScript 实现主题切换核心逻辑
- 样式层:基于 Tailwind CSS + CSS 变量实现响应式主题样式
- 后端配置层:通过 Django 模型提供 8 种配色方案选择
值得注意的是,项目实现了双主题系统: - 配色方案(Color Scheme):purple/blue/green/orange/pink/red/indigo/teal - 明暗模式(Dark Mode):dark/light
两者相互独立,用户可以选择"紫色+深色"或"蓝色+浅色"等 16 种组合。
核心实现原理
1. 防闪烁技术:关键中的关键
问题:如果页面先显示默认主题,然后 JavaScript 加载后再切换到用户偏好的主题,会产生明显的闪烁,严重影响用户体验。
解决方案:在 <head> 标签中注入一段立即执行的内联脚本,在任何 CSS 加载前就确定并应用主题:
<script>
(function() {
const STORAGE_KEY = 'dark-mode-enabled';
function getPreferredTheme() {
// 优先级1:用户手动设置
const saved = localStorage.getItem(STORAGE_KEY);
if (saved !== null) return saved === 'dark' ? 'dark' : 'light';
// 优先级2:系统主题偏好
if (window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
// 优先级3:默认浅色
return 'light';
}
function applyTheme(theme) {
if (theme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
document.documentElement.classList.add('dark');
} else {
document.documentElement.removeAttribute('data-theme');
document.documentElement.classList.remove('dark');
}
}
// 立即应用主题
const theme = getPreferredTheme();
applyTheme(theme);
})();
</script>
这段脚本的关键点:
- ✅ 使用 IIFE(立即执行函数)确保立即运行
- ✅ 同步读取 localStorage,无异步延迟
- ✅ 直接操作 document.documentElement,在 DOM 解析早期就应用主题
- ✅ 同时设置 data-theme 属性和 dark 类名,兼容不同的 CSS 选择器
2. 主题状态管理
项目使用 localStorage 作为唯一的状态存储:
// 存储键
const STORAGE_KEY = 'dark-mode-enabled';
// 保存主题
localStorage.setItem(STORAGE_KEY, 'dark'); // 或 'light'
// 读取主题
const saved = localStorage.getItem(STORAGE_KEY);
为什么不使用后端? - ⚡ 即时响应:无需网络请求,切换立即生效 - 🔒 隐私友好:主题偏好无需上传服务器 - 📱 离线可用:即使离线也能正常切换 - 🚀 减轻服务器负载:每个主题切换不需要 API 调用
3. 核心 JavaScript 模块
darkMode.js 是整个深色模式的控制中心,提供了完整的 API:
// 全局 API
window.DarkMode = {
getCurrentTheme, // 获取当前主题
setTheme, // 设置主题('dark' 或 'light')
toggle // 切换主题
};
// 使用示例
window.DarkMode.toggle(); // 切换
window.DarkMode.setTheme('dark'); // 设置为深色
console.log(window.DarkMode.getCurrentTheme()); // 'dark'
核心函数实现:
function setTheme(theme) {
const validTheme = theme === 'dark' ? 'dark' : 'light';
// 1. 应用到 DOM
applyTheme(validTheme);
// 2. 保存到 localStorage
localStorage.setItem(STORAGE_KEY, validTheme);
// 3. 触发自定义事件
const event = new CustomEvent('themeChanged', {
detail: { theme: validTheme }
});
document.dispatchEvent(event);
}
4. CSS 样式系统
Tailwind CSS 配置
Tailwind 的深色模式配置非常简洁:
// tailwind.config.js
export default {
darkMode: ['selector', '[data-theme="dark"]'],
theme: {
extend: {
colors: {
primary: {
500: 'rgb(var(--color-primary-500) / <alpha-value>)',
// 使用 CSS 变量,支持动态主题切换
}
}
}
}
}
CSS 变量实现多配色方案
项目支持 8 种配色方案,通过 CSS 变量实现:
/* 默认紫色主题 */
:root {
--color-primary-500: 168, 85, 247;
--color-primary-600: 147, 51, 234;
}
/* 蓝色主题 */
:root[data-color-scheme="blue"] {
--color-primary-500: 59, 130, 246;
--color-primary-600: 37, 99, 235;
}
/* 使用时 */
.button {
background-color: rgb(var(--color-primary-500));
}
Tailwind 的 dark: 前缀
在整个项目中,大量使用 Tailwind 的 dark: 前缀:
/* 卡片样式 */
.card {
@apply bg-white dark:bg-gray-800;
@apply border-gray-200 dark:border-gray-700;
@apply text-gray-900 dark:text-gray-100;
}
/* 输入框 */
.input {
@apply bg-white dark:bg-gray-800;
@apply border-gray-300 dark:border-gray-600;
@apply focus:ring-primary-500 dark:focus:ring-primary-400;
}
5. 系统主题跟随
项目实现了智能的系统主题跟随功能:
function setupSystemThemeListener() {
if (!window.matchMedia) return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const listener = function(e) {
// 只有在用户未手动设置时才跟随系统
if (localStorage.getItem(STORAGE_KEY) === null) {
setTheme(e.matches ? 'dark' : 'light');
}
};
// 监听系统主题变化
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', listener);
}
}
设计哲学:用户手动选择优先于系统设置。一旦用户明确选择了主题,就不再自动跟随系统变化。
6. 用户交互体验
主题切换按钮
页面右上角有一个固定定位的切换按钮:
<button type="button"
class="dark-mode-toggle-btn"
@click="window.DarkMode && window.DarkMode.toggle()">
<span class="icon-light">☀️</span>
<span class="icon-dark">🌙</span>
</button>
按钮样式包含精美的动画效果:
.dark-mode-toggle-btn {
@apply hover:scale-110 hover:rotate-12;
@apply transition-all duration-300;
@apply shadow-lg hover:shadow-xl;
}
键盘快捷键
支持 Ctrl/Cmd + Shift + D 快速切换主题:
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'D') {
e.preventDefault();
toggleTheme();
}
});
平滑过渡动画
所有主题切换都有 300ms 的平滑过渡:
* {
transition: background-color 0.3s ease,
border-color 0.3s ease,
color 0.3s ease;
}
主题切换完整流程
页面首次加载
用户访问页面
↓
HTML 解析,立即执行防闪烁脚本
↓
读取 localStorage 或检测系统主题
↓
设置 <html data-theme="dark/light">
↓
CSS 加载,根据 data-theme 应用样式
↓
JavaScript 完全加载,初始化完整功能
↓
绑定按钮、快捷键、系统监听
↓
页面完全渲染(无闪烁)
用户点击切换
点击切换按钮
↓
调用 window.DarkMode.toggle()
↓
getCurrentTheme() → 'light'
↓
setTheme('dark')
├─ applyTheme('dark') → 修改 DOM
├─ localStorage.setItem('dark-mode-enabled', 'dark')
└─ 触发 'themeChanged' 事件
↓
CSS 过渡动画(300ms)
↓
切换完成
后端配色方案配置
虽然深色模式完全由前端控制,但配色方案由后端提供配置:
# blog/models.py
class BlogSettings(models.Model):
COLOR_SCHEMES = (
('purple', '紫色主题 - Purple Dream'),
('blue', '蓝色主题 - Ocean Blue'),
('green', '绿色主题 - Forest Green'),
# ... 更多配色
)
color_scheme = models.CharField(
max_length=20,
choices=COLOR_SCHEMES,
default='purple'
)
通过 Context Processor 传递到模板:
# blog/context_processors.py
def seo_processor(requests):
value = {
'COLOR_SCHEME': setting.color_scheme,
# ...
}
cache.set(key, value, 60 * 60 * 10) # 缓存 10 小时
return value
在模板中使用:
<body data-color-scheme="{{ COLOR_SCHEME|default:'purple' }}">
结语
DjangoBlog 的深色模式实现展示了如何在现代 Web 应用中打造一个完整、流畅、高性能的主题切换系统。通过防闪烁技术、前端状态管理、CSS 变量、系统主题跟随等技术的组合,实现了优秀的用户体验。
这套方案不仅适用于 Django 项目,其核心思想和技术细节可以应用到任何现代 Web 框架中。希望本文能为你的项目提供参考和启发。
技术栈:Django + Alpine.js + Tailwind CSS + Vite
核心文件:
- frontend/src/features/darkMode.js - 深色模式核心逻辑
- frontend/src/styles/main.css - 主题样式
- templates/share_layout/base.html - 防闪烁脚本
- frontend/tailwind.config.js - Tailwind 配置
在线演示:DjangoBlog
本文由 liangliangyy 原创,转载请注明出处。
评论
0