官网:https://pinia.vuejs.org/
牛逼教程:https://www.bilibili.com/video/BV11Y411b7nb

一、环境初始化

1.创建项目

  1. $ yarn create vite

然后跟着提示一步步走,使用ts

2.安装pinia

  1. $ yarn add pinia

二、基本使用

1.创建pinia示例并挂载

  1. import { createApp } from 'vue'
  2. import App from './App.vue'
  3. import { createPinia } from 'pinia'
  4. // 创建pinia实例
  5. const pinia = createPinia()
  6. // 挂载到根实例上
  7. createApp(App).use(pinia).mount('#app')

2.基本使用

  1. import { defineStore } from "pinia"
  2. // 1.定义并导出容器
  3. // 参数1:容器的ID,必须唯一,将来Pinia会把所有的容器挂载到根容器,每个容器的名字就是这里的ID
  4. export const useMainStore = defineStore('main', {
  5. /**
  6. * 类似与组件的data, 用来存储全局状态
  7. * 1.必须是函数:这样是为了在服务端渲染的时候避免交叉请求导致的数据状态污染(客户端其实无所谓)
  8. * 2.必须是箭头函数:为了更好的ts类型推导
  9. * 返回值:一个函数,调用该函数即可得到容器实例
  10. */
  11. state: () => {
  12. return {
  13. count: 100,
  14. foo: 'bar',
  15. arr: [1, 2, 3]
  16. }
  17. },
  18. /**
  19. * 类似于组件的computed,用来封装计算属性,有【缓存】功能
  20. */
  21. getters: {
  22. // 每个函数接受一个可选参数:state状态对象
  23. // count10(state) {
  24. // console.log('count10()调用了');// 具有缓存功能
  25. // return state.count + 10
  26. // }
  27. // (不建议)如果不使用state而使用this,此时就不能对返回值类型做自动推导了,必须手动指定
  28. count10(): number {
  29. return this.count + 10
  30. }
  31. },
  32. /**
  33. * 完全类比于Vue2组件中的methods(可以直接用this),用来【封装业务逻辑】,修改state
  34. */
  35. actions: {
  36. /**
  37. * 注意!!不能使用箭头函数定义actions!!一定要用普通函数!!!
  38. * why?因为箭头函数绑定了外部this
  39. */
  40. changeState(num: number) {
  41. // 可以直接使用this,像极了Vue2
  42. // this.count++
  43. // this.foo = 'hello'
  44. // this.arr.push(4)
  45. // 对于批量修改,建议使用patch做优化
  46. this.$patch((state) => {
  47. state.count += num
  48. state.foo = 'hello'
  49. state.arr.push(4)
  50. })
  51. }
  52. }
  53. })
  54. // 2.容器中的state
  55. // 3.修改state
  56. // 4.actions的使用

打开App.vue,砍掉没用的,我们直接使用项目中HelloWorld.vue组件

  1. <script setup lang="ts">
  2. import HelloWorld from './components/HelloWorld.vue'
  3. </script>
  4. <template>
  5. <HelloWorld />
  6. </template>
  7. <style>
  8. #app {
  9. font-family: Avenir, Helvetica, Arial, sans-serif;
  10. -webkit-font-smoothing: antialiased;
  11. -moz-osx-font-smoothing: grayscale;
  12. text-align: center;
  13. color: #2c3e50;
  14. margin-top: 60px;
  15. }
  16. </style>

