pinia实践及分析

# 简介

Pinia.js 是新一代的状态管理器,由 Vue.js团队中成员所开发的,因此也被认为是下一代的 Vuex,即 Vuex5.x,在 Vue3.0 的项目中使用也是备受推崇。

Pinia 是 Vue.js 的轻量级状态管理库,最近很受欢迎。它使用 Vue 3 中的新反应系统来构建一个直观且完全类型化的状态管理库。

Pinia.js 有如下特点:

  • 完整的 typescript 的支持;
  • 足够轻量,压缩后的体积只有1.6kb;
  • 去除 mutations,只有 state,getters,actions;
  • actions 支持同步和异步;
  • 没有模块嵌套,只有 store 的概念,store 之间可以自由使用,更好的代码分割;
  • 无需手动添加 store,store 一旦创建便会自动添加;

# Pinia与Vuex的对比

# 背景

  • Pinia的成功可以归功于其管理存储数据的独特功能(可扩展性、存储模块组织、状态变化分组、多存储创建等)。
  • 另一方面,Vuex也是为Vue框架建立的一个流行的状态管理库,它也是Vue核心团队推荐的状态管理库。 Vuex高度关注应用程序的可扩展性、开发人员的工效和信心。它基于与Redux相同的流量架构。
  • 我们将对Pinia和Vuex进行比较。我们将分析这两个框架的设置、社区优势和性能。我们还将看一下Vuex 5与Pinea 2相比的新变化。

# 设置

# Pinia 设置

Pinia 很容易上手,因为它只需要安装和创建一个store。

要安装 Pinia,您可以在终端中运行以下命令:

yarn add pinia@next
# or with npm
npm install pinia@next
1
2
3

该版本与Vue 3兼容,如果你正在寻找与Vue 2.x兼容的版本,请查看v1分支。

Pinia是一个围绕Vue 3 Composition API的封装器。因此,你不必把它作为一个插件来初始化,除非你需要Vue devtools支持、SSR支持和webpack代码分割的情况:

//app.js
import { createPinia } from 'pinia'
app.use(createPinia())
1
2
3

在上面的片段中,你将Pinia添加到Vue.js项目中,这样你就可以在你的代码中使用Pinia的全局对象。

为了创建一个store,你用一个包含创建一个基本store所需的states、actions和getters的对象来调用 defineStore 方法。

// stores/todo.js
import { defineStore } from 'pinia'
export const useTodoStore = defineStore({
  id: 'todo',
  state: () => ({ count: 0, title: "Cook noodles", done:false })
})
1
2
3
4
5
6

# Vuex 设置

Vuex 也很容易设置,需要安装和创建store。

要安装Vuex,您可以在终端中执行以下命令:

npm install vuex@next --save
# or with yarn
yarn add vuex@next --save
1
2
3

要创建store,你可以使用包含创建基本store所需的states、actions和 getter 的对象调用 createStore 方法:

