# 前言

我們已經知道元件與元件之間的溝通可以使用 propsemits ,不過這東西就只能設定兩個元件溝通的狀態。

如果我們的元件樹的結構長得比較複雜一點,尾端的元件如果要將狀態傳回根元件,就必須一層一層又一層的傳遞狀態,單一一個狀態可能已經很麻煩了,要是各種狀態需要交互運作,那將會一場維護上的災難。

為此 Vue 提供了一個狀態管理工具:Vuex。這個狀態管理工具隨著版本的演進,更變了他的名稱叫做 Pinia,Logo 是一顆大鳳梨。

長得頗為可愛 XD

這篇文章預計要紀錄我們該怎麼在 Vue 中使用這顆大鳳梨

# 安裝大鳳梨

要使用大鳳梨,第一步就是先安裝他,一樣可以使用 npm 或是 yarn

npm install pinia
yarn install pinia

# 建立大鳳梨的實體

如同 createAppcreateRouter 那樣,我們也需要建構這顆大鳳梨的實體,建立實體的方法,是 'pinia' 引入的 createPinia 方法

import { createPinia } from "pinia";

建構實體與引用實體:

// 建立大鳳梨實體
const pinia = createPinia();
// 建立 Vue 實體
const app = createApp(App);
// 在 Vue 實體中使用大鳳梨
app.use(pinia);
app.mount("#app");

這個寫法也可以用 「串」 的:

createApp(App).use(pinia).mount("#app");

如果覺得建構實體、引用實體兩個行為要分開寫很麻煩,也可以直接把他們寫在一起:

const app = createApp(App);
app.use(createPinia());
app.mount("#app");

# 大鳳梨的狀態管理觀念

事實上 Pinia 語法跟 Vue 一樣,區分為 Option 與 Setup 兩種語法,筆者認為寫起來最優雅的會是 Setup 語法,而且他的寫法幾乎與 <script setup> 內容一樣,不用特別為了要使用 Pinia 而特別去記兩套語法。

不過由於 Pinia 官網中幾乎都是使用 Option 語法當範例,所以還是蠻有機會遇到跟著官網使用 Option 語法的團隊,為此我們可以用 Option 語法使用的專有名詞來對應到 Setup 語法中

接下來要使用的語法都會是 Setup 語法,不過帶到 Option 的專有名詞。

# Store 儲存庫

要建立一個全域的狀態管理工具,需要有一個 Store 儲存庫,可以把他想像成一個倉庫,專門管理「某部分」的商業邏輯。

一個 Vue 專案的 Store 不一定只有一個,可以根據需求建立多個儲存庫,以達到好維護的狀態管理效果。

要定義一個儲存庫,一般我們會在 src 資料夾中新增一個 store 資料夾,並且建立一個預設資料夾 index.js

在 js 檔案中透過 pinia 引入 defineStore 方法:

import { defineStore } from "pinia";

我們要在這個方法中定義兩個參數,一個是 store 名稱,另一個是 callback。

第二個參數的內容,就是 Option Store 與 Setup Store 的差異所在,有興趣的讀者可以直接參考 Pinia 官網

export const useBookStore = defineStore("book", () => {
  //store 其他邏輯
});

store 第一個參數的名稱會被當成 ID,Pinia 會把他用來連結 devTools。

defineStore 會有一個回傳值,一樣可以任意命名,不過官方建議這個名稱以 use... 開頭為命名風格,且之後要使用這個 store ,都會在程式中用這個命名引入。

# State

State 狀態,就是對應到 Vue 的 ref()
其用法就跟之前在 Vue 裡面使用 ref() 一樣,該引入的要引入,該使用 .value 就使用。
唯一要注意的小地方是,在 Store 中使用的「變數」,記得都要 return 出來,之後才有辦法使用。

基本上,state 同時也代表著 store 的核心內容。

import { ref } from "vue";
export const useBookStore = defineStore("book", () => {
  const books = ref([
    { id: 1, title: "0 陷阱!0 誤解!8 天重新認識JavaScript!" },
    { id: 2, title: "重新認識Vue.js: 008 天絕對看不完的Vue.js 3.0 指南" },
    {
      id: 3,
      title:
        "D3.js資料視覺化實用攻略:完整掌握Web開發技術,繪製互動式圖表不求人",
    },
  ]);
  return { books };
});

# Getter

既然 ref() 代表 State ,那麼 Getter 就是 computed() 了。

