Astro 「群島架構」Nano Stores 繼續學習 - 2

更新 發佈閱讀 11 分鐘

第二題:深色模式(Dark Mode)

深色模式(Dark Mode) 的核心在於如何讓狀態「持久化」(即使重新整理網頁,設定也不會消失),以及如何與系統或手動設定同步。

在 Nano Stores 中,我們可以使用原生提供的 persistent 擴充功能,這能讓我們省去手寫 localStorage.getItem/setItem 的麻煩。


練習目標

  1. 建立一個持久化的 Atom ($isDark)。
  2. 建立一個切換開關元件(使用 React 或 原生 JS)。
  3. 根據狀態自動切換 HTML 的 class="dark"
  4. 確保頁面重新整理後,狀態依然正確。

第一步:安裝持久化插件

Nano Stores 官方提供了一個專門處理持久化的套件:

npm install @nanostores/persistent

第二步:建立 Store (src/themeStore.ts)

這裡我們使用 persistentAtom。它的用法和 atom 幾乎一樣,但第一個參數是 localStorage 的 Key,第二個是初始值。

當你使用 @nanostores/persistent 中的 persistentAtom 時,它會自動處理與瀏覽器 LocalStorage 的同步。

它是如何運作的?

  1. 寫入 (Write): 當你執行 $theme.set('dark') 時,它會同時更新記憶體中的狀態,並在 LocalStorage 中存入 theme: "dark"
  2. 讀取 (Read): 當使用者重新整理網頁時,它會先去 LocalStorage 找有沒有一個 key 叫作 'theme'。如果有,就用那個值作為初始值;如果沒有,才使用你設定的預設值 'light'
  3. 監聽 (Listen): 它甚至會監聽 LocalStorage 的事件。如果你在同一個網站開了兩個分頁,在 A 分頁切換主題,B 分頁也會跟著同步變化。

你可以在瀏覽器檢查它
如果你想親眼確認,可以依照以下步驟:

  1. 在網頁上按 F12 (或右鍵「檢查」)。
  2. 切換到 Application (應用程式) 頁籤。
  3. 在左側找到 Local Storage
  4. 點擊你的網址,你就會看到 key: theme 和 value: light/dark
import { persistentAtom } from '@nanostores/persistent'

// 'isDark' 是存在 localStorage 裡的 Key
// 第二個參數是預設值,這裡設定為字串 'light' (persistent 目前主要支援字串)
export const $theme = persistentAtom('theme', 'light')

export function toggleTheme() {
const current = $theme.get()
$theme.set(current === 'light' ? 'dark' : 'light')
}

❌ 可能會出現錯誤

  Argument of type '"light" | "dark"' is not assignable to parameter of type '"light"'.
Type '"dark"' is not assignable to type '"light"'.

這個錯誤是因為 TypeScript 的自動型別推斷太過嚴格了。

當你寫 persistentAtom('theme', 'light') 時,TypeScript 會以為這個 Store 永遠只能是 'light' 這個字串,而不接受其他的字串(例如 'dark')。

💡 解決方法:明確定義型別

你需要告訴 TypeScript,這個 Store 的型別是 'light' | 'dark'(這兩者之一),而不僅僅是字串 'light'

🆗 修正後的程式碼:

import { persistentAtom } from '@nanostores/persistent'

// 1. 定義允許的型別
type Theme = 'light' | 'dark'

// 2. 在建立時明確指定型別 <Theme>
export const $theme = persistentAtom<Theme>('theme', 'light')

export function toggleTheme() {
const current = $theme.get()
// 現在這裡就不會報錯了
$theme.set(current === 'light' ? 'dark' : 'light')
}

❔ 為什麼會報錯?(原理分析)

如果你不加上 <Theme>,TypeScript 的推斷邏輯如下:
它看到初始值是 'light'
它為了安全,把這個 Store 的型別鎖定為 字面量型別 (Literal Type)"light"
當你試圖 .set("dark") 時,它會說:「嘿!你說過這個變數只能是 "light",你現在給我 "dark" 是不合法的。」
這在 TypeScript 中稱為 Type Widening(型別拓寬) 的問題。
widening [ˈwaɪdənɪŋ]


