Vue.js,一個輕量級且易於上手的 JavaScript 框架,已經在全球範圍內獲得了廣泛的應用。
Vue.js 的狀態管理庫 Vuex,也為開發者提供了一個統一的狀態管理方案。然而,隨著 Vue.js 的發展和進化,我們看到了一個新的狀態管理庫的誕生 --- Pinia。在這篇文章中,我們將探討 Vuex 和 Pinia 的差異,並了解 Pinia 如何成為 Vue.js 狀態管理的新選擇。
Vuex 的起源與挑戰
Vuex 是 Vue.js 的官方狀態管理庫,它提供了一個集中式存儲來管理所有元件的狀態。Vuex 的核心概念包括狀態(state)、突變(mutations)、行為(actions)和 getters。這些概念使得狀態管理變得結構化且可預測。
然而,隨著應用的規模和複雜性的增加,Vuex 的一些限制開始浮現。例如,Vuex 的模塊結構可能導致應用的狀態分散在多個模塊中,使得狀態的追蹤和管理變得困難。此外,Vuex 的 API 在某些情況下可能顯得冗長和複雜,尤其是在使用 TypeScript 進行開發時。
Pinia 的誕生
為了解決這些問題,Vue.js 團隊開發了 Pinia。Pinia 是一個新的狀態管理庫,它提供了一個簡單且靈活的 API,並且對 TypeScript 有良好的支持。
Pinia
這個名字音近西班牙語的 Piña
,意思是鳳梨。而鳳梨是一種由多個小果實組成的複合果實,這與 Pinia 的組織方式相似。
Pinia 與 Vuex 的比較
讓我們來看一下 Pinia 與 Vuex 的一些主要差異。
首先,Pinia 提供了一個更簡單的 API。在 Vuex 中,你需要創建一個包含 state
、mutations
、actions
和 getters
的大型對象來定義一個 store。而在 Pinia 中,你可以使用 defineStore
函數來創建一個 store,這個函數接受一個包含 id
、state
、getters
和 actions
的對象。
其次,Pinia 對 TypeScript 有更好的支持。在 Vuex 中,由於 JavaScript 的動態特性,很難對 store 的狀態和行為進行靜態類型檢查。而 Pinia 通過使用 defineStore
函數和提供的 useStore
函數,可以讓 TypeScript 對 store 的狀態和行為進行靜態類型檢查。
最後,Pinia 提供了更靈活的 store 組織方式。在 Vuex 中,所有的 store 都必須在一開始就被定義和創建,並且被組織成一個大的 store 樹。而在 Pinia 中,你可以在任何地方創建和使用 store,這使得你可以更靈活地組織和管理你的狀態。
Vuex 的實際應用
讓我們來看一個實際的例子,來了解如何在 Vue.js 應用中使用 Vuex。
假設我們正在開發一個購物車應用,我們需要在購物車中添加商品,並計算購物車中所有商品的總價格。
首先,我們需要安裝 Vuex:
npm install vuex
安裝完後,我們先建立一個 store,在 src 底下新增一個 stores 資料夾,並新增 cart.js:
import { createStore } from "vuex";
const store = createStore({
state: {
items: []
},
getters: {
totalCost: (state) =>
state.items.reduce((total, item) => total + item.price * item.quantity, 0)
},
mutations: {
addItem(state, item) {
let existingItem = state.items.find((i) => i.id === item.id);
if (existingItem) {
existingItem.quantity += item.quantity ? item.quantity : 1;
} else {
state.items.push({
...item,
quantity: item.quantity ? item.quantity : 1
});
}
},
removeItem(state, itemId) {
state.items = state.items.filter((item) => item.id !== itemId);
}
}
});
export default store;
Vuex 的 store 必須要先從 createStore
函數創建,這個函數接受一個包含 state
、getters
和 mutations
的物件。在這個例子中,我們定義了一個 items
狀態用來存放購物車的商品,一個 totalCost
getter
是用來計算所有商品的總價格,兩個 mutation
addItem
和 removeItem
分別來新增商品到購物車還有從刪除商品。
接著,我們需要在 main.js
中引入 剛剛建立的 store:
import { createApp } from "vue";
import App from "./App.vue";
import store from "./stores/cart";
const app = createApp(App);
app.use(store);
app.mount("#app");
然後建立兩個元件,分別是商品列表和購物車:
// src/components/Cart.vue
<template>
<div>
<h2>你的購物車</h2>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }} - ${{ item.price }} x {{ item.quantity }}
<button @click="removeItem(item.id)">刪除</button>
</li>
</ul>
<div>總費用: ${{ totalCost }}</div>
</div>
</template>
<script setup>
import { computed } from "vue";
import { useStore } from "vuex";
const store = useStore();
const items = computed(() => store.state.items);
const totalCost = computed(() => store.getters.totalCost);
const removeItem = (id) => store.commit("removeItem", id);
</script>
這一個 Cart 元件,主要用於顯示購物車中的商品列表,並且可以從購物車中移除商品。我們可以使用 useStore
函數來取得 store,並且使用 computed
函數來計算 totalCost
這個 getter 的值。最後,我們可以使用 commit
函數來呼叫 removeItem
這個 mutation。
// src/components/ItemList.vue
<template>
<div>
<h2>商品</h2>
<ul>
<li v-for="product in products" :key="product.id">
{{ product.name }} - ${{ product.price }}
<button @click="addToCart(product)">加入到購物車</button>
</li>
</ul>
</div>
</template>
<script setup>
import { useStore } from "vuex";
const store = useStore();
const products = [
{ id: 1, name: "商品 1", price: 100 },
{ id: 2, name: "商品 2", price: 200 },
{ id: 3, name: "商品 3", price: 300 },
];
const addToCart = (product) => store.commit("addItem", product);
</script>
這一個 ItemList 元件,主要用於顯示商品列表,並且可以將商品加入到購物車中。我們可以使用 useStore
函數來取得 store,並且使用 commit
函數來呼叫 addItem
這個 mutation。
最後可以看一下 demo 的效果:
Pinia 的實際應用
我們一樣用購物車的例子,來了解如何在 Vue.js 應用中使用 Pinia。
首先,我們需要安裝 Pinia:
npm install pinia
安裝完後,我們需要在 main.js
中引入 Pinia:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
然後,就可以來創建一個 Pinia store:
// src/stores/cart.js
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: []
}),
getters: {
totalCost(state) {
return state.items.reduce(
(total, item) => total + item.price * item.quantity,
0
)
}
},
actions: {
addItem(item) {
let existingItem = this.items.find((i) => i.id === item.id)
if (existingItem) {
const newItem = {
...existingItem,
quantity: existingItem.quantity + item.quantity
}
this.items = this.items.map((i) => (i.id === item.id ? newItem : i))
} else {
this.items.push(item)
}
},
removeItem(itemId) {
this.items = this.items.filter((item) => item.id !== itemId)
}
}
})
在這個例子中,我們創建了一個名為 'cart' 的 store,主要有一個狀態:'items' 為一個陣列,用於存儲購物車中的商品。我們還定義了一個 getter
'totalCost',用於計算購物車的總費用。此外,我們還定義了三個 action
:'addItem'、'removeItem',用於添加商品到購物車、從購物車中移除商品。
接著我們有兩個元件,一個是用於顯示購物車的元件,另一個是用於顯示商品列表的元件。首先,我們來看一下購物車元件:
// src/components/Cart.vue
<template>
<div>
<h2>你的購物車</h2>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }} - ${{ item.price }} x {{ item.quantity }}
<button @click="removeItem(item.id)">刪除</button>
</li>
</ul>
<div>總費用: ${{ totalCost }}</div>
</div>
</template>
<script setup>
import { storeToRefs } from "pinia";
import { useCartStore } from "../stores/cart";
const cartStore = useCartStore();
const { items, totalCost } = storeToRefs(cartStore);
function removeItem(id) {
cartStore.removeItem(id);
}
</script>
這一個 Cart 元件,我們使用 useCartStore
來獲取 cart
store 的實例,然後我們使用 storeToRefs
來將 store 的狀態轉換為 ref,這樣我們就可以在模板中直接使用這些狀態。最後,我們還定義了一個 removeItem
函數,用於從購物車中移除商品。
接著我們來看一下商品列表元件:
// src/components/ItemList.vue
<template>
<div>
<h2>商品</h2>
<ul>
<li v-for="product in products" :key="product.id">
{{ product.name }} - ${{ product.price }}
<button @click="addToCart(product)">加入到購物車</button>
</li>
</ul>
</div>
</template>
<script setup>
import { useCartStore } from "../stores/cart";
const cartStore = useCartStore();
const products = [
{ id: 1, name: "商品 1", price: 100 },
{ id: 2, name: "商品 2", price: 200 },
{ id: 3, name: "商品 3", price: 300 },
];
function addToCart (product) {
cartStore.addItem({
id: product.id,
name: product.name,
price: product.price,
quantity: 1,
});
}
</script>
這一個 ProductList 元件,這裡我們定義了一個 products
陣列,用於當作我們的假資料。最後,定義一個 addToCart
函數,用於將商品添加到購物車。
這樣就完成 Pinia 的範例,效果跟 Vuex 的範例是一樣的。
從 Vuex 到 Pinia,Vue 的狀態管理已經經歷了一次重要的進化。Pinia 以其簡單的 API、良好的 TypeScript 支持和靈活的 store 組織方式,為 Vue 開發者提供了一個新的狀態管理選擇。
如果你對 Vue 和 Pinia 有興趣並且想要有更深的了解,無論你是一個 Vue 的新手,還是一個有經驗的開發者,非常推薦你參加 Vue.js 3.x 與 Pinia 前端開發實戰 課程。這個課程將帶你從基礎到進階,學習如何在 Vue 中使用 Pinia 進行狀態管理,並通過實戰來掌握這些知識。快來加入我們,一起探索 Vue 和 Pinia 的世界吧!