一、为什么需要深色模式自动切换
现在很多操作系统都支持深色模式了,比如Windows 10/11的夜间模式、macOS的暗色主题。作为一款现代化的桌面应用,如果能够跟随系统自动切换深色/浅色模式,用户体验会好很多。想象一下,当用户把系统切换到深色模式时,你的应用也能自动变成暗色系,这种无缝衔接的感觉特别棒。
Electron作为跨平台桌面应用开发框架,天然支持这个功能。不过要实现得优雅,还是需要一些技巧的。下面我们就来详细聊聊具体怎么做。
二、检测系统主题变化的核心API
Electron提供了nativeTheme模块来获取和监听系统主题变化。这个模块在渲染进程和主进程都能使用,但最佳实践是在主进程监听,然后通知渲染进程。
// 主进程代码 (main.js)
const { app, BrowserWindow, nativeTheme } = require('electron')
let mainWindow
app.whenReady().then(() => {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
// 监听系统主题变化
nativeTheme.on('updated', () => {
updateTheme()
})
function updateTheme() {
const isDarkMode = nativeTheme.shouldUseDarkColors
mainWindow.webContents.send('theme-changed', isDarkMode)
}
// 初始化时发送一次当前主题
updateTheme()
})
三、渲染进程的主题切换实现
收到主进程的通知后,渲染进程需要做两件事:修改CSS样式和保存用户偏好。这里我们使用CSS变量来实现主题切换,这样维护起来更方便。
// 渲染进程代码 (renderer.js)
const { ipcRenderer } = require('electron')
// 监听主题变化
ipcRenderer.on('theme-changed', (event, isDarkMode) => {
setTheme(isDarkMode)
saveThemePreference(isDarkMode)
})
function setTheme(isDarkMode) {
const root = document.documentElement
if (isDarkMode) {
root.style.setProperty('--bg-color', '#1e1e1e')
root.style.setProperty('--text-color', '#ffffff')
root.style.setProperty('--primary-color', '#64b5f6')
} else {
root.style.setProperty('--bg-color', '#ffffff')
root.style.setProperty('--text-color', '#333333')
root.style.setProperty('--primary-color', '#1976d2')
}
}
function saveThemePreference(isDarkMode) {
localStorage.setItem('darkMode', isDarkMode)
}
// 初始化时检查本地存储的用户偏好
const savedMode = localStorage.getItem('darkMode')
if (savedMode !== null) {
setTheme(savedMode === 'true')
}
对应的CSS可以这样写:
/* styles.css */
:root {
--bg-color: #ffffff;
--text-color: #333333;
--primary-color: #1976d2;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
transition: all 0.3s ease;
}
button {
background-color: var(--primary-color);
color: white;
}
四、处理用户自定义主题偏好
有时候用户可能想覆盖系统设置,单独为你的应用指定主题。这时候我们需要在设置界面添加切换选项,并处理好与系统自动切换的优先级关系。
// 设置界面代码 (settings.js)
document.getElementById('dark-mode-toggle').addEventListener('change', (e) => {
const useDarkMode = e.target.checked
ipcRenderer.send('set-theme-manually', useDarkMode)
localStorage.setItem('theme-override', useDarkMode ? 'dark' : 'light')
})
// 主进程处理手动设置
ipcMain.on('set-theme-manually', (event, useDarkMode) => {
nativeTheme.themeSource = useDarkMode ? 'dark' : 'light'
updateTheme()
})
这里有个关键点:nativeTheme.themeSource可以设置为dark、light或system。当设置为system时会跟随系统,设置为另外两个值则会覆盖系统设置。
五、完整实现方案的最佳实践
结合前面几点,这里给出一个更完整的实现方案:
- 应用启动时,检查本地存储是否有用户手动设置的主题偏好
- 如果没有手动设置,则跟随系统主题
- 监听系统主题变化,但如果用户有手动设置则忽略
- 提供设置界面允许用户切换"跟随系统"或手动选择主题
// 增强版主进程代码
const { app, BrowserWindow, nativeTheme, ipcMain } = require('electron')
const Store = require('electron-store')
const schema = {
theme: {
type: 'string',
enum: ['system', 'dark', 'light'],
default: 'system'
}
}
const store = new Store({ schema })
app.whenReady().then(() => {
// 应用启动时应用存储的主题设置
applyThemeSetting()
// 监听系统主题变化(仅在theme=system时生效)
nativeTheme.on('updated', () => {
if (store.get('theme') === 'system') {
updateTheme()
}
})
// 处理渲染进程的主题设置请求
ipcMain.handle('get-theme', () => {
return {
preference: store.get('theme'),
current: nativeTheme.shouldUseDarkColors
}
})
ipcMain.on('set-theme', (event, theme) => {
store.set('theme', theme)
applyThemeSetting()
})
})
function applyThemeSetting() {
const theme = store.get('theme')
nativeTheme.themeSource = theme === 'system' ? 'system' : theme
updateTheme()
}
function updateTheme() {
const isDarkMode = nativeTheme.shouldUseDarkColors
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send('theme-changed', isDarkMode)
})
}
六、可能遇到的问题和解决方案
- Flash of Unstyled Content (FOUC)
在页面加载初期可能会有短暂的主题闪烁。解决方法是在HTML中内联关键CSS,或者使用preload脚本在页面加载前设置初始主题。
// preload.js
const { ipcRenderer } = require('electron')
document.addEventListener('DOMContentLoaded', () => {
ipcRenderer.send('get-initial-theme')
})
ipcRenderer.on('set-initial-theme', (event, isDarkMode) => {
const root = document.documentElement
// 和内联CSS相同的逻辑
})
- 第三方UI库的兼容性
如果你使用了像Material-UI这样的UI库,它们可能有自己的主题系统。这时需要将Electron的主题变化传递给这些库。
// 对于Material-UI
import { createTheme, ThemeProvider } from '@material-ui/core/styles'
function App() {
const [darkMode, setDarkMode] = useState(false)
useEffect(() => {
window.electron.onThemeChanged(setDarkMode)
return () => window.electron.offThemeChanged(setDarkMode)
}, [])
const theme = createTheme({
palette: {
type: darkMode ? 'dark' : 'light'
}
})
return (
<ThemeProvider theme={theme}>
{/* 你的组件 */}
</ThemeProvider>
)
}
七、进阶技巧:根据时间自动切换
除了跟随系统设置,我们还可以实现根据日出日落时间自动切换。这需要使用地理位置和日出日落时间API。
// 主进程中
const SunCalc = require('suncalc')
function calculateSunTimes() {
// 获取用户位置(简化示例,实际需要用户授权)
const latitude = 39.9042 // 北京
const longitude = 116.4074
const times = SunCalc.getTimes(new Date(), latitude, longitude)
return {
sunrise: times.sunrise,
sunset: times.sunset
}
}
function startThemeScheduler() {
const { sunrise, sunset } = calculateSunTimes()
const now = new Date()
if (now > sunset || now < sunrise) {
nativeTheme.themeSource = 'dark'
} else {
nativeTheme.themeSource = 'light'
}
// 每天重新计算
setInterval(() => {
const now = new Date()
if (now.getHours() === 0 && now.getMinutes() === 0) {
startThemeScheduler()
}
}, 60 * 1000)
}
八、总结与最佳实践建议
实现一个完善的深色模式自动切换功能需要考虑很多细节。以下是我的几点建议:
- 始终尊重用户选择,提供"跟随系统"、"始终深色"、"始终浅色"三个选项
- 使用CSS变量管理主题颜色,方便维护和扩展
- 处理好初始加载时的主题闪烁问题
- 对于复杂应用,考虑使用状态管理工具(如Redux)来管理主题状态
- 测试在不同操作系统下的表现,特别是边界情况
深色模式不仅仅是颜色反转,更是一种用户体验的全面提升。通过Electron提供的API,我们可以相对容易地实现这个功能,让应用看起来更专业、用起来更舒适。
评论