news 2026/9/15 22:02:55

深入解析 Scalar useColorMode Hook:Vue 应用中的暗色/亮色模式状态管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入解析 Scalar useColorMode Hook:Vue 应用中的暗色/亮色模式状态管理

深入解析 Scalar useColorMode Hook:Vue 应用中的暗色/亮色模式状态管理

【免费下载链接】scalarScalar is an open-source API platform: 🌐 Modern REST API Client 📖 Beautiful API References ✨ 1st-Class OpenAPI/Swagger Support项目地址: https://gitcode.com/GitHub_Trending/sc/scalar

导读

@scalar/use-hooks是 Scalar 开源 API 平台(REST API 客户端、API 文档与 OpenAPI 工具链)的 Vue 组合式函数(composable hook)集合,useColorMode是其中负责主题色彩模式(color mode)的核心钩子。它统一处理系统偏好检测、localStorage 持久化、模式切换与 CSS 类应用,让 Vue 组件可以零成本接入暗色/亮色主题体系。读完本文,你将掌握useColorMode的完整 API、参数优先级规则、SSR/SSG 下的水合(hydration)安全策略,以及它与@scalar/helpers/theme底层工具的分工原理。

功能概览:一个组合式钩子解决暗色模式全链路

useColorMode是一个基于 Vue 3 Composition API 的组合式函数,其定位在 useColorMode/README.md 中描述得非常清楚:

A composable hook that provides color mode (dark/light) functionality. Handles system preferences, local storage persistence, and provides methods to toggle and set the color mode. Automatically applies appropriate CSS classes to enable theme switching.

翻译过来即:它提供色彩模式(暗色/亮色)功能,处理系统偏好、本地存储持久化,提供切换(toggle)与设置(set)方法,并自动应用合适的 CSS 类以启用主题切换。也就是说,一个钩子覆盖了暗色模式从「读取偏好 → 解析决策 → 持久化 → 应用到 DOM」的完整链路。

其核心源码位于 useColorMode.ts,对外导出通过 index.ts 完成,同时重新导出了ColorModeDarkLightMode两个类型。

安装与导入

useColorMode@scalar/use-hooks包一起发布,安装方式(见 packages/use-hooks/README.md):

npm add @scalar/use-hooks

按需导入:

import { useColorMode } from '@scalar/use-hooks/useColorMode'

基本用法:一行代码接入主题

在原文档给出的最简用法中,只需在<script setup>中调用useColorMode()即可。调用后钩子会自动监听色彩模式变化,并把对应的 CSS 类应用到页面上:

<script setup lang="ts"> import { useColorMode } from '@scalar/use-hooks/useColorMode' // Watches for changes in the color mode and applies the appropriate CSS classes useColorMode() </script> <template> <!-- Template goes here --> </template>

从源码(useColorMode.ts)可以看到,调用时钩子会立即完成一次模式初始化并建立响应式监听:

colorMode.value = overrideColorMode ?? savedColorMode ?? initialColorMode // Watch for color mode or system preference changes and update the body class. watch([colorMode, systemPreference], applyBodyColorMode, { immediate: true })

watchimmediate: true意味着挂载即应用一次类名,之后任何色彩模式或系统偏好的变化都会同步刷新 DOM。

返回值 API 详解

useColorMode返回 6 个成员(useColorMode.ts),其中多个是可写(writable)的,赋值即触发持久化与 DOM 更新:

返回值类型说明
colorModeWritableComputedRef<ColorMode>当前模式,可选'light' \| 'dark' \| 'system',赋值即调用setColorMode
darkLightModeWritableComputedRef<DarkLightMode>已解析的暗/亮模式('light' \| 'dark'),system会被替换为系统偏好值
isDarkModeWritableComputedRef<boolean>当前是否为暗色模式,赋布尔值等价于setColorMode(value ? 'dark' : 'light')
toggleColorMode() => void在暗/亮之间切换并写入 localStorage
setColorMode(value: ColorMode) => void设置指定模式并写入 localStorage
getSystemModePreference() => DarkLightMode读取操作系统偏好('light' \| 'dark'

关键设计:system是偏好而非渲染值

源码中定义了三种可选项:

/** Possible color modes */ export type ColorMode = 'light' | 'dark' | 'system'

而底层@scalar/helpers/theme/color-mode中的DarkLightMode只有'light' | 'dark'两种。system之所以不能直接用于渲染,是因为它只是一个「待解析的偏好」——必须先在运行时对操作系统查询一次(prefers-color-scheme),才能知道最终该渲染成什么颜色。因此darkLightMode这个可写计算属性充当了「解析层」:

const darkLightMode = computed<DarkLightMode>({ get: () => (colorMode.value === 'system' ? systemPreference.value : colorMode.value), set: setColorMode, })

这意味着:当colorModesystem时,darkLightMode返回的始终是lightdark中的某一个;业务组件只需绑定darkLightMode/isDarkMode,就无需关心用户到底选了「跟随系统」还是固定模式。

切换与设置:一次赋值,双写状态

toggleColorModesetColorMode的实现(useColorMode.ts)遵循「先改内存状态、再写 localStorage」的模式,存储键名为colorMode

function toggleColorMode() { // Update state colorMode.value = darkLightMode.value === 'dark' ? 'light' : 'dark' // Store in local storage if (typeof window === 'undefined') { return } window?.localStorage?.setItem('colorMode', colorMode.value) } function setColorMode(value: ColorMode) { colorMode.value = value if (typeof window === 'undefined') { return } window?.localStorage?.setItem('colorMode', colorMode.value) }

注意其中的typeof window === 'undefined'守卫:在服务端渲染(SSR/SSG)环境下,两个方法会安静地只更新内存状态而跳过存储写入,从而避免直接抛错。

选项参数与优先级规则

useColorMode接受一个可选对象,包含两个参数(useColorMode.ts):

export function useColorMode( opts: { /** The initial color mode to use */ initialColorMode?: ColorMode /** Override the color mode */ overrideColorMode?: ColorMode } = {}, ) { const { initialColorMode = 'system', overrideColorMode } = opts // ... }
  • initialColorMode:默认'system'。当 localStorage 中没有历史记录时的初始模式。
  • overrideColorMode:默认undefined。一旦传入,将强制锁定模式,忽略 localStorage 与initialColorMode

初始化的优先级在源码注释中写得非常明确(useColorMode.ts):

Priority: overrideColorMode -> localStorage -> initialColorMode

即:强制覆盖参数 > 用户上次保存的偏好 > 默认初始模式。这一规则在测试中得到了完整验证(useColorMode.test.ts):

  • initialColorMode会被 localStorage 值覆盖(initialColorMode is overridden by localStorage value);
  • overrideColorMode优先级最高,即使调用setColorMode('light')或系统偏好为 light,body 上依然是dark-mode类(respects overrideColorMode option)。

localStorage 值的安全性

从 localStorage 读出的字符串会先经过@scalar/validation的 schema 校验(union([literal('system'), literal('dark'), literal('light')])),只有合法值才会被采纳,非法值(如foobar)会被忽略并回退到initialColorMode(useColorMode.ts)。对应的测试handles unknown localStorage values确认了这一点:存入非法值时调用钩子不会抛异常。

CSS 类应用机制与底层 helpers

useColorMode本身不直接操作类名,而是把脏活交给@scalar/helpers/theme/color-mode。这正是 Scalar 主题体系的一个关键设计:每个主题的暗色/亮色模式都是成对的一组类选择器,而非媒体查询(packages/helpers/src/theme/README.md)。

applyColorMode的实现如下(color-mode.ts):

const COLOR_MODE_CLASSES = { light: 'light-mode', dark: 'dark-mode', } as const satisfies Record<DarkLightMode, string> export const applyColorMode = (mode: DarkLightMode, target: HTMLElement = document.body): void => { target.classList.toggle(COLOR_MODE_CLASSES.dark, mode === 'dark') target.classList.toggle(COLOR_MODE_CLASSES.light, mode === 'light') }

两个类都会被 toggle(而不是只添加其中一个),这样元素永远不会同时携带light-modedark-mode;目标元素默认为document.bodyuseColorMode中对应的applyBodyColorMode(useColorMode.ts)会优先使用overrideColorMode,其次使用「已解析」后的暗/亮值,再调用applyColorMode完成类名交换:

const classMode = overrideColorMode ?? (colorMode.value === 'system' ? systemPreference.value : colorMode.value) applyColorMode(classMode === 'dark' ? 'dark' : 'light')

系统偏好的读取

getSystemColorMode负责读取操作系统偏好(color-mode.ts),行为分三档:

  • window(SSR):返回'light',与服务端渲染结果一致;
  • window但无matchMedia:返回'dark'(该分支在真实浏览器几乎不可达,主要是 stub 测试环境);
  • 正常浏览器:通过window.matchMedia('(prefers-color-scheme: dark)')?.matches判定'dark''light'

SSR / SSG 下的水合安全策略

这是useColorMode最讲究的实现细节之一。模块级共享了systemPreferenceref,初始值故意设为'light'(useColorMode.ts):

It defaults to'light'so the first client render matches the server, wherewindow/matchMediado not exist. We resolve the real value inonMountedto avoid a hydration mismatch.

即:首屏客户端渲染必须与服务端渲染一致(服务端没有window/matchMedia,只能渲染 light),真实系统偏好推迟到onMounted生命周期再解析,从而避免水合不一致。同样地,无window时 localStorage 读取会直接视为'system',不会错误地回退到initialColorMode(否则服务端与客户端初始值会分叉)。

对应的测试验证了完整的时序行为(useColorMode.test.ts):

  • defers the system preference to onMounted to stay hydration-safe:挂载前darkLightMode'light'(与服务端一致),挂载后才升级为真实的暗色偏好;
  • keeps the body class aligned with the toggle before mount:挂载前 body 类与亮色 toggle 保持一致,挂载后 body 类与 toggle 同步升级。

在挂载时,钩子还会建立系统偏好监听(useColorMode.ts),并在卸载时清理:

onMounted(() => { systemPreference.value = getSystemModePreference() if (typeof window !== 'undefined' && typeof window?.matchMedia === 'function') { mediaQuery.value = window.matchMedia('(prefers-color-scheme: dark)') mediaQuery.value?.addEventListener('change', handleChange) } }) onUnmounted(() => { mediaQuery.value?.removeEventListener('change', handleChange) })

一旦用户操作系统在运行时切换深浅色,handleChange会更新systemPreference,进而触发watch重新应用 body 类。测试listens to system preference changes模拟了这一过程:系统偏好从 light 变为 dark 后,body 类随之从light-mode切换为dark-mode

模块级共享状态:多实例一致性

colorModesystemPreference都声明在模块作用域(module scope),而非函数内部。这意味着所有useColorMode实例共享同一份状态与同一次系统解析结果。测试shares the resolved system preference across instances证明:即使第一个实例尚未挂载,当第二个实例解析出真实系统偏好后,第一个实例的darkLightMode也会立即同步为正确的值——页面中多个组件各自调用该钩子时不会出现状态分叉。

实际应用:API 参考组件中的主题接入

useColorMode已在 Scalar 自身的核心产品中投入使用。例如 API 参考渲染组件 ApiReference.vue 就调用了该钩子来驱动文档界面在暗色/亮色主题间切换。这也印证了该钩子的通用性:凡是需要跟随 Scalar 主题体系的 Vue 组件,都可以直接复用,无需各自重复实现偏好检测与持久化逻辑。

测试与可靠性保障

该钩子配套了非常完整的 Vitest 测试套件(useColorMode.test.ts,共 14 组用例),覆盖了:

  • 默认值为system、localStorage 优先级、非法存储值容错;
  • toggleColorMode/setColorMode的状态与存储双写;
  • colorMode/isDarkMode可写计算属性的赋值行为;
  • 系统偏好检测与动态变化监听;
  • body 类名(dark-mode/light-mode)的正确应用与互斥;
  • initialColorModeoverrideColorMode的优先级;
  • window/document的 SSG 环境不抛错;
  • 水合安全时序与多实例共享状态;
  • matchMedia缺失时的优雅降级。

这些用例同时充当了行为契约文档:任何对该钩子的重构都必须保持上述语义不变。

小结

useColorMode用约 140 行代码把暗色模式涉及的「系统偏好解析、localStorage 持久化、优先级决策、CSS 类应用、SSR 水合安全、多实例共享、监听与清理」全部封装完毕。使用时只需遵循两条核心规则:默认system跟随系统、优先级为 override > localStorage > initial。对于需要在 Vue 应用中构建主题切换能力的开发者,这个钩子及其测试套件本身就是一份高质量的实现范本。

【免费下载链接】scalarScalar is an open-source API platform: 🌐 Modern REST API Client 📖 Beautiful API References ✨ 1st-Class OpenAPI/Swagger Support项目地址: https://gitcode.com/GitHub_Trending/sc/scalar

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 22:01:32

汕头建站模板搭建避坑指南:3种方案对比

汕头建站模板搭建避坑指南:3种方案对比 别再被那些一眼假、加载慢、还容易出bug的模板网站坑了。很多汕头老板花了几千块买模板,结果上线三个月,客户问为什么网站打不开,SEO排名还掉到首页外。…

作者头像 李华
网站建设 2026/9/15 21:56:20

Qt/C++贪吃蛇游戏开发:从游戏循环到碰撞检测的工程实践

简介&#xff1a;基于Qt与C的贪吃蛇游戏毕业设计完整方案&#xff0c;适合计算机相关专业学生完成课程设计或毕业设计时参考。压缩包内含可编译运行的源码、毕业设计论文、任务书以及答辩PPT&#xff0c;从需求分析、总体设计到编码实现均有对应文档支撑&#xff1b;论文中对技…

作者头像 李华
网站建设 2026/9/15 21:55:30

拒绝丑模板!汕头建站模板搭建的3个最佳实践避坑指南

拒绝丑模板!汕头建站模板搭建的3个最佳实践避坑指南 别再盯着那些一眼假、加载慢、还改不动的免费模板发愁了。很多老板觉得建站就是下个模板换个Logo,结果上线后客户直摇头,说像十年前网页,更别提转化了。这就是典型的“模板网站太丑不够用”。…

作者头像 李华