在 Pinia 中, Getter 完全等同於 State 的 computed()
如果看不懂什麼叫「State 的 computed() 」的話,可以回頭想想 computed() 在 Vue 扮演的腳色,他是一個用來以狀態 (State) 產出狀態 (State) 的工具,後者會是前者的「計算值」,所以才會說「Getter 代表 State 的 computed()

在 Store 使用 Computed 的時候一樣,該引入的時候要引入,寫完之後記得要 return 出來。

import { ref, computed } from "vue";
export const useBookStore = defineStore("book", () => {
  const books = ref([
    { id: 1, title: "0 陷阱!0 誤解!8 天重新認識JavaScript!" },
    { id: 2, title: "重新認識Vue.js: 008 天絕對看不完的Vue.js 3.0 指南" },
    {
      id: 3,
      title:
        "D3.js資料視覺化實用攻略:完整掌握Web開發技術,繪製互動式圖表不求人",
    },
  ]);
  // getter
  const hasBook = computed(() => books.value.length > 0);
  // 記得要 return
  return { books, hasBook };
});

# Action

有了 狀態,有了 計算值,當然也少不了「方法」。
Action 就是這個 method ,他的用法就跟 Vue 中使用 「方法」的方式一樣,定義一個函數就搞定了。

import { ref, computed } from "vue";
export const useBookStore = defineStore("book", () => {
  const books = ref([
    { id: 1, title: "0 陷阱!0 誤解!8 天重新認識JavaScript!" },
    { id: 2, title: "重新認識Vue.js: 008 天絕對看不完的Vue.js 3.0 指南" },
    {
      id: 3,
      title:
        "D3.js資料視覺化實用攻略:完整掌握Web開發技術,繪製互動式圖表不求人",
    },
  ]);
  const hasBook = computed(() => books.value.length > 0);
  // action
  const plusBook = () => {
    books.value.push({
      id: books.value.length + 1,
      title: "New Book",
    });
  };
  // action
  const deleteBook = () => {
    books.value.pop();
  };
  // 一樣記得要 return
  return { books, hasBook, plusBook, deleteBook };
});

# 使用 Store

要使用 Store ,在需要元件中引入當初對 Store 回傳值的命名 useXXXStore 即可直接使用:

import { useBookStore } from "./store";
const store = useBookStore();

接著就可以使用這個 store 定義的 stategetteraction

<button @click="store.plusBook">新增一本書</button>
<ul>
  <li v-for="book in store.books" :key="book.id">{{ book.title }}</li>
</ul>
<div>是否有書 : {{ store.hasBook ? "是" : "否"}}</div>

# 補充:在大鳳梨中使用 watch

我們也可以在 defineStore 的 callback 內容中,使用 watch 監聽 state 的變化,使用方式就跟一般的 watch 寫法一樣:

import { defineStore } from "pinia";
import { ref, computed, watch } from "vue";
export const useBookStore = defineStore("book", () => {
  const books = ref([
    { id: 1, title: "0 陷阱!0 誤解!8 天重新認識JavaScript!" },
    { id: 2, title: "重新認識Vue.js: 008 天絕對看不完的Vue.js 3.0 指南" },
    {
      id: 3,
      title:
        "D3.js資料視覺化實用攻略:完整掌握Web開發技術,繪製互動式圖表不求人",
    },
  ]);
  const hasBook = computed(() => books.value.length > 0);
  const plusBook = () => {
    books.value.push({
      id: books.value.length + 1,
      title: "New Book",
    });
  };
  const deleteBook = () => {
    books.value.pop();
  };
  // 在 Pinia 中使用 watch
  watch(
    books,
    (val) => {
      // 當書本資料發生變化時,將資料存入 localStorage
      localStorage.setItem("piniaState", JSON.stringify(val));
    },
    { deep: true }
  );
  return { books, hasBook, plusBook, deleteBook };
});

# 後記

這篇文章主要是在記錄 Pinia 的語法,範例程式的邏輯或許直接寫在 component 也可以達到一樣的效果。

不過使用 Pinia 的一個優點是,如果有多處元件都需要更改狀態的話,我們可以在大鳳梨中定義好「更改狀態的邏輯方法」,在需要的地方中引入方法後,即可達成這個需求,不用再單獨對各個元件定義 propsemits 。畢竟寫 code 這種事情,多寫一個地方代表要多維護一個地方,能在單一位置處理好一套邏輯持續複用,才能讓我們的程式更優雅。

同時根據官方教學所說的,無論我們在開發大專案或是小專案,都很適合直接引入 Pinia 進行開發,畢竟他其實也沒什麼副作用。