iT邦幫忙

2024 iThome 鐵人賽

DAY 13
0
JavaScript

從PM到前端開發:我的React作品集之旅系列 第 13

Day 13: 告別 Sass map-get,用 CSS 變數簡化主題管理

  • 分享至 

  • xImage
  •  

在上一篇文章中,我們介紹了如何使用 Sass 和 map-get 來管理多主題樣式。隨著應用規模的擴大,程式碼變得越來越難以維護。今天,我們將探討為什麼選擇 CSS 變數 來優化主題切換機制,以提升靈活性和效率。

為什麼要拋棄 map-get

如以下表格所示, 使用 CSS 變數而不是Sassmap-get是主要是三方面的考量: 動態性靈活性維護成本。CSS 變數不僅能夠在運行時動態更新樣式,還能簡化主題切換邏輯,減少樣式冗餘,並與 JavaScript 無縫結合,為現代應用提供了更高的靈活性和性能優勢。因此,隨著應用的擴大和需求的增多,CSS 變數成為了實現多主題樣式管理的最佳選擇。

特性 Sass (map-get) CSS 變數
動態性 靜態編譯,無法在運行時動態更新樣式 可以在運行時動態更新樣式
主題切換 需要為每個主題生成獨立的 CSS 文件 可在運行時動態切換主題,不需重新編譯
維護成本 每次新增或修改主題,都需要手動更新 Sass 地圖 只需定義一次 CSS 變數即可,維護更簡單
樣式冗餘 每個主題都會生成完整的樣式,文件較大 減少樣式重複,文件較小
瀏覽器支持 需要預處理器來編譯 Sass 為 CSS 原生支持,現代瀏覽器可直接使用
與 JavaScript 結合 無法直接與 JavaScript 結合 可以與 JavaScript 無縫結合,動態修改變數
可讀性 map-get 語法簡單,但地圖過大時不直觀 變數名簡單直觀,使用時更易理解

實際演練

在這次代碼重構中,我們的主要變更集中在 Navbar 組件的樣式處理上。原先的實現使用了 Sass 中的 map-get 來根據主題返回對應的樣式。而現在,我們將這部分邏輯改為基於 CSS 變數,這樣主題樣式可以在運行時動態更新。

Step 1: 簡化 Navba.js

Navbar.js 中,之前我們使用 className 判斷主題樣式,這需要依賴 Sass 的靜態編譯。而現在,我們通過引入 CSS 變數,只需要一個 className={styles.navbar},讓樣式在運行時自動更新,減少了複雜度。

https://ithelp.ithome.com.tw/upload/images/20240911/20168330UEFh4AZtK1.png

Step 2: 定義 CSS 變數

我們將使用 Sass @each 來動態生成 CSS 變數,取代之前基於 map-get 的靜態樣式。這樣的方式能自動將定義的主題轉換為 CSS 變數,並應用到不同的主題模式中,提升靈活性與維護性。

首先,我們在 _themes.scss 中定義兩套主題 light-modedark-mode,每個主題包含各自的顏色和樣式變量。接著,利用 Sass @each 循環將這些主題變量轉換為 CSS 變數。