下面是HelloWorld.vue的内容

  1. <template>
  2. <p>{{ mainStore.count }}</p>
  3. <p>{{ mainStore.foo }}</p>
  4. <p>{{ mainStore.arr }}</p>
  5. <p>{{ mainStore.count10 }}</p>
  6. <p>{{ mainStore.count10 }}</p>
  7. <p>{{ mainStore.count10 }}</p>
  8. <hr />
  9. <p>{{ count }}</p>
  10. <p>{{ foo }}</p>
  11. <p>
  12. <button @click="handleChangeState">修改数据</button>
  13. </p>
  14. </template>
  15. <script lang="ts" setup>
  16. import { storeToRefs } from 'pinia'
  17. import { useMainStore } from '../store'
  18. // 【哪里使用写哪里】,此时要在HelloWorld组件中用,那就写这里。这很Composition API
  19. const mainStore = useMainStore()
  20. // 禁止!这样会丧失响应性,因为pinia在底层将state用reactive做了处理
  21. // const { count, foo } = mainStore
  22. // 解决方案:将结构出的数据做ref响应式代理
  23. const { count, foo } = storeToRefs(mainStore)
  24. const handleChangeState = () => {
  25. // ==============数据修改的几种方式=============
  26. // 方式一:直接修改
  27. // mainStore.count++
  28. // 方式二:使用 $patch(对象) 批量修改,建议使用,底层做了性能优化
  29. // mainStore.$patch({
  30. // count: mainStore.count + 1,
  31. // foo: 'hello',
  32. // arr: [...mainStore.arr, 4] // 这就不优雅了,所以有了方式三
  33. // })
  34. // 方式三:使用 $patch(回调函数),这个更优雅,墙裂推荐!!!
  35. // 回调函数中的state参数,就是Store定义时里面的state!
  36. // mainStore.$patch((state) => {
  37. // state.count++
  38. // state.foo = 'hello'
  39. // state.arr.push(4)
  40. // })
  41. // 方式四:逻辑较为复杂时,应封装到Store的actions中,并对外暴露接口
  42. mainStore.changeState(10)
  43. }
  44. </script>

从以上几种修改store数据的方式,可以看出pinia的使用非常的简便+灵活,也非常的Composition API。推荐使用后两种方式。

三、购物车案例

1.准备工作

需求说明

  • 商品列表
    • 展示商品列表
    • 添加到购物车
  • 购物车
    • 展示购物车商品列表
    • 展示总价格
    • 订单结算
    • 展示结算状态

      页面模板

      ```vue

  1. ```vue
  2. <template>
  3. <ul>
  4. <li>
  5. 商品名称 - 商品价格
  6. <button>添加到购物车</button>
  7. </li>
  8. <li>
  9. 商品名称 - 商品价格
  10. <button>添加到购物车</button>
  11. </li>
  12. <li>
  13. 商品名称 - 商品价格
  14. <button>添加到购物车</button>
  15. </li>
  16. </ul>
  17. </template>
  18. <script lang="ts" setup>
  19. </script>
  1. <template>
  2. <div class="cart">
  3. <h2>我的购物车</h2>
  4. <p>
  5. <i>请添加一些商品到购物车</i>
  6. </p>
  7. <ul>
  8. <li>商品名称 - 商品价格 × 商品数量</li>
  9. <li>商品名称 - 商品价格 × 商品数量</li>
  10. <li>商品名称 - 商品价格 × 商品数量</li>
  11. </ul>
  12. <p>商品总价</p>
  13. <p>
  14. <button>结算</button>
  15. </p>
  16. <p>结算成功 / 失败.</p>
  17. </div>
  18. </template>
  19. <script lang="ts" setup>
  20. </script>

数据接口

  1. /**
  2. * src/api/shop.ts
  3. * Mocking client-server processing
  4. */
  5. export interface IProduct {
  6. id: number
  7. title: string
  8. price: number
  9. inventory: number // 库存
  10. }
  11. const _products: IProduct[] = [
  12. {id: 1, title: 'iPad 4 Mini', price: 500.01, inventory: 2},
  13. {id: 2, title: 'H&M T-Shirt White', price: 10.99, inventory: 10},
  14. {id: 3, title: 'Charli XCX -Sucker CD', price: 19.99, inventory: 5}
  15. ]
  16. export const getProducts = async () => {
  17. await wait(1000)
  18. return _products
  19. }
  20. export const buyProducts = async () => {
  21. await wait(1000)
  22. return Math.random() > 0.5
  23. }
  24. /**
  25. * 封装了Promise版本的定时器
  26. * @param delay 延迟时间
  27. * @returns Promise
  28. */
  29. async function wait(delay:number) {
  30. return new Promise(resolve => setTimeout(resolve, delay))
  31. }

2.开始开发

定义Store