//store.js
import {createStore} from 'vuex'
const useStore = createStore({
  state: {
    todos: [
      { id: 1, title: '...', done: true }
    ]
  },
  getters: {
    doneTodos (state) {
      return state.todos.filter(todo => todo.done)
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

要访问 Vuex 全局对象,需要在 Vue.js 项目根文件中添加 Vuex,如下所示:

//index.js
import { createApp } from 'vue'
import App from './App.vue'
import {useStore} from './store'
createApp(App).use(store).mount('#app')
1
2
3
4
5

# 使用

# Pinia使用

使用 Pinia,可以按如下方式访问该store:

export default defineComponent({
  setup() {
    const todo = useTodoStore()
    return {
      // 只允许访问特定的state
      state: computed(() => todo.title),
    }
  },
})
1
2
3
4
5
6
7
8
9

请注意,在访问其属性时省略了 store 的 state 对象。

# Vuex使用

使用Vuex,可以按如下方式访问store:

import { computed } from 'vue'
export default {
  setup () {
    const store = useStore()
    return {
      // 访问计算函数中的状态
      count: computed(() => store.state.count),
      // 访问计算函数中的getter
      double: computed(() => store.getters.double)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 社区及评分

# 社区和生态系统的力量

  • 在撰写本文时,Pinia 的社区很小,这导致 Stack Overflow 上的贡献很少,解决方案也很少。由于 Pinia 于去年年初开始流行,并且目前取得了进展,因此其社区正在快速增长。希望很快会有更多的贡献者和解决方案出现在 Pinia 上。
  • Vuex 是 Vue.js 核心团队推荐的状态管理库,拥有庞大的社区,核心团队成员做出了重大贡献。 Stack Overflow 上很容易找到 Vuex 错误的解决方案。

# 学习曲线和文档

  • 这两个状态管理库都相当容易学习,因为它们在 YouTube 和第三方博客上都有很好的文档和学习资源。对于以前有使用 Redux、MobX、Recoil 等 Flux 架构库经验的开发人员来说,他们的学习曲线更容易。
  • 这两个库的文档都很棒,并且以对经验丰富的开发人员和新开发人员都友好的方式编写。

# GitHub 评分

  • 在撰写本文时,Pania 有两个主要版本:v1 和 v2,其中 v2 在 GitHub 上拥有超过 1.6k 星。鉴于它最初于 2019 年发布并且相对较新,它无疑是 Vue.js 生态系统中增长最快的状态管理库之一。
  • 同时,从 Vuex 创建之日到现在,Vuex 库已经发布了五个稳定版本。尽管 v5 处于实验阶段,但 Vuex 的 v4 是迄今为止最稳定的版本,在 GitHub 上拥有大约 26.3k 星。

# 性能

  • Pinia和Vuex都非常快,在某些情况下,使用Pinia的web应用程序会比使用Vuex更快。这种性能的提升可以归因于Pinia的极轻的重量,Pinia体积约1KB。
  • 尽管Pinia是在Vue devtools的支持下建立的,但由于Vue devtools没有暴露出必要的API,所以一些功能如时间旅行和编辑仍然不被支持。当开发速度和调试对你的项目来说更重要时,这是值得注意的。

# 比较 Pinia 2 和 Vuex 4

# 差异

Pinia 将这些与 Vuex 3 和 4 进行了比较:

  • 突变不再存在。他们经常被认为非常冗长。他们最初带来了 devtools 集成,但这不再是问题。
  • 无需创建自定义的复杂包装器来支持 TypeScript,所有内容都是类型化的,并且 API 的设计方式尽可能地利用 TS 类型推断。

这些是Pinia在其状态管理库和Vuex之间的比较中提出的额外见解:

  • Pinia 不支持嵌套存储。相反,它允许你根据需要创建store。但是,store仍然可以通过在另一个store中导入和使用store来隐式嵌套;
  • 存储器在被定义的时候会自动被命名。因此,不需要对模块进行明确的命名;
  • Pinia允许你建立多个store,让你的捆绑器代码自动分割它们;
  • Pinia允许在其他getter中使用getter;
  • Pinia允许使用 $patch 在devtools的时间轴上对修改进行分组。
this.$patch((state) => {
  state.posts.push(post)
  state.user.postsCount++
}).catch(error){
  this.errors.push(error)
}
1
2
3
4
5
6

将 Pinia 2(目前处于 alpha 阶段)与 Vuex 进行比较,我们可以推断出 Pinia 领先于 Vuex 4。

Vue.js核心团队为Vuex 5制定了一个开放的RFC,类似于Pinia使用的RFC。目前,Vuex通过RFC来尽可能多地收集社区的反馈。希望Vuex 5的稳定版本能够超越Pinea 2。

据同时也是 Vue.js 核心团队成员并积极参与 Vuex 设计的 Pinia 的创建者(Eduardo San Martin Morote)所说,Pania 和 Vuex 的相似之处多于不同之处:

Pinia试图尽可能地接近Vuex的理念。它的设计是为了测试Vuex的下一次迭代的建议,它是成功的,因为我们目前有一个开放的RFC,用于Vuex 5,其API与Pinea使用的非常相似。我对这个项目的个人意图是重新设计使用全局Store的体验,同时保持Vue的平易近人的理念。我保持Pinea的API与Vuex一样接近,因为它不断向前发展,使人们很容易迁移到Vuex,甚至在未来融合两个项目(在Vuex下)。

尽管 Pinia 足以取代 Vuex,但取代 Vuex 并不是它的目标,因此 Vuex 仍然是 Vue.js 应用程序的推荐状态管理库。

Pinia API 与 Vuex ≤4 有很大不同,:

  • 没有mutations。mutations被认为是非常冗长的。最初带来了 devtools 集成,但这不再是问题。
  • 不再有模块的嵌套结构。您仍然可以通过在另一个store中导入和使用store来隐式嵌套store,但 Pinia 通过设计提供扁平结构,同时仍然支持store之间的交叉组合方式。您甚至可以拥有store的循环依赖关系。
  • 更好typescript支持。无需创建自定义的复杂包装器来支持 TypeScript,所有内容都是类型化的,并且 API 的设计方式尽可能地利用 TS 类型推断。
  • 不再需要注入、导入函数、调用它们,享受自动补全!
  • 无需动态添加stores,默认情况下它们都是动态的,您甚至不会注意到。请注意,您仍然可以随时手动使用store来注册它,但因为它是自动的,所以您无需担心。
  • 没有命名空间模块。鉴于store的扁平架构,“命名空间”store是其定义方式所固有的,您可以说所有stores都是命名空间的。
  • Pinia 大小约1kb;

# Vuex 和 Pinia 的优缺点

Vuex的优点

  • 支持调试功能,如时间旅行和编辑;
  • 适用于大型、高复杂度的Vue.js项目;

Vuex的缺点

  • 从 Vue 3 开始,getter 的结果不会像计算属性那样缓存
  • Vuex 4有一些与类型安全相关的问题;

Pinia的优点

  • 完整的 TypeScript 支持:与在 Vuex 中添加 TypeScript 相比,添加 TypeScript 更容易
  • 极其轻巧(体积约 1KB)
  • store 的 action 被调度为常规的函数调用,而不是使用 dispatch 方法或 MapAction 辅助函数,这在 Vuex 中很常见;
  • 支持多个Store;
  • 支持 Vue devtools、SSR 和 webpack 代码拆分;

Pinia的缺点

  • 不支持时间旅行和编辑等调试功能

# 何时使用Pinia,何时使用Vuex

根据我的个人经验,由于Pinea是轻量级的,体积很小,它适合于中小型应用。它也适用于低复杂度的Vue.js项目,因为一些调试功能,如时间旅行和编辑仍然不被支持。

将 Vuex 用于中小型 Vue.js 项目是过度的,因为它重量级的,对性能降低有很大影响。因此,Vuex 适用于大规模、高复杂度的 Vue.js 项目。

# 安装及store配置

# 安装

npm install pinia --save

# store配置

新建 src/store 目录并在其下面创建 index.ts,导出 store

// src/store/index.ts
import { createPinia } from 'pinia'
const store = createPinia()
export default store
1
2
3
4

在 main.ts 中引入并使用。

// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
const app = createApp(App)
app.use(store)
1
2
3
4
5
6

# State

# 定义State

在 src/store 下面创建一个user.ts

//src/store/user.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore("user",{
  // id: 'user', // 此处也可定义id,  id必填,且需要唯一
  state: () => {
    return {
      name: '张三'
    }
  }
})
1
2
3
4
5
6
7
8
9
10

# 获取 state

<template>
  <div>{{ userStore.name }}</div>
</template>

<script lang="ts" setup>
import { useUserStore } from '@/store/user'

const userStore = useUserStore()
</script>
1
2
3
4
5
6
7
8
9

也可以结合 computed 获取。

const name = computed(() => userStore.name)
1

state 也可以使用解构,但使用解构会使其失去响应式,这时候可以用 pinia 的 storeToRefs

import { storeToRefs } from 'pinia'
const { name } = storeToRefs(userStore)
1
2

# 修改 state

方式一:最简单的方式;

但一般不建议这么做,建议通过 actions 去修改 state,action 里可以直接通过 this 访问。

mainStore.count++;
mainStore.foo = "samy"
1
2

方式二:如果要修改多个数据,建议$patch批量更新

mainStore.$patch({
	count: mainStore.count + 1,
	foo: "samy",
	arr: [...mainStore.arr, 4]
})
1
2
3
4
5

方式三:更好的批量更新的方式 $patch一个函数

mainStore.$patch(state => {
	state.count++
	state.foo = "samy"
	state.arr.push(4)
})
1
2
3
4
5

方式四: 逻辑比较多可以封装到actions处理

mainStore.changeState(10)

import { defineStore } from 'pinia'
export const useMainStore = defineStore('user'{
  state: () => {
    return {
      count: 100,
      foo: "bar",
      arr: [1,2,3]
    }
  },
  // 类似组件的 mthods, 封装业务逻辑,修改state
  actions: {
      // 不要使用箭头函数修改action,会导致this指向丢失,因为箭头函数绑定的是外部this
      changeState(num: number) {
          // 通过this可以访问state里面的数据进行修改
          this.count += num
          this.foo = "samy"
          this.arrr.push(4)
          // 同样也可以使用 this.$patch({}) 或 this.$patch(state => {})
      }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script lang="ts" setup>
import { useUserStore } from '@/store/user'

const userStore = useUserStore()
userStore.updateName('李四')
</script>
1
2
3
4
5
6

# Getters

export const useUserStore = defineStore({
 id: 'user',
 state: () => {
   return {
     name: '张三'
   }
 },
 getters: {
   fullName: (state) => {
     return state.name + '丰'
   }
 }
})
userStore.fullName // 张三丰
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { defineStore } from 'pinia'
import { otherState } from "@/store/otherState.js";

export const useMainStore = defineStore('main'{
  state: () => {
    return {
      count: 100,
      foo: "bar",
      arr: [1,2,3]
    }
  },
  // 类似组件的 computed, 用来封装计算属性,有缓存的功能
  gettters: {
      // 函数接受一个可选参数 state 状态对象
      countPlus10(state) {
          console.log('countPlus调用了')
          return state.count + 10
      }
      // 如果getters 中使用了this不接受state参数,则必须手动指定返回值的类型,否则无法推导出来
       countPlus20(): number{
          return this.count + 10
      }
      
       // 获取其它 Getter, 直接通过 this
      countOtherPlus() {
          return this.countPlus20;
      }

      // 使用其它 Store
      otherStoreCount(state) {
          // 这里是其他的 Store,调用获取 Store,就和在 setup 中一样
          const otherStore = useOtherStore();
          return otherStore.count;
      },
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

组件使用

mainStore.countPlus10
1

# Actions

action 支持 async/await 的语法,轻松应付异步处理的场景。

# 异步 action

action 可以像写一个简单的函数一样支持 async/await 的语法,让你愉快的应付异步处理的场景。

export const useUserStore = defineStore({
  id: 'user',
  actions: {
    async login(account, pwd) {
      const { data } = await api.login(account, pwd)
      return data
    }
  }
})
1
2
3
4
5
6
7
8
9

# action 间相互调用

action 间的相互调用,直接用 this 访问即可。

 export const useUserStore = defineStore({
  id: 'user',
  actions: {
    async login(account, pwd) {
      const { data } = await api.login(account, pwd)
      this.setData(data) // 调用另一个 action 的方法
      return data
    },
    setData(data) {
      console.log(data)
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13

在 action 里调用其他 store 里的 action 也比较简单,引入对应的 store 后即可访问其内部的方法了。

// src/store/user.ts
import { useAppStore } from './app'
export const useUserStore = defineStore({
  id: 'user',
  actions: {
    async login(account, pwd) {
      const { data } = await api.login(account, pwd)
      const appStore = useAppStore()
      appStore.setData(data) // 调用 app store 里的 action 方法
      return data
    }
  }
})
// src/store/app.ts
export const useAppStore = defineStore({
  id: 'app',
  actions: {
    setData(data) {
      console.log(data)
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 数据持久化

插件 pinia-plugin-persist 可以辅助实现数据持久化功能。

# 安装

npm i pinia-plugin-persist --save
1

# 使用

// src/store/index.ts
import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'

const store = createPinia()
store.use(piniaPluginPersist)

export default store
1
2
3
4
5
6
7
8

接着在对应的 store 里开启 persist 即可。

export const useUserStore = defineStore('user', {
 // 开启数据缓存,数据默认存在 sessionStorage 里,并且会以 store 的 id 作为 key。
  persist: {
    enabled: true
  }
  state: () => {
    return {
      name: '张三'
    }
  },
})
1
2
3
4
5
6
7
8
9
10
11

image-20220213161438587

# 自定义 key

你也可以在 strategies 里自定义 key 值,并将存放位置由 sessionStorage 改为 localStorage。

persist: {
  enabled: true,
  strategies: [
    {
      key: 'my_user',
      storage: localStorage,
    }
  ]
}
1
2
3
4
5
6
7
8
9

image-20220213161526732

# 持久化部分 state

默认所有 state 都会进行缓存,你可以通过 paths 指定要持久化的字段,其他的则不会进行持久化。

state: () => {
  return {
    name: '张三',
    age: 18,
    gender: '男'
  }  
},

persist: {
  enabled: true,
  strategies: [
    {
      // 只持久存储name和age到localStorage
      storage: localStorage,
      paths: ['name', 'age']
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

上面我们只持久化 name 和 age,并将其改为localStorage, 而 gender 不会被持久化,如果其状态发送更改,页面刷新时将会丢失,重新回到初始状态,而 name 和 age 则不会。

# 新版本完整示范

# 示范

// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => {
    return { count: 0 }
  },
  // could also be defined as
  // state: () => ({ count: 0 })
  actions: {
    increment() {
      this.count++
    },
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在组件中使用它:

import { useCounterStore } from '@/stores/counter'

export default {
  setup() {
    const counter = useCounterStore()
    counter.count++
    // with autocompletion ✨
    counter.$patch({ count: counter.count + 1 })
    // or using an action instead
    counter.increment()
  },
}
1
2
3
4
5
6
7
8
9
10
11
12

您甚至可以使用函数(类似于组件setup())为更高级的用例定义 Store:

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }
  return { count, increment }
})
1
2
3
4
5
6
7

如果不熟悉setup()Composition API,别担心,Pania 也支持类似 Vuex的 map helpers (opens new window)。您以相同的方式定义stores,使用mapStores(), mapState(), 或mapActions()

const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    }
  }
})

const useUserStore = defineStore('user', {
  // ...
})

export default {
  computed: {
    // other computed properties
    // ...
    // gives access to this.counterStore and this.userStore
    ...mapStores(useCounterStore, useUserStore)
    // gives read access to this.count and this.double
    ...mapState(useCounterStore, ['count', 'double']),
  },
  methods: {
    // gives access to this.increment()
    ...mapActions(useCounterStore, ['increment']),
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 几个主要点

Pinia有多种对状态的修改方式

  • 使用actions,直接是函数形式;actions:类似组件的methods,用来封装业务逻辑,同步异步都可以;

    VueX需要同步使用 mutations,异步操作使用actions;注意:Pinia中没有mutations

  • 在使用Pinia的过程中可以发现自动补全是相当优秀;

  • 直接在状态上修改;

    const countPlus_1 = useCounter.count++
    
    1
  • 使用store的$patch函数,支持选项和回调函数两种写法,回调函数适用于状态为数组或其他之类的需要调用状态方法进行修改

    const countPlus_2 = useCounter.$patch({ count: useCounter.count + 1 })
    
    1
    const countPlus_3 = useCounter.$patch((state) => state.count++)
    
    1
  • 对状态的结构需要使用StoreToRefs函数

    const { count } = storeToRefs(useCounter)
    
    1

# Pinia实战案例

# 1.需求说明

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

# 2.创建启动项目

npm init vite@latest

Need to install the following packages:
	create-vite@1atest
ok to proceed? (y)
√ Project name: ... shopping-cart
√ select a framework: > vue
√ select a variant: > vue-ts

scaffo1ding project in c:\Users\yun\Projects\pinia-examp1es\shopping-cart. . .
Done. Now run:

    cd shopping-cart
    npm insta11
    npm run dev
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3.页面模板

<!-- src/App.vue -->
<template>
  <div>
    <h1>Pinia - 购物车示例</h1>
    <hr />
    <h2>商品列表</h2>
    <ProductList />
    <hr />
    <ShoppingCart />
  </div>
</template>

<script setup lang="ts">
import ProductList from "./components/ProductList.vue";
import ShoppingCart from "./components/ShoppingCart.vue";
</script>

<style lang="scss" scoped></style>

<!-- src/ProductList.vue -->
<template>
  <ul>
    <li>商品名称 - 商品价格<br /><button>添加到购物车</button></li>
    <li>商品名称 - 商品价格<br /><button>添加到购物车</button></li>
    <li>商品名称 - 商品价格<br /><button>添加到购物车</button></li>
  </ul>
</template>

<script setup lang="ts"></script>

<style lang="scss" scoped></style>

<!-- src/ShoppingCart.vue -->
<template>
  <div class="cart">
    <h2>你的购物车</h2>
    <p><i>请添加一些商品到购物车</i></p>
    <ul>
      <li>商品名称 - 商品价格 × 商品数量</li>
      <li>商品名称 - 商品价格 × 商品数量</li>
      <li>商品名称 - 商品价格 × 商品数量</li>
    </ul>
    <p>商品总价:xxx</p>
    <p><button>结算</button></p>
    <p>结算成功 / 失败</p>
  </div>
</template>

<script setup lang="ts"></script>

<style lang="scss" scoped></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

# 4.数据接口

// src/api/shop.ts
export interface IProduct {
  id: number;
  title: string;
  price: number;
  inventory: number;
}

const _products: IProduct[] = [
  { id: 1, title: "苹果12", price: 600, inventory: 3 },
  { id: 2, title: "小米13", price: 300, inventory: 5 },
  { id: 3, title: "魅族12", price: 200, inventory: 6 },
];

// 获取商品列表
export const getProducts = async () => {
  await wait(100);
  return _products;
};

// 结算商品
export const buyProducts = async () => {
  await wait(100);
  return Math.random() > 0.5;
};

async function wait(delay: number) {
  return new Promise((resolve) => setTimeout(resolve, delay));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 5.展示商品列表

// src/store/products.ts
import { defineStore } from "pinia";
import { getProducts, IProduct } from "../api/shop";
export const useProductsStore = defineStore("products", {
  state: () => {
    return {
      all: [] as IProduct[], // 所有商品列表
    };
  },
  getters: {},
  actions: {
    async loadAllProducts() {
      const result = await getProducts();
      this.all = result;
    },
  },
});

<!-- ProductList.vue -->
<template>
  <ul>
     <li v-for="item in productsStore.all" :key="item.id">
      {{ item.title }} - {{ item.price }}- 库存{{ item.inventory }}<br />
       <button>添加到购物车</button>
    </li>
  </ul>
</template>

<script setup lang="ts">
import { useProductsStore } from "../store/products";
const productsStore = useProductsStore();

// 加载所有数据
productsStore.loadAllProducts();
</script>

<style lang="scss" scoped></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 6.添加到购物车

// src/store/cart.ts
import { defineStore } from "pinia";
import { IProduct, buyProducts } from "../api/shop";
import { useProductsStore } from "./products";

// 添加quantity类型并且合并IProduct除了inventory,最终数据 {id, title, price, quantity}
type CartProduct = {
  quantity: number;
} & Omit<IProduct, "inventory">;

export const useCartStore = defineStore("cart", {
  state: () => {
    return {
      cartProducts: [] as CartProduct[], // 购物车列表
    };
  },
  getters: {},
  actions: {
    addProductToCart(product: IProduct) {
      console.log("addProductToCart", product);
      // 检查商品是否有库存
      if (product.inventory < 1) {
        return;
      }
      // 检查购物车是否已有该商品
      const cartItem = this.cartProducts.find((item) => item.id === product.id);

      if (cartItem) {
        // 如果有则商品数量 + 1
        cartItem.quantity++;
      } else {
        // 如果没有则添加到购物车列表
        this.cartProducts.push({
          id: product.id,
          title: product.title,
          price: product.price,
          quantity: 1, // 第一次添加到购物车数量就是 1
        });
      }
      // 更新商品库存 引入另一个store
      // product.inventory--; 不建议这么做,不要相信函数参数,建议找到源数据修改
      const productsStore = useProductsStore();
      productsStore.decrementProduct(product);
    },
  },
});

// src/store/products.ts
actions: {
    async loadAllProducts() {
      const result = await getProducts();
      this.all = result;
    },
    // 减少库存
    decrementProduct(product: IProduct) {
      const result = this.all.find((item) => item.id === product.id);
      if (result) {
        result.inventory--;
      }
    },
  },

<!-- ProductList.vue -->
<template>
  <ul>
    <li v-for="item in productsStore.all" :key="item.id">
      {{ item.title }} - {{ item.price }}- 库存{{ item.inventory }}<br />
      <button @click="cartStore.addProductToCart(item)" :disabled="!item.inventory">添加到购物车</button>
    </li>
  </ul>
</template>

<script setup lang="ts">
import { useProductsStore } from "../store/products";
import { useCartStore } from "../store/cart";

const productsStore = useProductsStore();
const cartStore = useCartStore();

// 加载所有数据
productsStore.loadAllProducts();
</script>

<style lang="scss" scoped></style>

<!-- ShoppingCart.vue -->
<template>
  <div class="cart">
    <h2>你的购物车</h2>
    <p><i>请添加一些商品到购物车</i></p>
    <ul>
      <li v-for="item in cartStore.cartProducts" :key="item.id">
        {{ item.title }} - {{ item.price }}¥ × 数量{{ item.quantity }}
      </li>
    </ul>
    <p>商品总价:xxx</p>
    <p><button>结算</button></p>
    <p>结算成功 / 失败</p>
  </div>
</template>

<script setup lang="ts">
import { useCartStore } from "../store/cart";
const cartStore = useCartStore();
</script>

<style lang="scss" scoped></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107

# 7.展示购物车总价

// src/store/cart.ts
getters: {
    // 总价
    totalPrice(state) {
      return state.cartProducts.reduce((total, item) => {
        return total + item.price * item.quantity;
      }, 0);
    },
  },

<!-- ShoppingCart.vue -->
<p>商品总价:{{ cartStore.totalPrice }}</p>
1
2
3
4
5
6
7
8
9
10
11
12

# 8.购物车案例完成

// src/store/cart.ts
import { defineStore } from "pinia";
import { IProduct, buyProducts } from "../api/shop";
import { useProductsStore } from "./products";

// 添加quantity类型并且合并IProduct除了inventory,最终数据 {id, title, price, quantity}
type CartProduct = {
  quantity: number;
} & Omit<IProduct, "inventory">;

export const useCartStore = defineStore("cart", {
  state: () => {
    return {
      cartProducts: [] as CartProduct[], // 购物车列表
      checkutStatus: null as null | string, // 结算状态
    };
  },
  getters: {
    // 总价
    totalPrice(state) {
      return state.cartProducts.reduce((total, item) => {
        return total + item.price * item.quantity;
      }, 0);
    },
  },
  actions: {
    addProductToCart(product: IProduct) {
      console.log("addProductToCart", product);
      // 检查商品是否有库存
      if (product.inventory < 1) {
        return;
      }
      // 检查购物车是否已有该商品
      const cartItem = this.cartProducts.find((item) => item.id === product.id);

      if (cartItem) {
        // 如果有则商品数量 + 1
        cartItem.quantity++;
      } else {
        // 如果没有则添加到购物车列表
        this.cartProducts.push({
          id: product.id,
          title: product.title,
          price: product.price,
          quantity: 1, // 第一次添加到购物车数量就是 1
        });
      }
      // 更新商品库存 引入另一个store
      //   product.inventory--;
      const productsStore = useProductsStore();
      productsStore.decrementProduct(product);
    },
    async checkout() {
      const result = await buyProducts();
      this.checkutStatus = result ? "成功" : "失败";
	  // 清空购物车
      if (result) {
        this.cartProducts = [];
      }
    },
  },
});

<!-- ShoppingCart -->
<p><button @click="cartStore.checkout">结算</button></p>
<p v-show="cartStore.checkutStatus">结算{{ cartStore.checkutStatus }}</p>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

# 参考链接

上次更新: 2022/04/15, 05:41:28
×