1.Pinia #

1.1 介绍 #

Pinia 是 Vue 3 的一个状态管理库,由 Vue.js 核心团队成员 Eduardo San Martin Morote(也被称为 Posva)创建。Pinia 旨在为 Vue 3 提供一个更简洁、更直观的状态管理解决方案,与 Vue 3 的 Composition API 无缝集成。

Pinia 的主要特点:

  1. 简洁的 API:Pinia 提供了一个简单直观的 API,使得定义、访问和修改状态变得非常容易。

  2. 与 Composition API 集成:Pinia 完全支持 Vue 3 的 Composition API,使得在组件中使用 store 变得非常简单。

  3. DevTools 支持:Pinia 提供了与 Vue DevTools 的集成,使得状态的调试和跟踪变得更加容易。

  4. 类型安全:Pinia 在 TypeScript 中提供了很好的类型推断,这意味着你可以在不牺牲类型安全的情况下轻松地使用它。

  5. 模块化:与 Vuex 类似,Pinia 允许你将 store 分割成多个模块,使得状态管理更加模块化和可维护。

Pinia 的主要概念:

  1. Store:Store 是 Pinia 中的核心概念,它包含状态、getters 和 actions。你可以使用 defineStore 方法定义一个 store。

  2. State:State 是 store 中的数据。它是响应式的,这意味着当 state 改变时,依赖于它的组件会自动更新。

  3. Getters:Getters 允许你定义从 state 派生的计算属性。它们是缓存的,只有当它们的依赖发生变化时才会重新计算。

  4. Actions:Actions 是修改 state 的方法。与 Vuex 不同,Pinia 不区分 mutations 和 actions,你可以直接在 action 中修改 state。

1.2 对比vuex #

1.2.1 Vuex: #

  1. Mutation 和 Action 的区别

    • 在 Vuex 中,状态的直接修改是通过 mutations 完成的,而 actions 用于处理异步操作。这导致了一些困惑,例如:当没有异步逻辑时是否还需要 actions?在 Vuex 中,为了保持一致性,通常推荐的流程是:action -> mutation,即使没有异步操作。
  2. 树结构

    • Vuex 使用一个集中式的状态树,这意味着所有的状态都存储在一个大的对象中。这可能导致状态访问变得冗余,尤其是在大型应用中。
    • 为了解决这个问题,Vuex 提供了模块系统,但这又引入了另一个问题:模块命名可能与状态命名冲突,而且每个模块都需要设置 namespaced: true 属性。
  3. 辅助函数

    • Vuex 提供了一系列的辅助函数,如 mapState, mapActions, mapMutationscreateNamespacedHelpers。这些函数旨在简化在组件中使用状态和方法的过程,但它们也增加了额外的复杂性。
  4. 单一 Store

    • Vuex 只允许一个集中的 store。所有的状态、mutations、actions 和 getters 都定义在这个 store 中,尽管可以使用模块来组织它们。
  5. TypeScript 支持

    • Vuex 4.x 对 TypeScript 提供了基本的支持,但这主要是通过添加类型注释来实现的。这意味着在某些情况下,类型推断可能不是很准确或不够友好。

1.2.2 Pinia: #

  1. 没有 Mutations

    • Pinia 去掉了 mutations,只保留了 actions。这简化了状态管理的流程,不再需要区分直接修改状态还是异步操作。
  2. 多个 Store

    • Pinia 允许定义多个独立的 stores。每个 store 都是独立的,并且可以相互调用。
  3. TypeScript 支持

    • Pinia 为 TypeScript 提供了一流的支持。与 Vuex 相比,Pinia 的类型推断更加准确和友好。
  4. 简化的结构

    • Pinia 告别了 Vuex 的树结构,使得状态管理变得更加直观和简单。
  5. 支持 Options API 和 Composition API

    • Pinia 支持 Vue 3 的新特性,包括 Options API 和 Composition API。这使得在组件中使用 Pinia 更加灵活。

2.创建项目 #

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

3.defineStore #

pinia 是 Vue 3 的一个状态管理库,它提供了一个更简洁、更直观的方式来管理 Vue 应用的状态。其中,defineStorepinia 中的一个核心方法,用于定义一个 store。

defineStore 方法接受一个唯一的 id 和一个对象,该对象可以包含 state、getters、actions 等。

3.1 main.ts #

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');

3.2 counterStore.ts #

src\stores\counterStore.ts

import { defineStore } from 'pinia';

export const useCounterStore = defineStore({
  id: 'counter',

  state: () => ({
    count: 0
  })
});

3.3 Counter.vue #

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>

4.actions #

pinia 中,actions 是用于修改 store 中的状态的方法。与 Vuex 中的 actions 不同,piniaactions 可以直接修改状态,不需要通过 mutations

以下是如何在 pinia 中使用 actions 的步骤:

  1. 在你的 store 中定义 actions
  2. 在 Vue 组件中调用这些 actions

4.1 counterStore.ts #

src\stores\counterStore.ts

import { defineStore } from 'pinia';

export const useCounterStore = defineStore({
  id: 'counter',

  state: () => ({
    count: 0
  }),
+  actions: {
+    increment() {
+      this.count++;
+    },
+    decrement() {
+      this.count--;
+    }
+  }
});

4.2 Counter.vue #

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>

5.getters #

pinia 中,getters 是用于计算和返回 store 中的派生状态的函数。它们类似于 Vue 组件中的计算属性,因为它们会根据它们的依赖进行缓存,并且只有当依赖发生变化时才会重新计算。

以下是如何在 pinia 中使用 getters 的步骤:

  1. 在你的 store 中定义 getters
  2. 在 Vue 组件中使用这些 getters

5.1 userStore.ts #

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}`;
    }
  }
});

5.2 User.vue #

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>

6.storeToRefs #

storeToRefs 是一个实用函数,它可以将 store 的状态和 getters 转化为组合式 API 的 ref 对象。这样,你可以在模板中直接使用这些 ref 对象,而不是通过 store.xxx 的方式。

使用 storeToRefs 有以下好处:

  1. 在模板中,你可以直接使用状态和 getters,而不需要每次都通过 store 的前缀。
  2. 代码更加简洁和直观。

6.1 src\User.vue #

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>

7.$patch #

pinia 中,$patch 方法允许你一次性地更新 store 中的多个状态。这是一个非常有用的方法,尤其是当你需要在一个操作中更新多个状态时。

以下是如何在 pinia 中使用 $patch 方法的步骤:

  1. 在你的 store 中定义状态。
  2. 在 Vue 组件中调用 $patch 方法来更新状态。

7.1 userStore.ts #

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);
+   }
+ }
});

7.2 User.vue #

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>

8.购物车 #

8.1 App.vue #

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>

8.2 ProductList.vue #

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>

8.3 Cart.vue #

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>

8.4 productsStore.ts #

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();
    }
  }
});

8.5 cartStore.ts #

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);
      }
    }
  }
});

8.6 mock.ts #

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;
};

9.插件 #

9.1 main.ts #

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');

9.2 cartStore.ts #

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
+     }
+   ]
+ },
});

9.3 Cart.vue #

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>