$themes: (
    "light-mode": ("primary": #FFC0CB, // 淡粉色,用於Basic Stage按鈕
        "secondary": #E6E6FA, // 淡紫色,用於Advanced Stage按鈕
        "background-primary": #FFFFF0, // 淡黃色, 白色背景
        "background-gradient-end": lighten(#FFFFF0, 3%), // 淡黃色調亮後的顏色
        "highlight": #FFD700, // 金黃色,用於亮點或特定強調部分
        "text-primary": #1C1C1C, // 深灰色文字,用於標題和主要文字
        "text-secondary": #A9A9A9, // 中灰色,用於次要文字
        "button-text": #1C1C1C, // 按鈕文字黑色
    ),

    "dark-mode": ("primary": #4A90E2, // 亮藍色,用於Basic Stage按鈕
        "secondary": #F5A623, // 亮橙色,用於Advanced Stage按鈕
        "background-primary": #2C3E50, // 深藍色背景
        "background-gradient-end": lighten(#2C3E50, 3%), // 淡黃色調亮後的顏色
        "highlight": #FFC107, // 淡黃色,用於月亮或亮點
        "text-primary": #FFFFFF, // 白色文字,用於標題和主要文字
        "text-secondary": #90A4AE, // 淡灰色文字,用於次要文字
        "button-text": #FFFFFF, // 按鈕文字白色
    )
);

// 將 Sass 變數轉換為 CSS 變數
@each $theme-name, $theme-map in $themes {
    .#{$theme-name} {
        @each $property, $value in $theme-map {
            --#{$property}: #{$value};
        }
    }
}

這段代碼會將 Sass 定義的主題顏色變量轉換為對應的 CSS 變數,編譯後生成的 CSS 如下:

.light-mode {
  --primary: #FFC0CB;
  --secondary: #E6E6FA;
  --background-primary: #FFFFF0;
  --background-gradient-end: #FFFFFA;
  --highlight: #FFD700;
  --text-primary: #1C1C1C;
  --text-secondary: #A9A9A9;
  --button-text: #1C1C1C;
}

.dark-mode {
  --primary: #4A90E2;
  --secondary: #F5A623;
  --background-primary: #2C3E50;
   --background-gradient-end: #34495e;
  --highlight: #FFC107;
  --text-primary: #FFFFFF;
  --text-secondary: #90A4AE;
  --button-text: #FFFFFF;
}

這樣使用 Sass @each 循環的方式,極大地減少了重複定義 CSS 變數的工作。當你需要新增或修改主題樣式時,只需更新 $themes 變量即可,簡化了整體的維護過程,同時保證了代碼的可讀性和可擴展性。

這些 CSS 變數可以動態應用到樣式中,無論是 light-mode 還是 dark-mode,都能在運行時靈活切換。

Step 3: 在 CSS Modules 中使用 CSS 變數

在定義好 CSS 變數之後,接下來我們將它們應用到 Navbar 的樣式中。首先,我們將原本使用 lightModedarkMode 的靜態樣式移除,然後使用 CSS 變數來取代原有的 map-get 語法。這樣,當用戶切換主題時,樣式會自動適應當前的主題顏色,而無需重新編譯。

//src/components/navBar/Navbar.module.scss
.navbar {
    display: flex;
    justify-content: space-between; // 左右對齊
    align-items: center; // 垂直方向居中
    padding: 10px 20px;
    border-radius: 10px; //  外圍的黑色邊框 
    position: relative;
    height: 70%;
    max-width: 80%;
    margin: 5px auto;
    border: 2px solid var(--text-primary);
}

.navbarLinks li a {
    text-decoration: none;
    color: var(--text-secondary);
    position: relative; // 使文字相對定位,偽元素能相對於它定位
    font-size: 24px;
    font-weight: normal;
    transition: color 0.3s;
    z-index: 1;

    // 當菜單項目被選中時
    &.active,
    &.selected {
        color: var(--text-primary);
    }

    // 當菜單項目被選中時, 添加筆刷效果的偽元素
    &.active::after {
        content: '';
        position: absolute;
        left: 0;
        bottom: 0; // 確保偽元素貼合文字底部
        width: 100%;
        height: 0.5em; // 根據需要調整高度,這裡使用字體高度的一半
        background-color: var(--primary);
        border-radius: 4px; // 使背景稍微圓潤,模仿筆刷效果
        opacity: 0.6; // 調整透明度,使背景不會過於明顯
        transition: width 0.3s ease; // 添加動畫效果
        z-index: -1;
    }
}

這樣的改動通過 CSS 變數 動態設置顏色,使得在主題切換時,樣式會自動更新並反映當前的主題設定,這大大簡化了代碼維護工作。

Step 4: 在 ThemeContext.js 中處理主題切換

在之前的 ThemeContext.js 中,我們已經實現了主題切換的邏輯。接下來,我們需要確保當用戶點擊 ThemeButton 時,Navbar 組件的樣式能夠自動更新。

// src/utils/ThemeContext.jsx
import React, { createContext, useState, useContext, useEffect } from 'react';

// 創建一個主題上下文
const ThemeContext = createContext();

// ThemeProvider 組件負責提供主題狀態和切換功能
export const ThemeProvider = ({ children }) => {
    // 預設主題狀態(可以從 localStorage 讀取之前的主題選擇)
    const [isDarkMode, setIsDarkMode] = useState(() => {
        // 優先從 localStorage 中讀取主題設置,否則預設為 false (light-mode)
        const savedTheme = localStorage.getItem('theme');
        return savedTheme ? JSON.parse(savedTheme) : false;
    });

    // 當主題變更時,應用對應的主題類名到 body
    useEffect(() => {
        if (isDarkMode) {
            document.body.classList.add('dark-mode');
            document.body.classList.remove('light-mode');
        } else {
            document.body.classList.add('light-mode');
            document.body.classList.remove('dark-mode');
        }
        // 同時將主題狀態保存到 localStorage
        localStorage.setItem('theme', JSON.stringify(isDarkMode));
    }, [isDarkMode]);

    // 切換主題狀態
    const toggleTheme = () => {
        setIsDarkMode((prevMode) => !prevMode);
    };

    return (
        <ThemeContext.Provider value={{ isDarkMode, toggleTheme }}>
            {children}
        </ThemeContext.Provider>
    );
};

// 自定義 Hook,便於其他組件訪問主題上下文
export const useTheme = () => useContext(ThemeContext);

這段代碼的核心在於,當用戶切換主題時,我們會更新 isDarkMode 狀態,並相應地在 <body> 元素上應用對應的主題類名(light-modedark-mode),這樣整個應用都能隨之更新樣式。

結語

今天我們通過 CSS 變數與 CSS Modules 成功實現了對 Navbar 組件的主題切換功能的優化。這樣的做法大幅減少了樣式冗餘,並且允許在運行時動態切換主題,從而提高了應用的靈活性和可維護性。

亮點包括:

  1. CSS 變數的靈活性:主題顏色和樣式可以通過 CSS 變數在運行時進行動態修改,無需重新編譯。
  2. CSS Modules 的隔離特性:保證了每個元件的樣式隔離,防止全局樣式污染。
  3. 動態主題切換:通過 ThemeProviderThemeButton,我們輕鬆實現了應用內的主題切換功能,並確保所有元件的樣式會即時響應主題的變化。

這樣的解決方案特別適合那些需要支持多主題和動態切換的應用。若你的應用需要提供靈活的用戶體驗,不妨試試這樣的架構設計。

最後,我也同步修改了其他元件,請讀者自行練習,將相同的優化應用於其他部分的代碼中。完整代碼和練習題已上傳至 GitHub,鼓勵大家前往查看,回顧文章中的概念,並挑戰更進階的優化練習。

👉 前往 GitHub 的 v0.13.0-refactor-theme-css-vars 查看完整程式碼。


流光館Luma<∕> ✨ 期待與你繼續探索更多技術知識!



上一篇
Day 12: 打造 React 導航欄元件
下一篇
Day 14 : 打造高效的 React 按鈕設計方案(上)
系列文
從PM到前端開發:我的React作品集之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言