Vue3 - Pinia 中央狀態管理

tags: Vue
category: Front-End
description: Vue3 - Pinia 中央狀態管理
created_at: 2022/07/25 15:00:00

cover image


回到 手把手開始寫 Vue3


前言

終於進入到這個章節了,老樣子跟路由一樣,在最一開始,可以先回顧到 第一篇 - 開新專案與環境設定 這裡,在開啟專案的時候就將 Pinia 的選項打開,讓他幫你裝好。

✔ Add Pinia for state management? … No / Yes

這樣建立好專案之後,他一樣很貼心的會有一些預設的程式碼和改一下 main.js。

然後基本上就是多出一個 stores 的資料夾,然後裡面放著一個 counter.js 的 sample code。


為什麼要有中央狀態管理

自己管理好自己不行嗎? 其他組件需要的時候再透過 props 傳下去不行嗎?

Ans: 可以,但是變得複雜之後會很亂

先來一個小的示意圖:

假設你有多個組件,然後你一直往下層傳遞,然後又要偶爾 emit 上來,那再想像更複雜一些,整個邏輯就會變得超級複雜。

如果都有用到是還好,但也會有種情況可能是 A 傳去 B 傳去 C ,但是 B 根本沒用到那個 props 那是不是有點怪(?)

component

待會再寫一個範例是不使用中央狀態管理跟使用的 code 的差異。

先看看圖,如果有中央狀態管理:

component

圖片沒有寫箭頭是幹嘛,不過可以自己跟上張圖對照一下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

  1. 直接修改
// counter 會 + 1
counterStore.counter++;
  1. 使用 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

其他等之後如果做範例有需要用到在提吧(偷懶)




最後更新時間: 2022年07月25日.