在開始實作之前,我們需要先釐清一個核心問題:
「當使用者在我們的系統中操作時,有哪些資訊是必須被記錄下來的?」
簡單來說,這些需要被記錄、會隨著時間或操作而改變的資訊,就是我們應用程式的 「狀態 (State)」。
另外,狀態又可以再區分成兩大類:
假設你的登入畫面有一個「選擇門市」的下拉選單:
<select>
<option value="">請選擇門市</option>
<option value="store001">台北信義店</option>
<option value="store002">台中逢甲店</option>
<option value="store003">高雄夢時代店</option>
</select>
在這個情境中:
屬於「前端狀態」的部分為使用者目前選中的門市 (selectedStore: "store001")
→ 系統需要記住使用者的選擇,登入後才能顯示對應資料。
屬於「伺服器狀態」的部分為門市列表本身 (台北信義店、台中逢甲店...)
→ 這是後端提供的參考資料,通常從 API 取得,並不會因為單一使用者操作而改變。
👉 本篇文章將聚焦在 前端狀態 (Client-side State) 的管理。
如果不記錄,這些資訊就會在頁面切換或元件銷毀時消失,導致系統「忘記」使用者的操作,進而破壞使用體驗。
每個狀態都有它的「生命週期」:
理解「狀態的生命週期」,能幫助我們決定該把狀態放在元件內部,還是全域。
在我們目前的 Prototype 中,這些「狀態」非常零散。
有些存在 HTML 的 value
裡,有些是全域的 JS 變數,導致資料流向混亂,難以追蹤和管理。
因此,第一步就是把所有狀態都明確地「盤點」出來。
我們可以這樣思考:
是否需要被記住?
是否會隨操作改變?
誰會使用它?
使用者與身份驗證 (Auth)
isLoggedIn
: 使用者是否已登入 (Boolean)currentUser
: 當前登入的使用者資訊 (包含姓名、角色等)selectedStore
: 在登入頁選擇所屬的門市訂單管理 (Order Management)
orderFilters
: 搜尋表單中的所有篩選條件orderList
: 從後端 API 獲取到的訂單列表isLoadingOrders
: 是否正在讀取訂單報價單流程 (Quotation Flow)
currentQuotation
: 正在建立中的報價單資訊 (包含客戶資料、商品列表等)盤點完狀態後,最重要的決定來了:
這些狀態應該放在哪裡?
在 Vue 中,我們主要有兩種選擇:
<script setup>
中,使用 ref()
或 reactive()
。📌 範例:訂單列表的檢視方式
在「訂單管理」頁面中,使用者可以選擇用「卡片」或「表格」兩種不同的方式來瀏覽訂單列表。
卡片視圖
表格視圖
這個功能需要一個狀態來記錄「使用者當前選擇的是哪種視圖」。
我們可以稱這個狀態為 activeView
,它的值可能是 card
或 table
。
誰關心它?
只有「訂單列表」(OrderResultsPanel.vue
) 需要知道 activeView
的值,
因為它要根據這個值來決定顯示 <OrderCardGrid>(卡片)
還是 <OrderTable>(表格)
。
誰不關心它?
OrderSearchPanel.vue
) 完全不關心結果的呈現方式。OrderManagement.vue
頁面本身也不關心,它只負責傳遞訂單資料給訂單列表。👉 結論:activeView
的影響範圍,完全沒有超出「訂單列表」這個元件,因此屬於 區域狀態。
📌 範例:建立報價單流程
從我們的檔案:
quotation-create-step1.html
→ 步驟一:選擇客戶quotation-create-step2.html
→ 步驟二:加入商品quotation-create-step3.html
→ 步驟三:預覽與確認可以看出這是一個分步驟的操作。
問題來了:
當使用者在步驟一選擇了客戶,進入步驟二時,應用程式要如何「記得」剛剛選的是哪個客戶?
如果我們把「選擇的客戶」當作 步驟一的區域狀態:
👉 這顯然不行。
我們需要一個獨立於任何單一元件之外的、公共的、全域的地方來存放「正在建立中的報價單」資料。
這個地方,就是 全域狀態儲存庫 (Global State Store),也就是我們將用 Pinia 建立的 quotationStore
。
📌 流程示意
步驟一
使用者選擇了客戶 A
→ 呼叫 quotationStore.setCustomer(客戶A)
→ 存到 全域 Store
步驟二
元件建立時
→ 從 quotationStore
取出客戶 A 的資料顯示
→ 新增商品後呼叫 quotationStore.addProduct(商品B)
步驟三
從 quotationStore
取出客戶 A + 商品 B 的資料
→ 顯示成完整的預覽報價單
👉 quotationStore
就像一個 公共購物車,跨步驟共享資料,不會因為頁面切換而消失。
當我們把狀態分成 區域狀態 (Local State) 和 全域狀態 (Global State) 之後,下一個問題就來了:
全域狀態要存在哪裡?又該怎麼存?
在 Vue 裡:
ref()
或 reactive()
,存放在元件內部即可。👉 所以我們需要一個 集中存放全域狀態的工具,這就是 狀態管理 (State Management)。
ref()
、reactive()
完美搭配簡單來說:
如果把「元件的區域狀態」想像成小抽屜,那麼 Pinia Store 就是一個全域的倉庫,讓不同頁面或元件都能隨時存取同一份狀態。
基於前面盤點的分類,我們可以大概規劃出以下三個 Pinia Store 來管理全域狀態:
userStore.js
isLoggedIn
: 是否已登入currentUser
: 使用者資訊selectedStore
: 當前選擇的門市login(userInfo)
: 登入並更新狀態logout()
: 登出並清空狀態orderStore.js
orderFilters
: 當前的查詢條件orderList
: 訂單清單isLoadingOrders
: 是否正在載入fetchOrders()
: 從 API 抓取訂單setFilters()
: 設定查詢條件quotationStore.js
currentQuotation
: 當前編輯中的報價單setCustomer(customer)
: 設定客戶資訊addProduct()
: 新增商品removeProduct()
: 移除商品今天,我們完成了從「產品化」角度最重要的思維轉變:
我們不再將資料視為散落在各處的變數,而是將它們系統性地規劃為 區域狀態 和 全域狀態。
明日,我們將繼續深入 Vueの呼吸・參之型,探討在 Vue 中的路由架構設計 —— 心を燃やせ!!