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 开发者提供了一个新的状态管理选择。