從 Vuex 到 Pinia:Vue 狀態管理的進化

#vuex #pinia #狀態管理 #前端 #vue
從 Vuex 到 Pinia:Vue 狀態管理的進化
五倍技術部
技術文章
從 Vuex 到 Pinia:Vue 狀態管理的進化

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 中,你需要創建一個包含 statemutationsactionsgetters 的大型對象來定義一個 store。而在 Pinia 中,你可以使用 defineStore 函數來創建一個 store,這個函數接受一個包含 idstategettersactions 的對象。

其次,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 函數創建,這個函數接受一個包含 stategettersmutations 的物件。在這個例子中,我們定義了一個 items 狀態用來存放購物車的商品,一個 totalCost getter 是用來計算所有商品的總價格,兩個 mutation addItemremoveItem 分別來新增商品到購物車還有從刪除商品。

接著,我們需要在 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 的世界吧!