Appearance
Pinia 全域狀態管理架構
概述
本專案使用 Pinia 作為全域狀態管理解決方案。
為什麼選擇 Pinia?
- ✅ 官方推薦: Vuejs.org 官方推薦的 Vue 3 狀態管理方案
- ✅ 型別安全: 完整的 TypeScript 支援,自動推斷型別
- ✅ DevTools 整合: 強大的 Vue DevTools 支援,方便除錯
- ✅ 模組化: 每個 store 都是獨立模組,易於維護
- ✅ Composition API: 使用熟悉的 Vue 3 語法
- ✅ SSR 支援: 與 Nuxt 完美整合
目錄結構
app/stores/
└── user.ts # 使用者狀態Store 模板
使用 Composition API 語法 定義 Store:
typescript
// app/stores/myStore.ts
import { defineStore } from 'pinia'
export const useMyStore = defineStore('my-store', () => {
// 狀態 (ref)
const items = ref<Item[]>([])
const isLoading = ref(false)
const error = ref<Error | null>(null)
// Getters (computed)
const itemCount = computed(() => items.value.length)
const hasItems = computed(() => items.value.length > 0)
// Actions (function)
async function loadItems() {
isLoading.value = true
error.value = null
try {
const data = await $fetch('/api/v1/items')
items.value = data
} catch (e) {
error.value = e as Error
} finally {
isLoading.value = false
}
}
function clearItems() {
items.value = []
}
// 使用 readonly 保護狀態
return {
items: readonly(items),
isLoading: readonly(isLoading),
error: readonly(error),
itemCount,
hasItems,
loadItems,
clearItems,
}
})命名規範
typescript
// ✅ 正確
export const useUserPreferencesStore = defineStore('user-preferences', ...)
export const useAuthStore = defineStore('auth', ...)
// ❌ 錯誤
export const userPreferencesStore = defineStore('preferences', ...)
export const Auth = defineStore('auth', ...)- Store 函式名稱:
use<Name>Store - Store ID:
kebab-case
使用方式
vue
<script setup lang="ts">
const preferencesStore = useUserPreferencesStore()
// 讀取狀態
console.log(preferencesStore.preferences)
console.log(preferencesStore.isLoading)
// 呼叫 Action
await preferencesStore.updatePrimaryColor('blue')
// 解構使用(保持響應性)
const { preferences, isLoading } = storeToRefs(preferencesStore)
const { updatePrimaryColor } = preferencesStore
</script>Plugin 自動初始化
使用 Plugin 處理全域初始化邏輯:
typescript
// app/plugins/my-store-init.client.ts
export default defineNuxtPlugin(() => {
const { user, loggedIn } = useUserSession()
const myStore = useMyStore()
watch(
loggedIn,
(isLoggedIn) => {
if (isLoggedIn && user.value?.id) {
// 使用者登入 → 載入資料
myStore.loadData()
} else {
// 使用者登出 → 清空資料
myStore.clearData()
}
},
{ immediate: true },
)
})優點:
- ✅ 全域只執行一次
- ✅ 自動響應使用者狀態變化
- ✅ 不需要在組件中手動呼叫
最佳實踐
1. 使用 readonly 保護狀態
typescript
return {
preferences: readonly(preferences), // ✅ 防止外部直接修改
updatePreferences, // 通過 action 修改
}2. 避免在 Store 中使用 useState
typescript
// ❌ 不要這樣
export const useMyStore = defineStore('my-store', () => {
const data = useState('my-data', () => null) // 錯誤!
return { data }
})
// ✅ 使用 ref
export const useMyStore = defineStore('my-store', () => {
const data = ref(null) // 正確
return { data }
})3. 錯誤處理
typescript
async function loadData() {
isLoading.value = true
error.value = null
try {
const result = await $fetch('/api/data')
data.value = result
} catch (e) {
error.value = e as Error
console.error('Failed to load data:', e)
} finally {
isLoading.value = false
}
}4. 在 Plugin 中處理全域初始化
typescript
// app/plugins/my-store-init.client.ts
export default defineNuxtPlugin(() => {
const myStore = useMyStore()
myStore.initialize()
})除錯技巧
使用 Vue DevTools
- 安裝 Vue DevTools 瀏覽器擴充功能
- 開啟開發者工具 → Vue tab → Pinia
- 即時查看所有 store 狀態、mutations、actions
Console 檢查
typescript
// 在瀏覽器 console 中
const { $pinia } = useNuxtApp()
console.log($pinia.state.value) // 查看所有 store 狀態Composable 包裝策略
若需保持向後相容,可用 composable 包裝 store:
typescript
// app/composables/useMyFeature.ts
export function useMyFeature(config: MyConfig) {
const store = useMyStore()
// 初始化(如果尚未初始化)
if (!store.isInitialized) {
store.initialize(config)
}
return {
data: store.data,
isLoading: store.isLoading,
refresh: store.refresh,
}
}檢查清單
建立新 Store 時確認:
- [ ] 使用 Composition API 語法(
defineStore('id', () => {...})) - [ ] 命名遵循
use<Name>Store規範 - [ ] 使用
readonly()保護需要保護的狀態 - [ ] 錯誤處理完整(try/catch + error state)
- [ ] 需要全域初始化則建立對應 Plugin
- [ ] 避免使用
useState