第三步:建立切換開關元件 (src/components/ThemeToggle.astro)

我們直接用原生 JS 來實作,這樣最輕量。

<button id="theme-btn" class="p-2 border rounded">
切換模式
</button>

<script>
import { $theme, toggleTheme } from '../themeStore';

const btn = document.querySelector('#theme-btn');

// 點擊時呼叫 store 的切換函數
btn?.addEventListener('click', () => {
toggleTheme();
});

// 監聽變化:當切換時,更新按鈕文字或樣式
$theme.subscribe(value => {
btn.textContent = value === 'dark' ? '🌙 深色模式' : '☀️ 淺色模式';
});
</script>

第四步:最重要的——防止「閃爍」(Flash of Unstyled Content)

這是做深色模式最難的地方。如果我們等 JS 下載完才切換顏色,頁面會先白一下才變黑。 我們必須在 <head> 裡放一段極短的腳本,在頁面渲染前就讀取 Store。

在你的 src/layouts/Layout.astro 中:

---
// Layout 的頭部
---
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8" />
<title>Nano Stores 練習</title>

<script is:inline>
// 注意:這段 script 必須是 is:inline
// 我們直接從 localStorage 拿值,因為此時 Nano Stores 可能還沒加載
const theme = localStorage.getItem('theme') || 'light';
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
</script>

