从零开始同时手撸 React 和 Vue 后台 02 —— 明暗主题切换

154 阅读2分钟

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

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

ReactVue
仓库github.com/Levix0501/l…github.com/Levix0501/l…
预览next-admin.fecoder.cnvue-admin.fecoder.cn

往期:

实现思路

主题切换需要考虑:

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

Nextjs

antd

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

文档:ant-design.github.io/antd-style/…

import { ThemeProvider } from 'antd-style';

export default () => {
  return (
    <ThemeProvider themeMode={'light'}>
      <App />
    </ThemeProvider>
  );
};

Tailwind

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

文档:tailwindcss.com/docs/dark-m…

<!-- 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 文件

文档:element-plus.org/zh-CN/guide…

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

UnoCSS

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

文档:unocss.dev/presets/min…

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

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

文档:vueuse.org/core/useDar…

存储在 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 }
})

结语

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

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

ReactVue
仓库github.com/Levix0501/l…github.com/Levix0501/l…
预览next-admin.fecoder.cnvue-admin.fecoder.cn