以下代码学习点:

  • as类型断言
  • 如何在actions中写异步操作 ```typescript import { defineStore } from “pinia” import { getProducts, IProduct } from “../api/shop”

export const useProdunctsStore = defineStore(‘products’, { state: () => { return { all: [] as IProduct[] // 所有商品列表(学习类型断言的使用) } },

getters: {},

actions: { //actions既支持异步操作 async loadAllProducts() { const ret = await getProducts() this.all = ret }, // 也支持同步操作 decrementProduct(product: IProduct) { const ret = this.all.find(item => item.id === product.id) if (ret) { ret.inventory— } } } })

  1. 下面的代码,有以下学习点:
  2. - type类型合并与过滤
  3. - 跨容器通信的极致优雅操作
  4. ```typescript
  5. import { defineStore } from "pinia";
  6. import { buyProducts, IProduct } from "../api/shop";
  7. import { useProdunctsStore } from "./products";
  8. /**
  9. * {id, title, price, quantity}
  10. */
  11. type CartProduct = { // 合并
  12. num: number
  13. } & Omit<IProduct, 'inventory'> // Omit是过滤
  14. export const useCartStore = defineStore('cart', {
  15. state: () => {
  16. return {
  17. cartProducts: [] as CartProduct[],
  18. checkoutStatus: null as null | string
  19. }
  20. },
  21. getters: {
  22. // 总价
  23. totalPrice(state) {
  24. return state.cartProducts.reduce((total, item) => {
  25. return total + item.price * item.num
  26. }, 0)
  27. }
  28. },
  29. actions: {
  30. /**
  31. * 往购物车添加商品
  32. * 这是一个相对复杂的逻辑,与容器中的数据强相关,所以肯定要定义在actions里面!
  33. * @param product 需要添加的商品
  34. */
  35. addProductToCart(product: IProduct) {
  36. // 先看商品有没有库存
  37. if (product.inventory <= 0) {
  38. return
  39. }
  40. // 检查购物车中是否已有该商品
  41. const cartItem = this.cartProducts.find(item => item.id === product.id)
  42. // 如果有则让商品数量+1
  43. if (cartItem) {
  44. cartItem.num++
  45. } else {
  46. // 如果没有则添加到购物车列表中
  47. this.cartProducts.push({
  48. id: product.id,
  49. title: product.title,
  50. price: product.price,
  51. num: 1
  52. })
  53. }
  54. // 跟新商品库存(应该由商品容器暴露API)
  55. const productsStore = useProdunctsStore()
  56. productsStore.decrementProduct(product)
  57. // 跨容器通信!!!!!竟然如此简单!!!
  58. },
  59. /**
  60. * 结算
  61. */
  62. async checkOut() {
  63. const ret = await buyProducts()
  64. this.checkoutStatus = ret ? '成功' : '失败'
  65. if (ret) {
  66. this.cartProducts = [] // 清空购物车
  67. }
  68. }
  69. }
  70. })

重写组件

  1. <template>
  2. <ul>
  3. <li v-for="item in productsStore.all">
  4. {{ item.title }} - {{ item.price }}
  5. <br>
  6. <button :disabled="!item.inventory" @click="cartStore.addProductToCart(item)">添加到购物车</button>
  7. </li>
  8. </ul>
  9. </template>
  10. <script lang="ts" setup>
  11. import { useCartStore } from '../store/cart';
  12. import { useProdunctsStore } from '../store/products'
  13. const productsStore = useProdunctsStore()
  14. const cartStore = useCartStore()
  15. productsStore.loadAllProducts() // 加载所有数据
  16. </script>
  1. <template>
  2. <div class="cart">
  3. <h2>我的购物车</h2>
  4. <p>
  5. <i>请添加一些商品到购物车</i>
  6. </p>
  7. <ul>
  8. <li v-for="item in cartStore.cartProducts">{{ item.title }} - {{ item.price }} × {{ item.num }}</li>
  9. </ul>
  10. <p>商品总价: {{ cartStore.totalPrice }}</p>
  11. <p>
  12. <button @click="cartStore.checkOut">结算</button>
  13. </p>
  14. <p v-show="cartStore.checkoutStatus">结算{{ cartStore.checkoutStatus }}.</p>
  15. </div>
  16. </template>
  17. <script lang="ts" setup>
  18. import { useCartStore } from '../store/cart'
  19. const cartStore = useCartStore()
  20. </script>