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>