<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          如何實現(xiàn)網(wǎng)站自定義主題切換

          共 18536字,需瀏覽 38分鐘

           ·

          2024-07-10 08:49

              

          大廠技術(shù)  高級前端  Node進階

          點擊上方 程序員成長指北,關(guān)注公眾號

          回復(fù)1,加入高級Node交流群

          哈嘍大家好,我是Koala。

          很多網(wǎng)站或者管理系統(tǒng)都支持主題切換,包括明暗風(fēng)格的切換,皮膚切換,甚至自定義主題切換。主題的切換可以帶來更好的用戶體驗和提升個性化,增強可視化效果和情感感受。也是常用的功能之一了。

          那如何去實現(xiàn)一個自定義主題的網(wǎng)站呢?今天這篇文章給你帶來詳細的解密!

          以下是正文:


          最近準備用 React + Antd + UnoCSS 開發(fā)一個和 NestJS Admin 配套的系統(tǒng),想加個自定義主題功能,這里(https://zb81.github.io/my-theme)體驗。

          一、需求

          • 用戶可以自定義主題顏色,需要實時響應(yīng);
          • 用戶可以切換明暗模式,需要實時改變背景和文字顏色;
          • 當用戶切換系統(tǒng)主題時,網(wǎng)頁需要作出響應(yīng);
          • 主題顏色和明暗模式需要緩存至 localStorage

          二、準備工作

          這里使用的是 pnpm,用 npm 或 yarn 等包管理工具的記得替換命令。

          1. 創(chuàng)建項目

          首先,拉取 vite 模板:

          pnpm create vite my-theme --template react-ts

          清空 src 目錄:

          image.png
          // main.tsx
          import React from 'react'
          import ReactDOM from 'react-dom/client'
          import App from './App.tsx'

          ReactDOM.createRoot(document.getElementById('root')!).render(
          <React.StrictMode>
          <App />
          </React.StrictMode>,
          )
          // App.tsx
          function App() {
          return (
          <div>App</div>
          )
          }

          export default App

          啟動項目:

          pnpm run dev

          2. 安裝 Antd

          pnpm add antd
          // App.tsx
          import { Button } from 'antd'

          function App() {
          return (
          <div>
          <Button type="primary">123</Button>
          <Button>zzz</Button>
          </div>
          )
          }

          export default App
          image.png

          3. 安裝并配置 UnoCSS

          開始之前,先推薦兩個 VSCode 插件:

          • UnoCSS

            unocss-ext.png

            這個插件會讀取 uno.config.ts ,提供了類名的提示以及預(yù)覽:

            image.png
          • Iconify IntelliSense

            icon-ext.png

            這個插件提供了圖標名稱的提示和預(yù)覽功能:

            image.png

          1) 安裝并引入

          因為后續(xù)會用的 CSS 圖標,這里順帶安裝一下圖標庫。(體積很大,70M)

          pnpm add unocss @iconify/json -D

          配置 vite.config.ts

          // vite.config.ts
          import { defineConfig } from 'vite'
          import react from '@vitejs/plugin-react'
          import UnoCSS from 'unocss/vite'

          // https://vitejs.dev/config/
          export default defineConfig({
            plugins: [react(), UnoCSS()],
          })

          main.tsx 中引入:

          // main.tsx
          import 'virtual:uno.css'

          2) 配置文件

          在項目根目錄創(chuàng)建 uno.config.ts 配置文件:

          // uno.config.ts
          import { defineConfig, presetIcons, presetUno } from 'unocss'

          export default defineConfig({
            presets: [
              presetUno(),
              presetIcons({
                extraProperties: {
                  'display''inline-block',
                  'height''1.2em',
                  'width''1.2em',
                  'vertical-align''middle',
                },
                warn: true,
              }),
            ],
          })

          更多配置選項,請閱讀 UnoCSS 文檔(https://unocss.dev/config/)。

          3) 樣式重置

          pnpm add @unocss/reset

          main.tsx 中引入:

          import '@unocss/reset/tailwind-compat.css'

          4) 測試

          App.tsx 中隨便寫點代碼:

          // App.tsx
          import { Button } from 'antd'

          function App() {
          return (
          <div>
          <Button type="primary">123</Button>
          <Button>zzz</Button>

          <h1 className='mt-5 text-[red] text-10'>Hello, world!</h1>
          <div className='text-[green] text-20'>
          <div className='i-mdi:vuejs'></div>
          <div className='i-mdi:twitter'></div>
          </div>
          </div>
          )
          }

          export default App
          image.png

          三、需求實現(xiàn)

          1. 自定義主題顏色

          1) 組件引入并綁定狀態(tài)

          // App.tsx
          import { Button, ColorPicker } from "antd";
          import { useState } from "react";

          function App() {
          const [primaryColor, setPrimaryColor] = useState("#01bfff");

          return (
          <div className="p-4 flex items-center gap-x-3 mb-4">
          <ColorPicker
          value={primaryColor}
          onChange={(_, c) => setPrimaryColor(c)}
          />

          <span>{primaryColor}</span>

          <Button type="primary">123</Button>
          <Button>zzz</Button>
          </div>
          );
          }

          export default App;
          image.png

          2) 和 Antd 組件同步

          新版本的 Antd 采用了 CSS-in-JS 方案以及梯度變量演變算法,只需要提供一個基礎(chǔ)變量 colorPrimary ,主題相關(guān)的其它配色就能推算出來,比如按鈕點擊的波紋顏色等等。

          所以,我們只需要將 primaryColor 通過ConfigProvider提供給 Antd 就可以了:

          // App.tsx
          import { Button, ColorPicker, ConfigProvider, ThemeConfig } from "antd";
          import { useState } from "react";

          function App() {
          const [primaryColor, setPrimaryColor] = useState("#01bfff");

          const antdTheme: ThemeConfig = {
          token: { colorPrimary: primaryColor },
          };

          return (
          <ConfigProvider theme={antdTheme}>
          <div className="p-4">
          <div className="flex items-center gap-x-3 mb-4">
          <ColorPicker
          value={primaryColor}
          onChange={(_, c) => setPrimaryColor(c)}
          />

          <span>{primaryColor}</span>

          <Button type="primary">123</Button>
          <Button>zzz</Button>
          </div>
          </div>
          </ConfigProvider>
          );
          }

          export default App;
          antd.gif

          3) 和其他顏色同步

          這里使用 CSS 變量的方案來保持顏色同步:

          • 給根元素添加一個 CSS 變量 --primary-color;
          • 給 UnoCSS 添加一個顏色 primary: 'var(--primary-color)'
          • 添加一個副作用,讓 primaryColor--primary-color 保持同步。
          // uno.config.ts
          import { defineConfig, presetIcons, presetUno } from 'unocss'

          export default defineConfig({
            theme: {
              colors: {
                primary: 'var(--primary-color)'// 這里定義了一個顏色,通過 `text-primary` 的方式使用
              },
            },
            // ... rest config
          })
          // App.tsx
          import { Button, ColorPicker, ConfigProvider, ThemeConfig } from "antd";
          import { useEffect, useState } from "react";

          function App() {
          const [primaryColor, setPrimaryColor] = useState("#01bfff");

          useEffect(() => {
          document.documentElement.style.setProperty("--primary-color", primaryColor);
          }, [primaryColor]);

          const antdTheme: ThemeConfig = {
          token: { colorPrimary: primaryColor },
          };

          return (
          <ConfigProvider theme={antdTheme}>
          <div className="p-4 flex items-center gap-x-3 mb-4">
          <ColorPicker
          value={primaryColor}
          onChange={(_, c) => setPrimaryColor(c)}
          />

          {/* 這里使用了在 UnoCSS 中定義的 primary */}
          <span className="p-2 text-primary border border-primary">{primaryColor}</span>

          <Button type="primary">123</Button>
          <Button>zzz</Button>
          </div>
          </ConfigProvider>
          );
          }

          export default App;
          primary-color.gif

          2. 明暗模塊切換

          安裝 classnames 方便組裝類名:

          pnpm add classnames

          1) 封裝切換組件

          先給圖標按鈕加個 shortcut 組合類:

          // uno.config.ts
          import { defineConfig, presetIcons, presetUno } from 'unocss'

          export default defineConfig({
            shortcuts: {
              btn: 'p-2 font-semibold rounded-lg select-none cursor-pointer hover:bg-[#8882] dark:hover:bg-[#fff2]',
            },
            // ... rest config
          })

          創(chuàng)建組件 ToggleTheme.tsx

          // ToggleTheme.tsx
          import { Popover } from "antd";
          import classnames from "classnames";

          function upperFirst(str: string) {
          return `${str[0].toUpperCase()}${str.slice(1)}`;
          }

          export type ColorMode = "light" | "dark" | "auto";

          const iconMap = {
          light: <div className="i-material-symbols:light-mode-outline" />,
          dark: <div className="i-material-symbols:dark-mode-outline" />,
          auto: <div className="i-material-symbols:desktop-windows-outline-rounded" />,
          };

          const modes = ["light", "dark", "auto"] as const;

          interface Props {
          mode: ColorMode;
          onChange: (mode: ColorMode) => void;
          }

          function ToggleTheme({ mode, onChange }: Props) {
          const modeList = (
          <ul>
          {modes.map((m) => (
          <li
          key={m}
          // 這里使用了 shortcut `btn`
          className={classnames("btn flex items-center", {
          "text-primary": m === mode,
          })}
          onClick={() => onChange(m)}
          >
          {iconMap[m]}
          <span className="ml-2">{upperFirst(m)}</span>
          </li>
          ))}
          </ul>
          );

          return (
          <Popover
          placement="bottom"
          arrow={false}
          content={modeList}
          trigger="click"
          >
          {/* 這里也使用了 shortcut `btn` */}
          <a className="btn">{iconMap[mode!]}</a>
          </Popover>
          );
          }

          export default ToggleTheme;

          2) 引入組件

          // App.tsx
          import { Button, ColorPicker, ConfigProvider, ThemeConfig } from "antd";
          import { useEffect, useState } from "react";
          import ToggleTheme, { ColorMode } from "./ToggleTheme";

          function App() {
          // 其他代碼

          // 保存明暗模式
          const [mode, setMode] = useState<ColorMode>("light");

          return (
          <ConfigProvider theme={antdTheme}>
          <div className="p-4 flex items-center gap-x-3 mb-4">
          {/* 其他代碼 */}

          <ToggleTheme mode={mode} onChange={(m) => setMode(m)} />
          </div>
          </ConfigProvider>
          );
          }

          export default App;
          image.png

          3) 綁定 dark 類

          目前常用的黑暗模式方案是給根元素添加一個 dark 類,然后在代碼中通過 dark:text-yellow 指定黑暗模式下的樣式:

          .dark .dark\:text-yellow {
              --un-text-opacity1;
              colorrgb(250 204 21 / var(--un-text-opacity));
          }

          使用 useEffect 同步 dark 類:

          // App.tsx
          import { Button, ColorPicker, ConfigProvider, ThemeConfig } from "antd";
          import { useEffect, useState } from "react";
          import ToggleTheme, { ColorMode } from "./ToggleTheme";

          function App() {
          const [mode, setMode] = useState<ColorMode>("light");
          useEffect(() => {
          document.documentElement.classList.toggle("dark", mode === "dark");
          }, [mode]);

          return (
          <ConfigProvider theme={antdTheme}>
          {/* ... */}
          {/* 使用 dark:text-yellow 指定黑暗模式下的文字顏色 */}
          <h1 className="dark:text-yellow m-4 text-10">Light or dark</h1>
          </ConfigProvider>
          );
          }

          export default App;
          dark-text.gif

          4) 使用 CSS 變量同步顏色

          新建 main.css

          /* main.css */
          :root {
            /* 明亮模式的顏色 */
            --c-bg#fff;
            --c-scrollbar#eee;
            --c-scrollbar-hover#bbb;
            --c-text-color#333/* 字體顏色可以繼承 */
          }

          html {
            /* 使用 CSS 變量 */
            background-colorvar(--c-bg);
            colorvar(--c-text-color);
            overflow-x: hidden;
            overflow-y: scroll;
          }

          html.dark {
            /* 黑暗模式的顏色 */
            --c-bg#333;
            --c-scrollbar#111;
            --c-scrollbar-hover#222;
            --c-text-color#fff;
          }

          * {
            scrollbar-colorvar(--c-scrollbar) var(--c-bg);
          }

          ::-webkit-scrollbar {
            width6px;
          }

          ::-webkit-scrollbar:horizontal {
            height6px;
          }

          ::-webkit-scrollbar-track,
          ::-webkit-scrollbar-corner {
            backgroundvar(--c-bg);
            border-radius10px;
          }

          ::-webkit-scrollbar-thumb {
            backgroundvar(--c-scrollbar);
            border-radius10px;
          }

          ::-webkit-scrollbar-thumb:hover {
            backgroundvar(--c-scrollbar-hover);
          }

          引入 main.tsx

          // main.tsx
          import './main.css'

          5) 同步 Antd

          Antd 暴露的 theme 提供了幾種顏色算法,我們需要用到這兩種:

          • defaultAlgorithm 默認算法
          • darkAlgorithm 黑暗模式的算法

          我們需要根據(jù) modeConfgProvider 提供不同的算法:

          // App.tsx
          import { /* ... */ theme } from "antd"; // 引入 theme
          import { useEffect, useState } from "react";
          import ToggleTheme, { ColorMode } from "./ToggleTheme";

          function App() {
          // ...

          const antdTheme: ThemeConfig = {
          token: { colorPrimary: primaryColor },
          // 黑暗模式使用 darkAlgorithm
          algorithm: mode === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm,
          };

          return (
          <ConfigProvider theme={antdTheme}>
          {/* ... */}
          </ConfigProvider>
          );
          }

          export default App;

          效果如下:(注意看 zzz 按鈕的背景顏色)

          Kapture 2024-01-30 at 16.39.13.gif

          3. 監(jiān)聽系統(tǒng)主題

          剛剛我們實現(xiàn)了手動切換明暗模式,現(xiàn)在來實現(xiàn)根據(jù)當前的系統(tǒng)主題使用對應(yīng)的模式。

          1) 獲取并監(jiān)聽系統(tǒng)主題

          CSS 提供了媒體查詢 prefers-color-scheme: dark 用來監(jiān)聽系統(tǒng)明暗模式,如果我們想讀取,需要調(diào)用window.matchMedia該方法需要傳入一個查詢字符串,并返回一個MediaQueryList對象:

          • matches 布爾值
          • addEventListener 添加監(jiān)聽事件處理函數(shù)

          為了更好的邏輯封裝和復(fù)用,創(chuàng)建一個自定義 hook usePreferredDark.ts,返回系統(tǒng)是否處于黑暗模式:

          import { useState } from 'react'

          export function usePreferredDark({
            // 使用系統(tǒng)當前的明暗模式作為初始值
            const [matches, setMatches] = useState(window.matchMedia('(prefers-color-scheme: dark)').matches)

            // 監(jiān)聽系統(tǒng)的明暗模式變化
            window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change'(e) => {
              setMatches(e.matches)
            })

            return matches
          }

          測試:

          // App.tsx
          import { usePreferredDark } from "./usePreferredDark";

          function App() {
          // ...
          const preferredDark = usePreferredDark()

          useEffect(() => {
          document.documentElement.classList.toggle("dark", preferredDark);
          }, [preferredDark]);

          const antdTheme: ThemeConfig = {
          token: { colorPrimary: primaryColor },
          algorithm: preferredDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
          };

          return (
          <ConfigProvider theme={antdTheme}>
          {/* ... */}
          </ConfigProvider>
          );
          }

          export default App;

          效果如下:

          Kapture 2024-01-30 at 16.37.09.gif

          2) 結(jié)合 mode

          監(jiān)聽系統(tǒng)主題我們實現(xiàn)了,現(xiàn)在需要把 preferredDarkmode 結(jié)合起來判斷當前網(wǎng)頁是否處于黑暗模式,封裝一個自定義 hook useDark.ts,如果是黑暗模式,返回 true:

          // useDark.ts
          import { useMemo } from 'react'
          import { usePreferredDark } from './usePreferredDark'
          import type { ColorMode } from './ToggleTheme'

          export function useDark(mode: ColorMode// 外部傳入,用戶選擇的明暗模式
            const preferredDark = usePreferredDark() // 當前系統(tǒng)是否是黑暗模式
            const isDark = useMemo(() => {
              return mode === 'dark' || (preferredDark && mode !== 'light'// 簡化后的邏輯
            }, [mode, preferredDark])

            return isDark
          }

          邏輯解釋:

          • 因為 mode 是用戶選擇的,所以它優(yōu)先級最高,如果 mode === 'dark',直接短路返回 true;
          • 如果 mode === 'light',返回 false
          • 如果 mode === 'auto',返回當前系統(tǒng)是否處于黑暗模式

          測試:

          // App.tsx
          import { useDark } from "./useDark";

          function App() {
          // ...
          const [mode, setMode] = useState<ColorMode>("light");
          // 當前是否處于黑暗模式
          const isDark = useDark(mode);
          useEffect(() => {
          document.documentElement.classList.toggle("dark", isDark);
          }, [isDark]);

          const antdTheme: ThemeConfig = {
          token: { colorPrimary: primaryColor },
          algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
          };

          return (
          <ConfigProvider theme={antdTheme}>
          {/* ... */}
          </ConfigProvider>
          );
          }

          export default App;

          效果如下:

          Kapture 2024-01-30 at 16.36.11.gif

          最后修改 mode 的初始值為 auto

          // App.tsx
          function App() {
          // ...

          const [mode, setMode] = useState<ColorMode>("auto"); // 這里

          return (
          // ...
          );
          }

          export default App;

          4. 緩存至 localStorage

          這個很簡單,直接使用 ahooks 提供的 useLocalStorageState 即可。

          1) 安裝 ahooks

          pnpm add ahooks

          2) 替換 useState

          // App.tsx
          import { useLocalStorageState } from "ahooks"; // 引入

          // 定義 key 常量
          const PRIMARY_COLOR_KEY = "app_primary_color";
          const COLOR_MODE_KEY = "app_color_mode";

          function App() {
          const [primaryColor, setPrimaryColor] = useLocalStorageState(
          PRIMARY_COLOR_KEY,
          {
          defaultValue: "#01bfff",
          serializer: (v) => v, // 因為我們存的本身就是字符串,不需要 JSON 序列化
          deserializer: (v) => v,
          }
          );

          useEffect(() => {
          document.documentElement.style.setProperty(
          "--primary-color",
          primaryColor! // 注意這里非空斷言
          );
          }, [primaryColor]);

          const [mode, setMode] = useLocalStorageState<ColorMode>(COLOR_MODE_KEY, {
          defaultValue: "auto",
          serializer: (v) => v,
          deserializer: (v) => v as ColorMode,
          });

          const isDark = useDark(mode!);
          useEffect(() => {
          document.documentElement.classList.toggle("dark", isDark);
          }, [isDark]);

          const antdTheme: ThemeConfig = {
          token: { colorPrimary: primaryColor },
          algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
          };

          return (
          <ConfigProvider theme={antdTheme}>
          <div className="p-4 flex items-center gap-x-3 mb-4">
          <ColorPicker
          value={primaryColor}
          onChange={(_, c) => setPrimaryColor(c)}
          />

          <span className="p-2 text-primary border border-primary">
          {primaryColor}
          </span>

          <Button type="primary">123</Button>
          <Button>zzz</Button>

          {/* 注意這里非空斷言 */}
          <ToggleTheme mode={mode!} onChange={(m) => setMode(m)} />
          </div>

          <h1 className="dark:text-yellow m-4 text-10">Light or dark</h1>
          </ConfigProvider>
          );
          }

          export default App;

          效果如下:

          Kapture 2024-01-30 at 16.28.23.gif

          四、總結(jié)技術(shù)要點

          • window.matchMedia API
          • Antd ConfigProvider
          • useLocalStorageState
          • CSS 變量

          完整代碼見 GitHub(https://github.com/zb81/my-theme)。

          原文地址:https://juejin.cn/post/7329352197836914697

          zb81


          • Node 社群

          •      


            我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。


          • 最后不要忘了點個贊再走噢

          瀏覽 84
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  真人一级黄色片 | 日本韩国亚洲天堂网 | 天天综合网~91网站永久入口 | 在线观看日本艹b视频 | renrense在线观看 |