Vue后台开发:从零实现明暗主题切换功能

发表时间: 2024-08-14 12:00

为了对比 React 和 Vue 在相同项目上的开发体验,我决定从零开始同时手撸 React 和 Vue 后台

代码仓库会和本系列文章同时更新,欢迎 fork,感谢 star

往期:

从零开始同时手撸 React 和 Vue 后台 01 —— 创建项目

实现思路

主题切换需要考虑:

  1. 组件库的主题切换
  2. Tailwind CSS / UnoCSS 的主题切换
  3. 配置的存储

Nextjs

antd

antd 的主题切换可以通过 antd-style 轻松实现

文档:https://ant-design.github.io/antd-style/guide/switch-theme

import { ThemeProvider } from 'antd-style';export default () => {  return (    <ThemeProvider themeMode={'light'}>      <App />    </ThemeProvider>  );};

Tailwind

Tailwind 的主题切换可以通过在 html 根元素的 class 上增删 dark 实现

文档:https://tailwindcss.com/docs/dark-mode

<!-- Dark mode not enabled --><html><body>  <!-- Will be white -->  <div class="bg-white dark:bg-black">    <!-- ... -->  </div></body></html><!-- Dark mode enabled --><html class="dark"><body>  <!-- Will be black -->  <div class="bg-white dark:bg-black">    <!-- ... -->  </div></body></html>

存储在 cookie 中

至于配置的存储,我们选择将其保存在 cookie 中,这样可以在服务端渲染时就能知道当前的主题,从而避免客户端生成的 HTML 和服务端生成的 HTML 不一致。

核心代码

完整代码请查看代码仓库

'use client';import { ReactNode, createContext, useEffect, useState } from 'react';import { appConfig } from '@/configs/appConfig';import { useCookieValue } from '../hooks/useCookieValue';import { useSystemTheme } from '../hooks/useSystemTheme';import { Settings } from '../types/settings';import { isDarkTheme } from '../utils/theme';export type SettingsContextProps = {  settings: Settings;  updateSettings: (settings: Partial<Settings>) => void;};type SettingsProviderProps = {  children: ReactNode;  serverSettingsCookie: Settings;};export const SettingsContext = createContext<SettingsContextProps | null>(null);export const SettingsProvider = ({  children,  serverSettingsCookie,}: SettingsProviderProps) => {  const systemTheme = useSystemTheme();  const [settingsCookie, updateSettingsCookie] = useCookieValue(    appConfig.settingsCookieName,    serverSettingsCookie  );  const [settings, setSettings] = useState<Settings>(settingsCookie);  const updateSettings = (_settings: Partial<Settings>) => {    const newVal = { ...settings, ..._settings };    setSettings(newVal);    updateSettingsCookie(newVal);  };  useEffect(() => {    updateSettings({      systemTheme,    });    // eslint-disable-next-line react-hooks/exhaustive-deps  }, [systemTheme]);  useEffect(() => {    if (      isDarkTheme(settings.themeMode ?? appConfig.defaultThemeMode, systemTheme)    ) {      document.documentElement.classList.add('dark');    } else {      document.documentElement.classList.remove('dark');    }  }, [settings.themeMode, systemTheme]);  return (    <SettingsContext.Provider value={{ settings: settings, updateSettings }}>      {children}    </SettingsContext.Provider>  );};

Vite-Vue

Element-plus

element-plus 的暗色模式只需在 html 上添加一个名为 dark 的类,同时引入 css 文件

文档:https://element-plus.org/zh-CN/guide/dark-mode.html

<html class="dark">  <head></head>  <body></body></html>
// main.tsimport 'element-plus/theme-chalk/dark/css-vars.css'

UnoCSS

unocss 的暗色模式也可以通过在 html 上添加一个名为 dark 的类来实现

文档:https://unocss.dev/presets/mini

于是,我们通过在 html 上增删 dark 类,可以同时切换element-plusunocss 的主题

vueuse 提供的 useDark 可以帮我们轻松实现在 html 上 增删 dark 类

文档:https://vueuse.org/core/useDark/

存储在LocalStorage中

对于 CSR 来说,没有特别的需求,所以我们将配置存储在 LocalStorage

核心代码

完整代码请查看代码仓库

import { appConfig } from '@/configs/appConfig'import type { Settings } from '../types/settings'import { isDarkTheme } from '../utils/theme'export const useSettings = defineStore('settings', () => {  const settings = useLocalStorage<Settings>(appConfig.settingsStorageName, {    themeMode: appConfig.defaultThemeMode,    systemTheme: appConfig.defaultSystemTheme  })  const isSystemThemeDark = usePreferredDark()  const isDark = useDark()  watch(    settings,    ({ themeMode, systemTheme }) => {      isDark.value = isDarkTheme(themeMode, systemTheme)    },    {      immediate: true,      deep: true    }  )  watch(    isSystemThemeDark,    (val) => {      settings.value.systemTheme = val ? 'dark' : 'light'    },    {      immediate: true    }  )  const updateSettings = (_settings: Partial<Settings>) => {    Object.assign(settings.value, _settings)  }  return { settings, updateSettings }})

结语

至此,我们实现了明暗主题切换

接下去,我们会实现基本色的动态切换