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.jssample code


為什麼要有中央狀態管理

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

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

先來一個小的示意圖:

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

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

component

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

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

component

圖片沒有寫箭頭是幹嘛,不過可以自己跟上張圖對照一下XD

然後就可以開始想像,要是沒有中央狀態管理,組件一多就會大爆炸,然而有了之後,組件在多,需要就拿就很輕鬆。

而中間那個狀態也可以有很多塊,通常一個都會叫做 Store ,例如範例他幫你建的就叫做 CounterStore ,那需要用到跟 Counter 相關的功能就去找他拿或是操作。


跟全域變數有什麼差?

這個問題我第一時間的想法是,就算沒差,至少有人已經幫你做了比較好「管理」的解決方案。

因為如果單純放在 window ,他可能也沒有先前提到的 reactive,就算你強制幫他存了 reactive 的東西(我不確定會不會動,我沒有實驗XD),那 window 是一個誰都可以改的全域物件,那誰知道你要用的東西會不會被後人做了哪些事導致東西壞掉 (?)

在當初 Vue 還在第二版的時候,有次比賽沒有提供中央狀態管理的套件,當初熱門的還是 Vuex,而當時我的解決方案是多建立一個 Vue 實體,然後綁進我的 VuePrototype 上使用,這樣全域就都可以用單例模式的方式來使用這些狀態。

不過上面這個方式 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 還有 stategettersactions

這邊可以把那三個想成

  • 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: 0Double 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日.