Vue3 - Pinia 中央狀態管理
tags: Vue
category: Front-End
description: Vue3 - Pinia 中央狀態管理
created_at: 2022/07/25 15:00:00
回到 手把手開始寫 Vue3
前言
終於進入到這個章節了,老樣子跟路由一樣,在最一開始,可以先回顧到 第一篇 - 開新專案與環境設定 這裡,在開啟專案的時候就將 Pinia
的選項打開,讓他幫你裝好。
✔ Add Pinia for state management? … No / Yes
這樣建立好專案之後,他一樣很貼心的會有一些預設的程式碼和改一下 main.js
。
然後基本上就是多出一個 stores
的資料夾,然後裡面放著一個 counter.js
的 sample code
。
為什麼要有中央狀態管理
自己管理好自己不行嗎? 其他組件需要的時候再透過 props
傳下去不行嗎?
Ans
: 可以,但是變得複雜之後會很亂
先來一個小的示意圖:
假設你有多個組件,然後你一直往下層傳遞,然後又要偶爾 emit
上來,那再想像更複雜一些,整個邏輯就會變得超級複雜。
如果都有用到是還好,但也會有種情況可能是 A
傳去 B
傳去 C
,但是 B
根本沒用到那個 props
那是不是有點怪(?)
待會再寫一個範例是不使用中央狀態管理跟使用的 code
的差異。
先看看圖,如果有中央狀態管理:
圖片沒有寫箭頭是幹嘛,不過可以自己跟上張圖對照一下XD
然後就可以開始想像,要是沒有中央狀態管理,組件一多就會大爆炸,然而有了之後,組件在多,需要就拿就很輕鬆。
而中間那個狀態也可以有很多塊,通常一個都會叫做 Store
,例如範例他幫你建的就叫做 CounterStore
,那需要用到跟 Counter
相關的功能就去找他拿或是操作。
跟全域變數有什麼差?
這個問題我第一時間的想法是,就算沒差,至少有人已經幫你做了比較好「管理」的解決方案。
因為如果單純放在 window
,他可能也沒有先前提到的 reactive
,就算你強制幫他存了 reactive
的東西(我不確定會不會動,我沒有實驗XD),那 window
是一個誰都可以改的全域物件,那誰知道你要用的東西會不會被後人做了哪些事導致東西壞掉 (?)
在當初 Vue
還在第二版的時候,有次比賽沒有提供中央狀態管理的套件,當初熱門的還是 Vuex
,而當時我的解決方案是多建立一個 Vue
實體,然後綁進我的 Vue
的 Prototype
上使用,這樣全域就都可以用單例模式的方式來使用這些狀態。
不過上面這個方式 for
比賽用比較土炮,真的要開發還是用現今相對成熟的解決方案比較適合。
定義方式
先看看範例生成的 code
import { defineStore } from 'pinia'
export const useCounterStore = defineStore({
id: 'counter',
state: () => ({
counter: 0
}),
getters: {
doubleCount: (state) => state.counter * 2
},
actions: {
increment() {
this.counter++
}
}
})
基本上他把基本的用法都放上去了,也算夠用了(?),然後他就是定義一個 Store
然後定義一個不重複的 id
還有 state
、getters
與 actions
。
這邊可以把那三個想成
state
=>data
getters
=>computed
actions
=>methods
所以他定義了一個 data
裡面有 counter
預設是 0
然後提供了一個 computed
可以用 doubleCount
取到兩倍的 counter
最後也提供了一個 method
叫做 increment
可以把 counter++
他的行為大概是這樣。
基礎使用
定義好之後就可以在任何你想用的組件中使用了。
一樣把 App.vue
清乾淨,變成下面這樣
<script setup>
import { useCounterStore } from "./stores/counter";
const counterStore = useCounterStore();
</script>
<template>
<div>Counter: {{ counterStore.counter }}</div>
<div>Double Count: {{ counterStore.doubleCount }}</div>
</template>
這樣就會看到畫面上有 Counter: 0
與 Double Count: 0
但是都是 0
沒感覺,所以幫他做一個按鈕,去呼叫 increment
<button @click="counterStore.increment">Increment</button>
然後去戳他,就會看到畫面上的數字在變化。
修改 state
- 直接修改
// counter 會 + 1
counterStore.counter++;
- 使用
patch
可以一次修改多個
counterStore.$patch({
counter: counterStore.counter + 1,
});
這樣寫可能在改某些東西很麻煩,例如陣列,所以也提供另一種用法
counterStore.$patch((state) => {
state.counter++;
});
帶入一個函數就可以直接抓到當下的 state
,所以換成陣列的話你也可以 state.xxx.push()
重置 state
有時候可能希望把狀態重置,可以簡單使用 $reset
做到。
counterStore.$reset();
訂閱狀態
可以在狀態被修改的時候做一些處理
counterStore.$subscribe((mutation, state) => {
console.log(mutation, state);
});
可以自己點開看看裡面有什麼,然後再做對應的處理。
然後預設情況下在組件被卸載之後就會自動取消訂閱,如果希望保留就要下第二個參數。
counterStore.$subscribe(
(mutation, state) => {
console.log(mutation, state);
},
{ detached: true }
);
非同步的 action
這邊假設做一個很假的發 API
拿資料的展示,其實只是讓資料延遲1秒左右在回來
// ...
actions: {
increment() {
this.counter++;
},
async requestData() {
const data = await new Promise((resolve) =>
setTimeout(() => {
resolve({ counter: 500 });
}, 1000)
);
this.counter = data.counter;
},
},
// ...
這樣在其他地方如果要用,用法也是一樣
counterStore.requestData();
假設開個非同步的組件來玩玩
src/components/TheCounter.vue
<script setup>
import { useCounterStore } from '../stores/counter';
const counterStore = useCounterStore();
await counterStore.requestData();
</script>
<template>
<div>Counter: {{ counterStore.counter }}</div>
<div>Double Count: {{ counterStore.doubleCount }}</div>
<button @click="counterStore.increment">Increment</button>
</template>
然後再 App.vue
使用他,並加上之前提過的 Suspense
<script setup>
import TheCounter from "./components/TheCounter.vue";
</script>
<template>
<Suspense>
<TheCounter />
<template #fallback> Loading... </template>
</Suspense>
</template>
這時就會看到畫面在一開始顯示 Loading...
,而約一秒後顯示出原本的內容。
總結
以上大概就是基本 pinia
常用的使用方式了,因為目前都使用 setup
的語法糖,所以我就自動略過了比較麻煩的 options API
就是還要在那邊 mapState
之類的行為XD
其他等之後如果做範例有需要用到在提吧(偷懶)