<style is:global>
/* 定義簡單的深色模式樣式 */
:root { background: #ffffff; color: #000; }
:root.dark { background: #1a1a1a; color: #fff; }
</style>
</head>
<body>
<slot />

<script>
// 這裡我們還是要訂閱 Store,以便在用戶點擊按鈕時「即時」切換 class
import { $theme } from '../themeStore';
$theme.subscribe(value => {
if (value === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
});
</script>
</body>
</html>

扎實學習的小筆記:

  1. 為什麼要 is:inline? Astro 預設會打包腳本。但處理主題切換的腳本必須「阻塞」渲染,確保 HTML 標籤一出來就帶有正確的 class,否則使用者會被閃瞎。
  2. Persistent 的特性: persistentAtom 會自動處理 window.addEventListener('storage', ...)。這意味著如果你開兩個分頁,在 A 分頁切換深色模式,B 分頁會自動同步變黑!

進階挑戰:

現在你的深色模式是手動切換的。你能否修改 themeStore.ts,讓它在使用者第一次進入網站時,自動偵測系統的偏好設定 (window.matchMedia('(prefers-color-scheme: dark)'))?

留言
avatar-img
李昀瑾的沙龍
0會員
37內容數
李昀瑾的沙龍的其他內容
2026/01/25
第一題:「購物車」系統 這個練習的重點在於學習如何使用 map 來處理物件型態的狀態,以及如何在多個元件之間同步這些數據。 練習目標 建立一個商品清單,點擊「加入」按鈕時更新購物車。 若商品已在購物車,數量 +1+1;若不在,則新增一筆。 即時顯示購物車內的商品總數。 第一步:建立
2026/01/25
第一題:「購物車」系統 這個練習的重點在於學習如何使用 map 來處理物件型態的狀態,以及如何在多個元件之間同步這些數據。 練習目標 建立一個商品清單,點擊「加入」按鈕時更新購物車。 若商品已在購物車,數量 +1+1;若不在,則新增一筆。 即時顯示購物車內的商品總數。 第一步:建立
2026/01/23
✏️ 專題二實作 1. 建立 Store (src/store/themeStore.ts) import { atom } from 'nanostores' // 預設為 false (淺色模式) export const $isDark = atom(false) 2. 建立切換按鈕
Thumbnail
2026/01/23
✏️ 專題二實作 1. 建立 Store (src/store/themeStore.ts) import { atom } from 'nanostores' // 預設為 false (淺色模式) export const $isDark = atom(false) 2. 建立切換按鈕
Thumbnail
2026/01/20
Nano Stores與其它的狀態管理工具(如 Redux 或 Pinia)不同 Nano Stores 是 不可知框架(Framework-agnostic) 的。這意味著你可以在同一個 Astro 專案中,讓 React、Vue、Svelte 和原生 JS 共享同一個狀態。 第一階段:核心概
2026/01/20
Nano Stores與其它的狀態管理工具(如 Redux 或 Pinia)不同 Nano Stores 是 不可知框架(Framework-agnostic) 的。這意味著你可以在同一個 Astro 專案中,讓 React、Vue、Svelte 和原生 JS 共享同一個狀態。 第一階段:核心概
看更多
你可能也想看
Thumbnail
《轉轉生》(Re:INCARNATION)為奈及利亞編舞家庫德斯.奧尼奎庫與 Q 舞團創作的當代舞蹈作品,結合拉各斯街頭節奏、Afrobeat/Afrobeats、以及約魯巴宇宙觀的非線性時間,建構出關於輪迴的「誕生—死亡—重生」儀式結構。本文將從約魯巴哲學概念出發,解析其去殖民的身體政治。
Thumbnail
《轉轉生》(Re:INCARNATION)為奈及利亞編舞家庫德斯.奧尼奎庫與 Q 舞團創作的當代舞蹈作品,結合拉各斯街頭節奏、Afrobeat/Afrobeats、以及約魯巴宇宙觀的非線性時間,建構出關於輪迴的「誕生—死亡—重生」儀式結構。本文將從約魯巴哲學概念出發,解析其去殖民的身體政治。
Thumbnail
這是一場從「網路連結 → 線下見面」的活動,我一開始其實有些猶豫,畢竟地點對我來說不近,加上平常在社群裡其實不太主動互動。 但因為主辦人西打誠意滿滿地邀請,甚至還提出補貼車資,最後我決定自費參加。現在回頭看,真的很值得! 場地很有感,氛圍超溫暖 一踏進場地就被暖黃的燈光包圍,小閣樓超舒適,還
Thumbnail
這是一場從「網路連結 → 線下見面」的活動,我一開始其實有些猶豫,畢竟地點對我來說不近,加上平常在社群裡其實不太主動互動。 但因為主辦人西打誠意滿滿地邀請,甚至還提出補貼車資,最後我決定自費參加。現在回頭看,真的很值得! 場地很有感,氛圍超溫暖 一踏進場地就被暖黃的燈光包圍,小閣樓超舒適,還
Thumbnail
Nuxt.js 是以 Vue 為基底所建構的框架,透過 Nuxt.js,我們能夠更輕鬆地開發靜態頁面 (Static Site)、操作體驗良好的單頁式網站 (SPA)、甚至是顧及 SEO 的伺服器端渲染 (SSR) 網站。
Thumbnail
Nuxt.js 是以 Vue 為基底所建構的框架,透過 Nuxt.js,我們能夠更輕鬆地開發靜態頁面 (Static Site)、操作體驗良好的單頁式網站 (SPA)、甚至是顧及 SEO 的伺服器端渲染 (SSR) 網站。
Thumbnail
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
從實際應用中學習 Python 程式設計,提升技能並建立作品集。文章提供八個循序漸進的 Python 專案範例,涵蓋檔案操作、網路爬蟲、Web 應用、自動化腳本、數據分析、遊戲開發、API 互動及應用程式部署,並附上實戰建議及學習資源。
Thumbnail
從實際應用中學習 Python 程式設計,提升技能並建立作品集。文章提供八個循序漸進的 Python 專案範例,涵蓋檔案操作、網路爬蟲、Web 應用、自動化腳本、數據分析、遊戲開發、API 互動及應用程式部署,並附上實戰建議及學習資源。
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
Thumbnail
網站開發專案成功的關鍵在於與客戶的有效溝通。本文分享一個成功案例,說明如何透過明確掌握專案需求、主動提供技術方案、定期回報進度、完善技術協助及建立良好客戶關係,順利完成一個中文影片學習分享網站的建置,並獲得客戶高度滿意與後續合作機會。
Thumbnail
網站開發專案成功的關鍵在於與客戶的有效溝通。本文分享一個成功案例,說明如何透過明確掌握專案需求、主動提供技術方案、定期回報進度、完善技術協助及建立良好客戶關係,順利完成一個中文影片學習分享網站的建置,並獲得客戶高度滿意與後續合作機會。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News