# 前言
我們已經知道元件與元件之間的溝通可以使用 props
與 emits
,不過這東西就只能設定兩個元件溝通的狀態。
如果我們的元件樹的結構長得比較複雜一點,尾端的元件如果要將狀態傳回根元件,就必須一層一層又一層的傳遞狀態,單一一個狀態可能已經很麻煩了,要是各種狀態需要交互運作,那將會一場維護上的災難。
為此 Vue 提供了一個狀態管理工具:Vuex。這個狀態管理工具隨著版本的演進,更變了他的名稱叫做 Pinia,Logo 是一顆大鳳梨。
長得頗為可愛 XD
這篇文章預計要紀錄我們該怎麼在 Vue 中使用這顆大鳳梨
# 安裝大鳳梨
要使用大鳳梨,第一步就是先安裝他,一樣可以使用 npm
或是 yarn
npm install pinia |
yarn install pinia |
# 建立大鳳梨的實體
如同 createApp
與 createRouter
那樣,我們也需要建構這顆大鳳梨的實體,建立實體的方法,是 '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 定義的 state
、 getter
、 action
<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 的一個優點是,如果有多處元件都需要更改狀態的話,我們可以在大鳳梨中定義好「更改狀態的邏輯方法」,在需要的地方中引入方法後,即可達成這個需求,不用再單獨對各個元件定義 props
與 emits
。畢竟寫 code 這種事情,多寫一個地方代表要多維護一個地方,能在單一位置處理好一套邏輯持續複用,才能讓我們的程式更優雅。
同時根據官方教學所說的,無論我們在開發大專案或是小專案,都很適合直接引入 Pinia 進行開發,畢竟他其實也沒什麼副作用。