到目前為止,我們的飲料系統所有資料都放在 App.vue,
再一層一層傳下去(Single Source of Truth, SSOT)。
這樣做很單純,也很符合 Vue 的資料流:
資料從上往下流動 → 事件從下往上觸發
但當系統變大、頁面變多,就會開始出現幾個痛點:
為了解決這些問題,我們需要一座中央魔島——
Pinia Store:專門集中管理應用程式的狀態,就像城市裡的中央倉庫。
想像每個元件都是城市中的店家,
每家店都可以直接到中央倉庫取貨或補貨,而不必經過別的店轉交。
今天,我們先用飲料系統的角度理解 Store 的觀念與使用時機,
明天 Day12 我們才會正式把程式拆解,讓 orders
等狀態真正進入 Pinia。
現有做法 (App.vue 單一資料源) | 使用 Pinia Store |
---|---|
資料集中在最上層,必須用 props 層層往下傳 | 任何元件都能直接讀取或修改資料 |
換頁就失去狀態,必須重新抓 API | 可跨頁面共享,保留最新資料 |
子元件對父元件依賴強 | 子元件可獨立運作、減少耦合 |
Pinia Store 的本質就是 中央倉庫 (Central Store):
以下只是快速體驗,明天會完整拆解飲料系統。
安裝:
npm install pinia
在 main.js
啟用:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia()) // ✅ 注入中央倉庫
app.mount('#app')
src/stores/orderStore.js
:import { defineStore } from 'pinia'
export const useOrderStore = defineStore('order', {
state: () => ({
orders: [],
loading: false,
error: ''
}),
actions: {
async loadOrders() {
try {
this.loading = true
const res = await fetch('http://localhost:3000/api/orders')
this.orders = await res.json()
} catch (err) {
this.error = '載入失敗:' + err.message
} finally {
this.loading = false
}
},
async addOrder(payload) {
try {
this.loading = true
const res = await fetch('http://localhost:3000/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
this.orders.push(await res.json())
} catch (err) {
this.error = '新增失敗:' + err.message
} finally {
this.loading = false
}
}
}
})
defineStore
:建立一個 Storeimport { defineStore } from 'pinia'
export const useOrderStore = defineStore('order', {
// state, getters, actions...
})
區塊 | 作用 |
---|---|
defineStore('order', { ... }) |
建立一個 store 定義,並給它一個唯一 id (order )。這個 id 會變成 store 的名稱,用來除錯或 SSR。 |
useOrderStore |
命名慣例是 useXXXStore ,呼叫時 const store = useOrderStore() 取得 store 實例。 |
💡 可以有多個 store,例如
useUserStore
、useCartStore
,彼此獨立管理資料。
state: () => ({
orders: [],
loading: false,
error: ''
}),
名稱 | 型態 | 說明 |
---|---|---|
orders |
Array | 訂單資料清單 |
loading |
Boolean | 是否正在載入或處理 API(通常有前後端api的需求都會這樣定義狀態) |
error |
String | 錯誤訊息(用來處理萬一失敗的api狀態) |
寫法是函式 → state: () => ({ ... })
這樣每個使用此 store 的元件都會有同一份共享狀態(而不是每次呼叫都產生新的物件)。
使用方式:
const store = useOrderStore()
console.log(store.orders.length)
store.loading = true // 直接改值
actions: {
async loadOrders() { ... },
async addOrder(payload) { ... }
}
方法 | 功能 |
---|---|
loadOrders |
呼叫 API 取得所有訂單 |
addOrder |
呼叫 API 新增一筆訂單 |
特色:
內部可以用 this
直接修改 state,例如 this.orders.push()
。
支援同步或非同步(async/await)操作。
可以被任何組件呼叫:
const store = useOrderStore()
store.loadOrders()
為何不放在元件裡?
有些共用/重複用到的function可以在這邊定義,這樣就不需要再各自組件定義然後重複call function
<script setup>
import { useOrderStore } from '../stores/orderStore'
import { storeToRefs } from 'pinia'
const store = useOrderStore()
// 讓 state 轉為響應式的 ref(解構後仍保持響應)
const { orders, loading, error } = storeToRefs(store)
// 呼叫 actions
store.loadOrders()
</script>
函式 | 作用 |
---|---|
useOrderStore() |
取得 store 實例 |
storeToRefs() |
把 state 屬性轉成 ref ,在 template 裡使用時不會失去響應能力 |
export const useOrderStore = defineStore('order', {
state: () => ({}), // ✅ 集中存放資料
getters: {}, // ✅ 衍生資料 (computed)
actions: {} // ✅ 業務邏輯 (methods / API)
})
名稱 | 類似 Vue 的 | 用途 |
---|---|---|
state |
data() |
集中管理共享資料 |
getters |
computed |
計算衍生資料 |
actions |
methods |
處理邏輯、呼叫 API |
修改 src/App.vue:
<script setup>
import { useOrderStore } from './stores/orderStore'
import { storeToRefs } from 'pinia'
import { ref } from 'vue'
const store = useOrderStore()
const { orders, loading, error } = storeToRefs(store)
const name = ref('')
const drink = ref('')
// 初始載入
store.loadOrders()
async function add() {
if (!name.value || !drink.value) return
await store.addOrder({
name: name.value,
drink: drink.value
})
name.value = ''
drink.value = ''
}
</script>
<template>
<main>
<h1>Day11 – Pinia Store Demo</h1>
<div v-if="error" class="error">⚠️ {{ error }}</div>
<div v-if="loading">🔄 載入中...</div>
<div class="block">
<input v-model="name" placeholder="姓名" />
<input v-model="drink" placeholder="飲料" />
<button @click="add">新增訂單</button>
</div>
<h3>目前訂單</h3>
<ul>
<li v-for="o in orders" :key="o.id">{{ o.name }} - {{ o.drink }}</li>
</ul>
</main>
</template>
<style>
body { font-family: sans-serif; }
.block { margin: 12px 0; }
.error { color: #c62828; background: #ffeef0; padding: 6px; border-radius: 4px; }
</style>
後端的code可以沿用我們前面的後端code無須修改喔!!
方法 | 優點 | 缺點 |
---|---|---|
只用 App.vue (SSOT) | 概念簡單、檔案少,適合單頁或小型應用 | 1. 跨頁資料會遺失 2. 多層 props 傳遞麻煩 3. 元件耦合度高 |
使用 Pinia Store | 1. 資料集中、狀態可跨頁共享 2. 任何元件可直接取得資料 3. 更適合中大型或多頁應用 | 需多一個套件與學習成本 |
位置 | 功能 |
---|---|
main.js |
建立應用並注入 Pinia |
stores/orderStore.js |
以 state 儲存訂單資料,以 actions 封裝讀取與新增 |
App.vue |
直接從 store 取得 orders、loading、error,不再層層傳 props |
這個簡單版本只演示 Pinia store 的安裝與基本用法。
明天 Day12 會正式把 統計邏輯 與更完整的 CRUD 都搬進 store,
讓飲料系統真正升級成 中央管理架構。
並非所有應用都一定需要用到 store
只有當 狀態跨越多個元件 且 需要共用邏輯或方法 時,集中管理才會帶來優勢。
情境 | 說明 |
---|---|
跨頁面或多層級共用狀態 | 例如:登入的 userProfile 、網站的 theme (主題色系)、語系設定等,需要在多個頁面或組件中同時讀寫。 |
共用的商業邏輯或方法 | 例如:計算購物車金額、統計訂單數量、驗證登入權限等,不必在每個元件重複寫一次。 |
需要長期保存或快取 | 例如:使用者資訊、後端取得的設定檔,在整個 app 生命週期中都需要。 |
情境 | 說明 |
---|---|
單一頁面、單一元件就能處理的狀態 | 例如:單純的表單欄位輸入或一個小型元件的 UI 切換,直接用 ref 或 reactive 即可。 |
只在父子之間傳遞、結構很淺 | 用 props 和 emit 會更直覺,也更容易維護。 |
💡 重點提醒
我們還是會回顧到根據團隊的開發風格還有需求來定義使用的技術或是框架喔!!
ref
、computed
、props
就足夠。