iT邦幫忙

2025 iThome 鐵人賽

DAY 15
0
Vue.js

需求至上的 Vue 魔法之旅系列 第 15

Day11:中央魔島書院 – 共享的 store (Pinia)

  • 分享至 

  • xImage
  •  

前言:為什麼需要「中央魔島」來管理資料

到目前為止,我們的飲料系統所有資料都放在 App.vue
再一層一層傳下去(Single Source of Truth, SSOT)。
這樣做很單純,也很符合 Vue 的資料流:

資料從上往下流動 → 事件從下往上觸發

但當系統變大、頁面變多,就會開始出現幾個痛點:

  • 跨頁面資料無法保存
    換頁後元件被銷毀,資料就不見了,除非每次都重抓 API。
  • 多層元件傳遞繁瑣
    為了讓最裡層元件取得資料,需要一層一層傳 props。
  • 元件之間難以直接溝通
    任何資料交換都要繞回最上層。

為了解決這些問題,我們需要一座中央魔島——
Pinia Store:專門集中管理應用程式的狀態,就像城市裡的中央倉庫

想像每個元件都是城市中的店家,
每家店都可以直接到中央倉庫取貨或補貨,而不必經過別的店轉交。

今天,我們先用飲料系統的角度理解 Store 的觀念與使用時機
明天 Day12 我們才會正式把程式拆解,讓 orders 等狀態真正進入 Pinia。/images/emoticon/emoticon01.gif


1️⃣ 為什麼要使用 Pinia Store

現有做法 (App.vue 單一資料源) 使用 Pinia Store
資料集中在最上層,必須用 props 層層往下傳 任何元件都能直接讀取或修改資料
換頁就失去狀態,必須重新抓 API 可跨頁面共享,保留最新資料
子元件對父元件依賴強 子元件可獨立運作、減少耦合

Pinia Store 的本質就是 中央倉庫 (Central Store)

  • 所有資料都集中在 state
  • 修改資料的流程都透過 actions
  • 計算衍生資料則透過 getters(Day12 會介紹)

2️⃣ Pinia 安裝與初始化

以下只是快速體驗,明天會完整拆解飲料系統。

安裝:

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')

3️⃣ 建立最簡單的 Store

1.建立 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
      }
    }
  }
})

2. 說明pinia store的語法

defineStore:建立一個 Store

import { 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,例如 useUserStoreuseCartStore,彼此獨立管理資料。


state:集中資料狀態

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:定義方法與邏輯(簡單說就是store版的methods拉~)

actions: {
  async loadOrders() { ... },
  async addOrder(payload) { ... }
}
方法 功能
loadOrders 呼叫 API 取得所有訂單
addOrder 呼叫 API 新增一筆訂單
  • 特色

    • 內部可以用 this 直接修改 state,例如 this.orders.push()

    • 支援同步或非同步(async/await)操作。

    • 可以被任何組件呼叫:

      const store = useOrderStore()
      store.loadOrders()
      
  • 為何不放在元件裡?

    • 邏輯與資料都集中 在 store,避免每個元件都重複撰寫 API 呼叫。

有些共用/重複用到的function可以在這邊定義,這樣就不需要再各自組件定義然後重複call function


在元件中使用 Store

<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 裡使用時不會失去響應能力

pinia使用的結構

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無須修改喔!!

https://ithelp.ithome.com.tw/upload/images/20250929/20121052wlY9H60IbO.png


4️⃣ SSOT vs Store:優缺點對照

方法 優點 缺點
只用 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,
讓飲料系統真正升級成 中央管理架構。


提醒 : 使用 Pinia Store 的注意事項!!

並非所有應用都一定需要用到 store
只有當 狀態跨越多個元件需要共用邏輯或方法 時,集中管理才會帶來優勢。

適合使用 Store 的情境

情境 說明
跨頁面或多層級共用狀態 例如:登入的 userProfile、網站的 theme(主題色系)、語系設定等,需要在多個頁面或組件中同時讀寫。
共用的商業邏輯或方法 例如:計算購物車金額、統計訂單數量、驗證登入權限等,不必在每個元件重複寫一次。
需要長期保存或快取 例如:使用者資訊、後端取得的設定檔,在整個 app 生命週期中都需要。

不需要 Store 的情境

情境 說明
單一頁面、單一元件就能處理的狀態 例如:單純的表單欄位輸入或一個小型元件的 UI 切換,直接用 refreactive 即可。
只在父子之間傳遞、結構很淺 propsemit 會更直覺,也更容易維護。

💡 重點提醒

  • Store 是 中央管理的工具,不是一定要用的規格。
  • 過度使用 store 會讓專案結構複雜、學習成本變高。
  • 先思考 資料流向共用需求,再決定是否要把狀態搬進 store。

我們還是會回顧到根據團隊的開發風格還有需求來定義使用的技術或是框架喔!!

  • 什麼時候應該使用 Pinia。
  • 什麼時候保持簡單、直接用 refcomputedprops 就足夠。

上一篇
Day10:watch 監聽資料變化,打造更智慧的客製化選單 (watch vs computed vs method)
下一篇
Day 12 : 可靠的中央行政單位:Pinia Getters / Setters
系列文
需求至上的 Vue 魔法之旅18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言