Pinia 是 Vue 3 的一个状态管理库,由 Vue.js 核心团队成员 Eduardo San Martin Morote(也被称为 Posva)创建。Pinia 旨在为 Vue 3 提供一个更简洁、更直观的状态管理解决方案,与 Vue 3 的 Composition API 无缝集成。
Pinia 的主要特点:
简洁的 API:Pinia 提供了一个简单直观的 API,使得定义、访问和修改状态变得非常容易。
与 Composition API 集成:Pinia 完全支持 Vue 3 的 Composition API,使得在组件中使用 store 变得非常简单。
DevTools 支持:Pinia 提供了与 Vue DevTools 的集成,使得状态的调试和跟踪变得更加容易。
类型安全:Pinia 在 TypeScript 中提供了很好的类型推断,这意味着你可以在不牺牲类型安全的情况下轻松地使用它。
模块化:与 Vuex 类似,Pinia 允许你将 store 分割成多个模块,使得状态管理更加模块化和可维护。
Pinia 的主要概念:
Store:Store 是 Pinia 中的核心概念,它包含状态、getters 和 actions。你可以使用 defineStore
方法定义一个 store。
State:State 是 store 中的数据。它是响应式的,这意味着当 state 改变时,依赖于它的组件会自动更新。
Getters:Getters 允许你定义从 state 派生的计算属性。它们是缓存的,只有当它们的依赖发生变化时才会重新计算。
Actions:Actions 是修改 state 的方法。与 Vuex 不同,Pinia 不区分 mutations 和 actions,你可以直接在 action 中修改 state。
Mutation 和 Action 的区别:
mutations
完成的,而 actions
用于处理异步操作。这导致了一些困惑,例如:当没有异步逻辑时是否还需要 actions
?在 Vuex 中,为了保持一致性,通常推荐的流程是:action -> mutation
,即使没有异步操作。树结构:
namespaced: true
属性。辅助函数:
mapState
, mapActions
, mapMutations
和 createNamespacedHelpers
。这些函数旨在简化在组件中使用状态和方法的过程,但它们也增加了额外的复杂性。单一 Store:
TypeScript 支持:
没有 Mutations:
mutations
,只保留了 actions
。这简化了状态管理的流程,不再需要区分直接修改状态还是异步操作。多个 Store:
TypeScript 支持:
简化的结构:
支持 Options API 和 Composition API:
npm create vite@latest
√ Project name: ... pinia-examples
√ Select a framework: » Vue
√ Select a variant: » TypeScript
Scaffolding project in D:\aprepare\pinia-examples...
Done. Now run:
cd pinia-examples
npm install
npm install pinia
npm run dev
pinia
是 Vue 3 的一个状态管理库,它提供了一个更简洁、更直观的方式来管理 Vue 应用的状态。其中,defineStore
是 pinia
中的一个核心方法,用于定义一个 store。
defineStore
方法接受一个唯一的 id 和一个对象,该对象可以包含 state、getters、actions 等。
src\main.ts
import { createApp } from 'vue';
import App from './Counter.vue';
import { createPinia } from 'pinia';
const app = createApp(App);
app.use(createPinia());
app.mount('#app');
src\stores\counterStore.ts
import { defineStore } from 'pinia';
export const useCounterStore = defineStore({
id: 'counter',
state: () => ({
count: 0
})
});
src\Counter.vue
<script setup lang="ts">
import { useCounterStore } from './stores/counterStore';
const counterStore = useCounterStore();
</script>
<template>
<div>
<p>Count: {{ counterStore.count }}</p>
<button @click="counterStore.count++">+</button>
</div>
</template>
<style scoped>
</style>
在 pinia
中,actions
是用于修改 store 中的状态的方法。与 Vuex
中的 actions
不同,pinia
的 actions
可以直接修改状态,不需要通过 mutations
。
以下是如何在 pinia
中使用 actions
的步骤:
actions
。actions
。src\stores\counterStore.ts
import { defineStore } from 'pinia';
export const useCounterStore = defineStore({
id: 'counter',
state: () => ({
count: 0
}),
+ actions: {
+ increment() {
+ this.count++;
+ },
+ decrement() {
+ this.count--;
+ }
+ }
});
src\Counter.vue
<script setup lang="ts">
import { useCounterStore } from './stores/counterStore';
const counterStore = useCounterStore();
</script>
<template>
<div>
<p>Count: {{ counterStore.count }}</p>
<button @click="counterStore.count++">+</button>
+ <button @click="counterStore.increment">Increment</button>
+ <button @click="counterStore.decrement">Decrement</button>
</div>
</template>
<style scoped>
</style>
在 pinia
中,getters
是用于计算和返回 store 中的派生状态的函数。它们类似于 Vue 组件中的计算属性,因为它们会根据它们的依赖进行缓存,并且只有当依赖发生变化时才会重新计算。
以下是如何在 pinia
中使用 getters
的步骤:
getters
。getters
。src\stores\userStore.ts
import { defineStore } from 'pinia';
export const useUserStore = defineStore({
id: 'user',
state: () => ({
firstName: 'zhang',
lastName: 'san'
}),
getters: {
// Getter for the full name
fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
});
src\User.vue
<script setup lang="ts">
import { useUserStore } from './stores/userStore';
const userStore = useUserStore();
</script>
<template>
<div>
<p>First Name: {{ userStore.firstName }}</p>
<p>Last Name: {{ userStore.lastName }}</p>
<p>Full Name (from getter): {{ userStore.fullName }}</p>
</div>
</template>
<style scoped>
</style>
storeToRefs
是一个实用函数,它可以将 store 的状态和 getters 转化为组合式 API 的 ref
对象。这样,你可以在模板中直接使用这些 ref
对象,而不是通过 store.xxx
的方式。
使用 storeToRefs
有以下好处:
src\User.vue
<script setup lang="ts">
import { useUserStore } from './stores/userStore';
+import { storeToRefs } from 'pinia';
+const userStore = useUserStore();
+const { firstName, lastName, fullName } = storeToRefs(userStore);
</script>
<template>
<div>
+ <p>First Name: {{ firstName }}</p>
+ <p>Last Name: {{ lastName }}</p>
+ <p>Full Name (from getter): {{ fullName }}</p>
</div>
</template>
<style scoped>
</style>
在 pinia
中,$patch
方法允许你一次性地更新 store 中的多个状态。这是一个非常有用的方法,尤其是当你需要在一个操作中更新多个状态时。
以下是如何在 pinia
中使用 $patch
方法的步骤:
$patch
方法来更新状态。src\stores\userStore.ts
import { defineStore } from 'pinia';
export const useUserStore = defineStore({
id: 'user',
state: () => ({
firstName: 'zhang',
lastName: 'san'
}),
getters: {
// Getter for the full name
fullName() {
return `${this.firstName} ${this.lastName}`;
}
},
+ actions: {
+ updateNames(payload: { firstName?: string; lastName?: string }) {
+ this.$patch(payload);
+ }
+ }
});
src\User.vue
<script setup lang="ts">
import { useUserStore } from './stores/userStore';
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
const { firstName, lastName, fullName } = storeToRefs(userStore);
+const updateUserData = () => {
+ userStore.updateNames({ firstName: 'li', lastName: 'si' });
+};
</script>
<template>
<div>
<p>First Name: {{ firstName }}</p>
<p>Last Name: {{ lastName }}</p>
<p>Full Name (from getter): {{ fullName }}</p>
+ <button @click="updateUserData">Update Names</button>
</div>
</template>
<style scoped>
</style>
src\App.vue
<script setup lang="ts">
import ProductList from './components/ProductList.vue';
import Cart from './components/Cart.vue';
</script>
<template>
<ProductList />
<Cart />
</template>
<style scoped></style>
src\components\ProductList.vue
<script setup lang="ts">
import { useProductsStore } from "../stores/productsStore";
import { useCartStore } from "../stores/cartStore";
const productsStore = useProductsStore();
const cartStore = useCartStore();
productsStore.fetchProducts();
</script>
<template>
<div>
<div v-for="product in productsStore.products" :key="product.id">
<p>
{{ product.name }} - ${{ product.price }} - Stock: {{ product.stock }}
</p>
<button
@click="cartStore.addToCart(product)"
:disabled="product.stock <= 0"
>
Add to Cart
</button>
</div>
</div>
</template>
src\components\Cart.vue
<script setup lang="ts">
import { useCartStore } from "../stores/cartStore";
const cartStore = useCartStore();
cartStore.loadFromLocalStorage();
</script>
<template>
<div>
<div v-for="item in cartStore.cartItems" :key="item.product.id">
<p>
{{ item.product.name }} - ${{ item.product.price }} x
{{ item.quantity }}
</p>
</div>
<p>Total Quantity: {{ cartStore.totalQuantity }}</p>
<p>Total Price: ${{ cartStore.totalPrice }}</p>
<button @click="cartStore.checkout">Checkout</button>
</div>
</template>
src\stores\productsStore.ts
import { defineStore } from 'pinia';
import { fetchProducts } from '../api/mock';
export const useProductsStore = defineStore({
id: 'products',
state: () => ({
products: []
}),
actions: {
async fetchProducts() {
this.products = await fetchProducts();
}
}
});
src\stores\cartStore.ts
import { defineStore } from 'pinia';
import { useProductsStore } from './productsStore';
import { checkout } from '../api/mock';
export const useCartStore = defineStore({
id: 'cart',
state: () => ({
cartItems: []
}),
getters: {
totalQuantity() {
return this.cartItems.reduce((acc, item) => acc + item.quantity, 0);
},
totalPrice() {
return this.cartItems.reduce((acc, item) => acc + item.product.price * item.quantity, 0);
}
},
actions: {
addToCart(product) {
const item = this.cartItems.find(i => i.product.id === product.id);
if (item) {
item.quantity++;
} else {
this.cartItems.push({ product, quantity: 1 });
}
const productsStore = useProductsStore();
const productInStore = productsStore.products.find(p => p.id === product.id);
if (productInStore) productInStore.stock--;
this.saveToLocalStorage();
},
async checkout() {
try {
await checkout(this.cartItems);
this.cartItems = [];
this.saveToLocalStorage();
alert("Checkout successful!");
} catch (error) {
alert("Checkout failed: " + error.message);
}
},
saveToLocalStorage() {
localStorage.setItem('cartItems', JSON.stringify(this.cartItems));
},
loadFromLocalStorage() {
const savedData = localStorage.getItem('cartItems');
if (savedData) {
this.cartItems = JSON.parse(savedData);
}
}
}
});
src\api\mock.ts
export const fetchProducts = async () => {
return [
{ id: 1, name: "Product A", price: 100, stock: 3 },
{ id: 2, name: "Product B", price: 200, stock: 3 },
];
};
export const checkout = async (cartItems) => {
if (cartItems.length === 0) {
throw new Error("Cart is empty");
}
return true;
};
src\main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { createPinia } from 'pinia';
import piniaPersist from 'pinia-plugin-persist'
const app = createApp(App);
const pinia = createPinia();
pinia.use(piniaPersist)
app.use(pinia);
app.mount('#app');
src\stores\cartStore.ts
import { defineStore } from 'pinia';
import { useProductsStore } from './productsStore';
import { checkout } from '../api/mock';
export const useCartStore = defineStore({
id: 'cart',
state: () => ({
cartItems: []
}),
getters: {
totalQuantity() {
return this.cartItems.reduce((acc, item) => acc + item.quantity, 0);
},
totalPrice() {
return this.cartItems.reduce((acc, item) => acc + item.product.price * item.quantity, 0);
}
},
actions: {
addToCart(product) {
const item = this.cartItems.find(i => i.product.id === product.id);
if (item) {
item.quantity++;
} else {
this.cartItems.push({ product, quantity: 1 });
}
const productsStore = useProductsStore();
const productInStore = productsStore.products.find(p => p.id === product.id);
if (productInStore) productInStore.stock--;
},
async checkout() {
try {
await checkout(this.cartItems);
this.cartItems = [];
alert("Checkout successful!");
} catch (error) {
alert("Checkout failed: " + error.message);
}
},
},
+ persist: {
+ enabled: true,
+ strategies:[
+ {
+ key:'cart',
+ storage:localStorage
+ }
+ ]
+ },
});
src\components\Cart.vue
<script setup lang="ts">
import { useCartStore } from "../stores/cartStore";
+const cartStore:any = useCartStore();
</script>
<template>
<div>
<div v-for="item in cartStore.cartItems" :key="item.product.id">
<p>
{{ item.product.name }} - ${{ item.product.price }} x
{{ item.quantity }}
</p>
</div>
<p>Total Quantity: {{ cartStore.totalQuantity }}</p>
<p>Total Price: ${{ cartStore.totalPrice }}</p>
<button @click="cartStore.checkout">Checkout</button>
</div>
</template>