痛点:刷新闪烁

最朴素的暗色模式实现,是在按钮里直接 document.documentElement.classList.toggle('dark')。它能用,但有两个问题:

  1. 刷新闪烁(FOUC):页面加载时先显示浅色,再被脚本切回暗色。
  2. 状态分散:多个组件各管各的,状态不同步。

解决:组合式 API + 持久化

把逻辑抽成一个 useTheme 组合式函数,借助 Nuxt 的 useState 在全站共享同一份响应式状态:

export const useTheme = () => {
  const theme = useState<'light' | 'dark'>('theme', () => 'light')

  const toggle = () => {
    theme.value = theme.value === 'light' ? 'dark' : 'light'
    applyTheme()
    persist()
  }

  // 初始化时从 localStorage 读取,并立即写入 <html> class
  const initTheme = () => { /* ... */ }
  return { theme, toggle, initTheme }
}

配合 CSS 变量

JS 只负责切换一个 dark 类名,所有颜色变化交给 CSS 变量完成。这样切换瞬间完成、零重排开销:

:root { --c-bg: #fbfbfa; }
html.dark { --c-bg: #0f1115; }
body { background: var(--c-bg); }

避免 FOUC 的关键

app.vuesetup 阶段(最早执行的脚本)就调用 initTheme(),把正确的类名写到 <html> 上——在浏览器绘制第一帧之前就完成主题应用,用户便感知不到闪烁。

这一套组合拳,正是 Vue 3 "逻辑复用"理念的最小而美示例。