在上一篇文章中,我們介紹了如何使用 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 變數,這樣主題樣式可以在運行時動態更新。
Navba.js
在 Navbar.js
中,之前我們使用 className
判斷主題樣式,這需要依賴 Sass 的靜態編譯。而現在,我們通過引入 CSS 變數,只需要一個 className={styles.navbar}
,讓樣式在運行時自動更新,減少了複雜度。
我們將使用 Sass @each
來動態生成 CSS 變數,取代之前基於 map-get
的靜態樣式。這樣的方式能自動將定義的主題轉換為 CSS 變數,並應用到不同的主題模式中,提升靈活性與維護性。
首先,我們在 _themes.scss
中定義兩套主題 light-mode
和 dark-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
,都能在運行時靈活切換。
在定義好 CSS 變數之後,接下來我們將它們應用到 Navbar
的樣式中。首先,我們將原本使用 lightMode
和 darkMode
的靜態樣式移除,然後使用 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 變數 動態設置顏色,使得在主題切換時,樣式會自動更新並反映當前的主題設定,這大大簡化了代碼維護工作。
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-mode
或 dark-mode
),這樣整個應用都能隨之更新樣式。
今天我們通過 CSS 變數與 CSS Modules 成功實現了對 Navbar
組件的主題切換功能的優化。這樣的做法大幅減少了樣式冗餘,並且允許在運行時動態切換主題,從而提高了應用的靈活性和可維護性。
亮點包括:
ThemeProvider
和 ThemeButton
,我們輕鬆實現了應用內的主題切換功能,並確保所有元件的樣式會即時響應主題的變化。這樣的解決方案特別適合那些需要支持多主題和動態切換的應用。若你的應用需要提供靈活的用戶體驗,不妨試試這樣的架構設計。
最後,我也同步修改了其他元件,請讀者自行練習,將相同的優化應用於其他部分的代碼中。完整代碼和練習題已上傳至 GitHub,鼓勵大家前往查看,回顧文章中的概念,並挑戰更進階的優化練習。
👉 前往 GitHub 的 v0.13.0-refactor-theme-css-vars 查看完整程式碼。
✨ 流光館Luma<∕> ✨ 期待與你繼續探索更多技術知識!