从0到Vue3企业项目实战【04.从vue组件通讯到eventBus以及vuex(附mock接口与axios简单实践)】

2,095 阅读15分钟

这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

1.0 怎么创建组件?

1.1 为什么要用组件?

当我们碰见重复的标签的时候,通常我们是怎么写的呢,是时候拿出我的cv大法了?对对对这样是可以解决问题,但是并不优雅,有如下几个问题

  • 节点混乱,代码结构不清晰
  • 样式不隔离,代码耦合严重
  • js逻辑代码不抽离,功能不够内聚,复用性不佳
  • 复制粘贴好麻烦,我是懒癌患者
  • 后期代码维护好麻烦,还得到处寻找复制的代码 知道了上面的问题,那就赶快用起来组件吧,作为一个成熟的前端开发,不能老写重复代码不是,这就拿起组件,翻身做代码主

1.2 如何创建组件

  • 创建文件夹 我们需要在src目录下的component文件夹创建一个组件名.vue的文件夹(这里不要使用中文命名) image.png

image.png

  • 写入可复用的样式,结构,逻辑 button.vue组件
<template>
  <button class="btn hvr-push">测试</button>
</template>

<script>
export default {

}
</script>

<style scoped>
.btn {
  color: #fff;
  background-color: #409eff;
  padding: 0.625rem 1.25rem;
  font-size: 1.5rem;
  border-radius: 0.625rem;
  border: 1px solid #409eff;
  box-shadow: 0 8px 4px 4px #409eff;
}

@-webkit-keyframes hvr-push {
  50% {
    -webkit-transform: scale(0.8);
    transform: scale(0.8);
  }

  100% {
    -webkit-transform: scale(1);
    transform: scale(1);
  }
}

@keyframes hvr-push {
  50% {
    -webkit-transform: scale(0.8);
    transform: scale(0.8);
  }

  100% {
    -webkit-transform: scale(1);
    transform: scale(1);
  }
}

.hvr-push {
  vertical-align: middle;
  -webkit-transform: perspective(1px) translateZ(0);
  transform: perspective(1px) translateZ(0);
  box-shadow: 0 0 1px rgba(0, 0, 0, 0);
}

.hvr-push:active {
  -webkit-animation-name: hvr-push;
  animation-name: hvr-push;
  -webkit-animation-duration: 0.3s;
  animation-duration: 0.3s;
  -webkit-animation-timing-function: linear;
  animation-timing-function: linear;
  -webkit-animation-iteration-count: 1;
  animation-iteration-count: 1;
}
</style>
  • 在我们要使用的页面引入组件
    • 全局注册引入
    • 局部注册引入 main.js全局注册

// import 组件对象 from '文件路径'

// Vue.component("组件名", 组件对象)

import Button from './components/button.vue'

Vue.component("Button", Button)

app.vue引入

<template>
  <div>
    <Button></Button>
  </div>
</template>

<script>
export default {
 
}
</script>

<style>
</style>

app.vue局部注册引入

<template>
  <div>
    <Button></Button>
  </div>
</template>

<script>
// import 组件对象 from '文件路径'
import Button from './components/button.vue'
export default {
 // components: {
  //   组件名: 组件对象
  // },
  components: {
    Button: Button
  },
}
</script>

<style>
</style>

我们可以看到,注册组件和注册一个过滤器是很类似的,如果是全局注册那么都要挂载在Vue这个大对象上,我们通常的组件会使用局部注册的方式,全局注册一般考虑例如dialog,button等常用的组件才会选择全局注册,因为局部注册的性能优于全局组件

2.0 如何进行组件间的通信

我们已经学会了如何创建以及使用组件,但是单独会这些并不能让组件复用稍微复杂化一些场景,比如我们现在有一个需求:制作仿美团的一个电影的卡片组件,里面要展示电影的宣传图,电影名称,评分,以及按钮的一个卡片,每个卡片中的数据都不一样,这个时候光会上面的就不行了,还得会怎么实现组件通信

image.png

2.1父传子通信

顾名思义,父传子就是由父组件将特定的数据传递到子组件来进行展示或者进行逻辑编译
怎么区分谁是父组件谁是子组件?
父组件引入并使用了子组件,谁被引入谁就是子组件
一个父组件可以引入多个子组件
换而言之:一个爸爸可以有多个孩子,爸爸生了孩子,孩子就是爸爸的儿子(当然子组件也可以在多个页面被引用,能够分辨出关系即可)\


接下来我们实现一下上面案例,通过实践了解如何传递

2.1.1子组件的怎么写?

  • 定义props数组规定接收父组件传递哪些字段
  • 将字段放在需要展示或者使用的地方 子组件movie.vue
<template>
  <div class="box">
    <img :src="src" class="myimg" />
    <div class="info">
      <p class="score">
        <b>
          观众评
          <span class="scoreItem">{{score}}</span>
        </b>
      </p>
      <div class="name">
        <span class="title">{{title}}</span>
        <span class="btn">购票</span>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: ['score', 'title', 'src']
}
</script>

<style  scoped>
.box {
  width: 214px;
  height: 296px;
  display: inline-block;
  margin: 0 10px;
  position: relative;
}
.myimg {
  width: 100%;
  height: 100%;
  border-radius: 4px;
}

.info {
  width: 214px;
  height: 100px;
  position: absolute;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  color: #fff;
  font-size: 18px;
  padding: 10px;
  bottom: 0;
  box-sizing: border-box;
}

.score {
  font-size: 12px;
  margin-bottom: 2px;
}

.scoreItem {
  color: #fd9832;
  font-size: 16px;
}

.name {
  display: flex;
  justify-content: space-between;
}

.title {
  font-size: 16px;
}

.btn {
  background: #ff4949;
  border-radius: 100px;
  color: #fff;
  padding: 2px 12px 3px 12px;
  font-size: 14px;
  cursor: pointer;
  line-height: 20px;
  text-align: center;
}
</style>>

2.1.2父组件怎么写?

  • 引入并注册
  • 在标签内写上需要传递的值,可以动态的也可以是静态的 父组件app.vue
<template>
  <div>
    <movie
      src="https://p0.pipi.cn/mmdb/25bfd671339c7e8ea33139d0476cb0d92908d.jpg?imageMogr2/thumbnail/2500x2500%3E"
      score="9.5"
      title="人生大事"
    ></movie>
  </div>
</template>

<script>
import movie from './components/movie.vue'
export default {
  components: {
    movie: movie
  },
}
</script>

<style>
</style>

这样就可以实现如图的效果啦

image.png

上面使用的是传递固定值,实际开发中我们一般是使用接口获取到一个数组,我们不可能为每个卡片写一个固定的值,所以上面的代码还可以优化,使用for循环遍历上面的组件,并将动态的值传递给组件

2.1.3如何使用for循环遍历组件?

<template>
  <div>
    <movie v-for="obj in list" :key="obj.id" :src="obj.src" :score="obj.score" :title="obj.title"></movie>
  </div>
</template>

<script>
import movie from './components/movie.vue'
export default {
  data () {
    return {
      list: [{
        id: 1,
        src: 'https://p0.pipi.cn/mmdb/25bfd671339c7e8ea33139d0476cb0d92908d.jpg?imageMogr2/thumbnail/2500x2500%3E',
        title: '人生大事',
        score: '9.5'
      },
      {
        id: 2,
        src: 'https://p0.pipi.cn/mmdb/25bfd6d771f0fa57e267cb131f671a5889b2d.jpg?imageMogr2/thumbnail/2500x2500%3E',
        title: '神探大战',
        score: '9'
      },
      {
        id: 3,
        src: 'https://p0.pipi.cn/mmdb/25bfd671be15bf51baf0ee3a5d06b91bf94c3.jpg?imageMogr2/thumbnail/2500x2500%3E',
        title: '侏罗纪世界3',
        score: '8'
      }
      ]
    }
  },
  components: {
    movie: movie
  },
}
</script>

<style>
</style>

image.png

2.2 何为单向数据流?

从父向子的数据流向,叫单向数据流,我们通过props传递给子组件的值是只读的,当我们在子组件修改不通知父组件,会造成数据不一致性
总结:vue规定props本身是 只读的 不可以进行赋值
所以,当我们需要在自组件进行修改父组件传递过来的值的时候,我们需要在子组件调用父组件的方法来实现修改, 还是用上面的案例,我们把购票按钮改为差评按钮,当用户点击一次差评那么观众评分就降低0.1分,来常识下子向父的传递方法吧~

2.3 子向父_自定义事件

  • 父组件在组件引入处绑定处理方法,并编写好处理方法
<template>
  <div>
    <movie
      v-for="(obj,index) in list"
      :key="obj.id"
      :src="obj.src"
      :score="obj.score"
      :title="obj.title"
      :index="index"
      @cp="cp"
    ></movie>
    <!-- 组件中 写入 @自定义事件名="父组件中的方法名" -->
  </div>
</template>

<script>
import movie from './components/movie.vue'
export default {
  data () {
    return {
      list: [{
        id: 1,
        src: 'https://p0.pipi.cn/mmdb/25bfd671339c7e8ea33139d0476cb0d92908d.jpg?imageMogr2/thumbnail/2500x2500%3E',
        title: '人生大事',
        score: '9.5'
      },
      {
        id: 2,
        src: 'https://p0.pipi.cn/mmdb/25bfd6d771f0fa57e267cb131f671a5889b2d.jpg?imageMogr2/thumbnail/2500x2500%3E',
        title: '神探大战',
        score: '9'
      },
      {
        id: 3,
        src: 'https://p0.pipi.cn/mmdb/25bfd671be15bf51baf0ee3a5d06b91bf94c3.jpg?imageMogr2/thumbnail/2500x2500%3E',
        title: '侏罗纪世界3',
        score: '8'
      }
      ]
    }
  },
  methods: {
    cp (index, pf) {
      this.list[index].score > 0.01 && (this.list[index].score = (this.list[index].score - pf).toFixed(2))
    }
  },
  components: {
    movie: movie
  },
}
</script>

<style>
</style>

此处需要注意:

  1. @cp="cp" 中左边的cp指的是子组件中会调用this.$emit中写的名字,也就是子组件规定的名称,右边的cp指的是父组件中处理函数的名称也可以定义为其他的,就是methods中也要同步改名
  2. 子组件可以传递任意参数到父组件的处理方法中,这里是让子组件传递了一个索引减少的评分数两个参数,并通过两个参数对数组进行修改,实现差评的效果,记住:不要给右侧cp后面加()传参,这样会覆盖this.$emit传递过来的值
  3. 处理方法中使用了&&有短路的效果,我们知道&&是当两个条件都为真的时候才为真,所以为了节省性能,js会判断第一个条件是否为真,如果为真,那么才会判断执行后续代码,如果不为真就不会执行,这里起到了一个当评分不高于0.01的时候就不执行后续方法的一个效果
  4. 当js进行减法的时候可能会出现失去精度的问题,所有我们在后面加入一个toFixed实现保存后面两位小数,防止这种bug出现
  • 子组件中绑定好处理方法,并在该方法中使用this.$emit()调用父组件的处理函数
<template>
  <div class="box">
    <img :src="src" class="myimg" />
    <div class="info">
      <p class="score">
        <b>
          观众评
          <span class="scoreItem">{{score}}</span>
        </b>
      </p>
      <div class="name">
        <span class="title">{{title}}</span>
        <span class="btn" @click="wycp">差评</span>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: ['score', 'title', 'src', 'index'],
  methods: {
    wycp () {
      this.$emit('cp', this.index, 0.01)
    }
  }
}
</script>

<style  scoped>
.box {
  width: 214px;
  height: 296px;
  display: inline-block;
  margin: 0 10px;
  position: relative;
}
.myimg {
  width: 100%;
  height: 100%;
  border-radius: 4px;
}

.info {
  width: 214px;
  height: 100px;
  position: absolute;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  color: #fff;
  font-size: 18px;
  padding: 10px;
  bottom: 0;
  box-sizing: border-box;
}

.score {
  font-size: 12px;
  margin-bottom: 2px;
}

.scoreItem {
  color: #fd9832;
  font-size: 16px;
}

.name {
  display: flex;
  justify-content: space-between;
}

.title {
  font-size: 16px;
}

.btn {
  background: #ff4949;
  border-radius: 100px;
  color: #fff;
  padding: 2px 12px 3px 12px;
  font-size: 14px;
  cursor: pointer;
  line-height: 20px;
  text-align: center;
}
</style>>

3.0 步入状态管理殿堂

需要注意的是:

  1. 在需要的标签中绑定好动作,这个时候向取什么名字都一样没有影响
  2. 在wycp()方法中调用this.$emit()方法,第一个参数为,父组件需要@绑定的方法名称,后面两个参数会根据顺序传 递给父组件的处理方法

3.1 什么是状态

前端不可避免的会在页面中展示很多数据,我的理解是跟前端渲染相关的变量,我们统称为状态,状态的改变会影响渲染,也就是model -> view,主流得框架都会给你一个放置状态的函数,并对其进行处理,如vue的data中返回的对象通常会在其中存储状态,在data中的状态会对其进行订阅,当状态发生更新的时候,就会同步更新渲染等,是出于程序性能优化角度,通常是不建议在data放置与渲染无关的变量的

4.0 eventBus(事件总线)浅尝

4.1 eventBus(事件总线)与vuex(状态管理)初识

上面我们实现了双向数据流,也就是子组件的事件传递给父组件处理,从而实现状态更新,而当我们拥有两个组件,两个同级组件间需要通讯,那么又如何实现呢?按照上面的思路,我们可以将a组件的数据传给父组件,再由父组件将数据传递给b组件,这样确实可以实现组件间的通讯,父组件相当于一个代理,但是这还不够优雅,有以下几点可以优化一下

  • 两个通讯的子组件必须拥有同一个父组件,非同父组件的组件间通讯将变得复杂
  • 两个组件间通讯由父组件代理,在其余页面不能直接复用

这个时候我们就需要学习一下eventBusvuex了,当我们进行大量组件化后,组件间的通讯将变成我们伤脑筋的一件事,何解?👨‍👩‍👦我只想给所有组件找一个家,这两者也是应用了这种思想,让我们给组件间传递值的时候变得不再困难.

4.2 eventBus实践出真知

  • 需求: 还是上面的电影展示卡片组件,这个时候我们在这个基础之上在创建一个单独展示的卡片,当差评卡片点击差评的时候,两个卡片一起掉分

怎么解?来先学习一下evenBus,eventBus又称为事件总线,可以想象成一个你订阅了我的博客,我的博客就可以看做一个事件总线,当别人也订阅我这个博客的时候,在同一个文章下,他发的评论你也可以通过我这里看见,eventBus是通过创建一个空白vue对象,然后在两个组件间同时引用,并且通过eventBus.$on以及eventBus.$emit等的方式,用此类方式实现了组件间通讯

  • 实践 创建两个组件以及一个eventBus

image.png EventBus/index.js

import Vue from 'vue'
// 导出一个空白vue对象
export default new Vue()

movie.vue

<template>
  <div class="box">
    <img :src="src" class="myimg" />
    <div class="info">
      <p class="score">
        <b>
          观众评
          <span class="scoreItem">{{score}}</span>
        </b>
      </p>
      <div class="name">
        <span class="title">{{title}}</span>
        <span class="btn" @click="wycp">差评</span>
      </div>
    </div>
  </div>
</template>

<script>
import EventBus from '../EventBus'//会自动找到其中的index.js文件 如果没有请指明
export default {
  props: ['score', 'title', 'src', 'index'],
  methods: {
    wycp () {
      EventBus.$emit('cp', this.index, 0.01)
    }
  }
}
</script>

<style  scoped>
.box {
  width: 214px;
  height: 296px;
  display: inline-block;
  margin: 0 10px;
  position: relative;
}
.myimg {
  width: 100%;
  height: 100%;
  border-radius: 4px;
}

.info {
  width: 214px;
  height: 100px;
  position: absolute;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  color: #fff;
  font-size: 18px;
  padding: 10px;
  bottom: 0;
  box-sizing: border-box;
}

.score {
  font-size: 12px;
  margin-bottom: 2px;
}

.scoreItem {
  color: #fd9832;
  font-size: 16px;
}

.name {
  display: flex;
  justify-content: space-between;
}

.title {
  font-size: 16px;
}

.btn {
  background: #ff4949;
  border-radius: 100px;
  color: #fff;
  padding: 2px 12px 3px 12px;
  font-size: 14px;
  cursor: pointer;
  line-height: 20px;
  text-align: center;
}
</style>>

movieAdd.vue

<template>
  <div>
    <div class="box" v-for="(obj) in arr" :key="obj.id">
      <img :src="obj.src" class="myimg" />
      <div class="info">
        <p class="score">
          <b>
            观众评
            <span class="scoreItem">{{obj.score}}</span>
          </b>
        </p>
        <div class="name">
          <span class="title">{{obj.title}}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import EventBus from '../EventBus'//会自动找到其中的index.js文件 如果没有请指明
export default {
  props: ['arr'],
  created () {
    EventBus.$on('cp', (index, pf) => {
      this.arr[index].score > 0.01 && (this.arr[index].score = (this.arr[index].score - pf).toFixed(2))
    })
  },
}
</script>

<style  scoped>
.box {
  width: 214px;
  height: 296px;
  display: inline-block;
  margin: 0 10px;
  position: relative;
}
.myimg {
  width: 100%;
  height: 100%;
  border-radius: 4px;
}

.info {
  width: 214px;
  height: 100px;
  position: absolute;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  color: #fff;
  font-size: 18px;
  padding: 10px;
  bottom: 0;
  box-sizing: border-box;
}

.score {
  font-size: 12px;
  margin-bottom: 2px;
}

.scoreItem {
  color: #fd9832;
  font-size: 16px;
}

.name {
  display: flex;
  justify-content: space-between;
}

.title {
  font-size: 16px;
}

.btn {
  background: #ff4949;
  border-radius: 100px;
  color: #fff;
  padding: 2px 12px 3px 12px;
  font-size: 14px;
  cursor: pointer;
  line-height: 20px;
  text-align: center;
}
</style>>

需要注意的是:

  • EventBus.$emit('cp', this.index, 0.01)中cp为触发的事件名称,this.index0.01都是触发事件的时候传递的参数,会再该总线成下触发同名的方法
  • EventBus.$on('cp', (参数) => {执行方法}会向监听同名事件的触发,如果有该事件被emit,那么将会执行后面的回调函数,cp为监听事件的名称,()内为触发事件时传递的参数,并会执行{}内的代码

父组件引入并展示 App.vue

<template>
  <div>
    <movie
      v-for="(obj,index) in list"
      :key="obj.id"
      :src="obj.src"
      :score="obj.score"
      :title="obj.title"
      :index="index"
    ></movie>
    <!-- 组件中 写入 @自定义事件名="父组件中的方法名" -->
    <movie-add :arr="list"></movie-add>
  </div>
</template>

<script>
import movie from './components/movie.vue'
import MovieAdd from './components/movieAdd.vue'
export default {
  data () {
    return {
      list: [{
        id: 1,
        src: 'https://p0.pipi.cn/mmdb/25bfd671339c7e8ea33139d0476cb0d92908d.jpg?imageMogr2/thumbnail/2500x2500%3E',
        title: '人生大事',
        score: '9.5'
      },
      {
        id: 2,
        src: 'https://p0.pipi.cn/mmdb/25bfd6d771f0fa57e267cb131f671a5889b2d.jpg?imageMogr2/thumbnail/2500x2500%3E',
        title: '神探大战',
        score: '9'
      },
      {
        id: 3,
        src: 'https://p0.pipi.cn/mmdb/25bfd671be15bf51baf0ee3a5d06b91bf94c3.jpg?imageMogr2/thumbnail/2500x2500%3E',
        title: '侏罗纪世界3',
        score: '8'
      }
      ]
    }
  },
  components: {
    movie: movie,
    MovieAdd
  },
}
</script>

<style>
</style>
  • 实现效果

动画.gif

父组件没有任何的处理,就实现了组件间数据同步,如果我们在复用在其他地方依旧生效,这便是事件总线的好处,本质上事件总线还是一个通知的概念,我们可以非常方便的使用一个组件上下平行的通知其他的组件,也正是因为其很方便,如果我们使用不慎,就会引发一些难以维护的bug,当我们做的系统足够大的时候,我们就需要使用更为完善的vuex状态管理库,从通知转为状态共享层次

5.0 Vuex从0开始

5.1 关于vuex版本

本文是一个从0到vue3的教程,所以后面会介绍vuex4.x版本,当然因为是从0所有会优先从vue2.x版本开始教学,所以本章优先教学vue2.x配合vuex3.x版本如何使用vuex,后面也会教学vue3.x与vuex4.x

5.2 vuex的介绍

在现代web开发中,复杂多变的需求,以至于组件化开发已然成为了事实上的标准,但是大多数场景下组件并不是独立存在的,而是相互协同而存在的,故组件化间的通讯成为了必不可少的开发需求"在现代web开发中,复杂多变的需求,以至于组件化开发已然成为了事实上的标准,但是大多数场景下组件并不是独立存在的,而是相互协同而存在的,故组件化间的通讯成为了必不可少的开发需求,组件通讯我们可以分为一下需求:

  • 父 => 子 props
  • 子 => 父 $emit
  • 兄弟 => 兄弟 eventBus
  • 非关系型组件传值 vuex

vuex可以为我们完成 非关系型组件的数据共享 是一个状态共享库
vuex是采用了集中式的管理组件依赖共享数据的一个工具,可以解决不同组件共享的问题,主要分为三个核心模块=>state,mutations,actions.

  • state可以看做是存储数据的库,用于存放并共享状态数据
  • mutations可以看做是修改的方法,修改state数据必须通过mutations,并且只能执行同步方法
  • acions用于处理异步方法,对于数据的修改会通过mutations进行修改

小贴士:js是单线程的,同步任务指的是前面的任务执行完毕才会去执行后面的任务,一个个来,当我们在js在处理调用接口等的时候也需要等待执行完毕后再进入下一步吗?这将会使页面变得非常卡顿,所以js会将入ajax接口调用,setTimeout,以及Promis等延时操作放在异步的任务队列中..而异步中的执行顺序又可以细分为宏任务,与微任务等,有兴趣同学可以去仔细了解一下这里就不详细介绍了

来一个图了解一下vuex的各模块间的关系吧

vuex的执行顺序.png

vuex通过集中式得存储管理应用的所有组件的状态,并以相应的柜子保证状态以一种可预测的方式进行变化

5.3 vuex的使用

  1. 将vuex安装为运行时依赖(项目上线之后依旧使用的依赖)cmd控制台输入
//Yarn
yarn add vuex@3.6.2 --save
//npm
npm install vuex@3.6.2 --save
  1. main.js中引入并注册vuex
import Vuex from 'vuex'

Vue.use(Vuex)
  1. vuex的实例化并挂载在Vue对象上

const store = new Vuex.Store({
  state: {
    msg: 'hello Vuex',
    count: 0
  }
})

new Vue({
  render: h => h(App),
  store
}).$mount('#app')

完整的main.js

import Vue from 'vue'
import App from './App.vue'
import Button from './components/button.vue'
import Vuex from 'vuex'

Vue.use(Vuex) // 注册Vuex的功能

Vue.config.productionTip = false

// 方式1 全局过滤器
// 任意vue文件中均可直接使用
// 语法: Vue.filter("过滤器名", 值 => 处理后结果)

Vue.filter("reverse", (val, s) => val.split("").reverse().join(s))

Vue.component("Button", Button)

const store = new Vuex.Store({
  state: {
    msg: 'hello Vuex',
    count: 0
  }
})


new Vue({
  render: h => h(App),
  store
}).$mount('#app')

5.4 vuex基础-state

此时,我们的vuex就算是配置好了,可以在组件中直接获取到我们state中的值,以App.vue为例,我们要在插值表达式中取得msg,只需要{{ $store.state.msg }},因为template自动指向this的,在script标签内,我们要获取msg,那么值需要在前面加一个this即可,输入以下this.$store.state.msg活得vuex中msg的值,同时,我在控制台打印了this,我们可以在其中看到vuex已经挂载在了this上

<template>
  <div>
    <div>直接获取state状态{{ $store.state.msg }}</div>
    <button @click="showDia">你好vuex</button>
  </div>
</template>

<script>
export default {
  created () {
    console.log(this)
  },
  methods: {
    showDia () {
      alert(this.$store.state.msg)
    }
  }
}
</script>

<style>
</style>

image.png

5.4.1 利用计算属性简洁获取状态

假如我们在一个组件中需要多次获取这个状态值,每次都写个{{ $store.state.msg }}是不是太麻烦了?哎嘿,还记得之前学过的计算属性吗,我们可以通过计算属性为我们的state定义一个计算属性,通过计算属性直接获取,简洁很多哟~

<template>
  <div>
    <div>直接获取state状态{{ state.msg }}</div>
    <div>直接获取state状态{{ state.msg }}</div>
    <div>直接获取state状态{{ msg }}</div>
    <button @click="showDia">你好vuex</button>
  </div>
</template>

<script>
export default {
  created () {
    console.log(this)
  },
  methods: {
    showDia () {
      alert(this.$store.state.msg)
    }
  },
  computed: {
    state () {
      return this.$store.state
    },
    msg () {
      return this.$store.state.msg
    }
  }
}
</script>

<style>
</style>

image.png

5.4.2引入辅助函数简洁获取状态

vuex封装的有一个辅助函数,名为mapState,比我们使用计算属性的方法更为简洁,学习一下,将我们获取状态更丝滑

  • 引入mapState import { mapState } from 'vuex'
  • 在计算属性中展开定义该属性...mapState(['count', 'msg']) 注意:...是es6中的展开运算符语法会将数组或者对象内部的属性值展开,如:...['张三','李四'] = '张三', '李四'
<template>
  <div>
    <div>直接获取state状态{{ state.msg }}</div>
    <div>直接获取state状态{{ state.msg }}</div>
    <div>直接获取state状态{{ count }}</div>
    <div>直接获取state状态{{ msg }}</div>
    <button @click="showDia">你好vuex</button>
  </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
  created () {
    console.log(this)
  },
  methods: {
    showDia () {
      alert(this.$store.state.msg)
    }
  },
  computed: {
    state () {
      return this.$store.state
    },
    msg () {
      return this.$store.state.msg
    },
    ...mapState(['count', 'msg'])
  }
}
</script>

<style>
</style>

image.png

5.5 vuex基础-mutations

修改state必须通过mutations,mutations是一个对象,内部存储方法,每一个mutations方法都有其对于的参数,分别为:

  • state 指的是当前vuex中的state对象
  • pyload 载荷 提交mutations方法时传递的参数,可以是任意形式任意类型的值 如何使用?this.$store.commit("changeMSg", '点赞转发收藏三连')即可提交修改
    main.js中写好mutations方法
import Vue from 'vue'
import App from './App.vue'
import Button from './components/button.vue'
import Vuex from 'vuex'

Vue.use(Vuex) // 注册Vuex的功能

Vue.config.productionTip = false

// 方式1 全局过滤器
// 任意vue文件中均可直接使用
// 语法: Vue.filter("过滤器名", 值 => 处理后结果)

Vue.filter("reverse", (val, s) => val.split("").reverse().join(s))

Vue.component("Button", Button)

const store = new Vuex.Store({
  state: {
    msg: 'hello Vuex',
    count: 0
  },
  mutations: {
    changeMSg (state, payload) {
      state.msg = payload
    },
    addCount (state) {
      state.count += 1
    }
  }
})


new Vue({
  render: h => h(App),
  store
}).$mount('#app')

此时我们尝试创建一个子组件,通过子组件调用mutations修改方法,实现对父组件展示的状态进行修改,创建一个子组件child.vue

image.png

child.vue

<template>
  <div>
    <button @click="changeMSg">修改字符串</button>
    <button @click="addCount">+1</button>
  </div>
</template>

<script>
export default {
  methods: {
    changeMSg () {
      // 调用mutations, 提交修改
      // 参数1 mutations名称
      // 参数2 载荷
      this.$store.commit("changeMSg", '点赞转发收藏三连')
    },
    addCount () {

      this.$store.commit("addCount")
    }
  }
}
</script>

<style>
</style>

App.vue中引入并使用

<template>
  <div>
    <div>直接获取state状态{{ state.msg }}</div>
    <div>直接获取state状态{{ state.msg }}</div>
    <div>直接获取state状态{{ count }}</div>
    <div>直接获取state状态{{ msg }}</div>
    <button @click="showDia">你好vuex</button>
    <child></child>
  </div>
</template>

<script>
import { mapState } from 'vuex'
import child from './components/child.vue'
export default {
  components: { child },
  created () {
    console.log(this)
  },
  methods: {
    showDia () {
      alert(this.$store.state.msg)
    },
  },
  computed: {
    state () {
      return this.$store.state
    },
    msg () {
      return this.$store.state.msg
    },
    ...mapState(['count', 'msg'])
  }
}
</script>

<style>
</style>

动画.gif

这里需要注意:payload位置上是形参,所以我们取名并没有限制,如果你想叫别的名称也行,只是我们通常取名为payload

5.5.1引入mapMutations简洁写修改方法

类似于mapState,mutations也为我们封装了一个辅助函数mapMutations,同样的只需要传递一个数组字符串类型的参数给这个函数,那么将为我们传入参数中的mutations方法以对象的形式返回,所以也需要用展开运算符,并写在method方法中

<template>
  <div>
    <!-- 默认的第一个参数是 事件参数对象 如果不定义则传递时间参数对象去了 -->
    <!-- 如果还是要传递事件参数对象 可以传递事件参数对象的实参 $event -->
    <button @click="changeMsg('点赞转发收藏三连')">修改字符串</button>
    <button @click="addCount">+1</button>
    <button @click="show">展示mutation</button>
  </div>
</template>

<script>
import { mapMutations } from 'vuex'
export default {
  methods: {
    ...mapMutations(['addCount', 'changeMsg']),
    // changeMSg () {
    //   // 调用mutations, 提交修改
    //   // 参数1 mutations名称
    //   // 参数2 载荷
    //   this.$store.commit("changeMSg", '点赞转发收藏三连')
    // },
    // addCount () {
    //   this.$store.commit("addCount")
    // }
    show () {
      console.log(mapMutations(['addCount', 'changeMsg']));
    }
  }
}
</script>

<style>
</style>

image.png

5.6 vuex基础-actions

当我们需要异步更新state的数据的时候时,我们家需要用到actions,与mutations相同也是一个对象内部存储方法,此时我们利用mock模拟一个接口,用action来实现异步操作,推荐大家可以使用一下国内研发的一款api工具,apifox,我个人用这个感觉很方便

5.6.1 apifox的使用

Apifox点击进入官网下载 安装没什么特别的,选好安装路径即可

  1. 打开程序 image.png
  2. 新建项目,项目名称vue-demo,点击保存 image.png
  3. 点击进入项目 image.png
  4. 新建接口 image.png 5.点击高级mock,新建期望,点击快捷请求上方就是我们模拟的接口地址 image.png image.png ok,这样就创建了一个模拟接口,本地环境可以通过地址获取到期望值,现在我们再学习一下如何使用axios,来在vue中获取到接口值

5.6.2 axios的使用

  1. 安装axios,控制台输入
yarn add axios
// 或者
npm install axios

当在package.json中看到版本后证明安装成功 image.png 2. 此时还并不会大量运用到axios,所以此处不对axios进行二次封装,后续企业级项目实战会进行完整封装,此处这样调用接口即可

import axios from "axios"

axios.get("mock出来的地址").then((res) => {
        console.log(res);
        // 接口调用成功回调
      }).catch((error) => {
        // 接口调用失败毁掉
      });

5.6.3 actions的基本调用

我们以getCount为例,将调用接口获取到的count值覆盖为state中count的值,分为以下几步

  • main.js中store内添加actions,mutations内与acitons内都写上getCount方法
  • 引入axios,并在acions内getCount方法中调用接口,成功回调调用mutations中的getCount方法

main.js

import Vue from 'vue'
import App from './App.vue'
import Button from './components/button.vue'
import Vuex from 'vuex'
// 引入axios
import axios from "axios"

Vue.use(Vuex) // 注册Vuex的功能

Vue.config.productionTip = false

// 方式1 全局过滤器
// 任意vue文件中均可直接使用
// 语法: Vue.filter("过滤器名", 值 => 处理后结果)

Vue.filter("reverse", (val, s) => val.split("").reverse().join(s))

Vue.component("Button", Button)

const store = new Vuex.Store({
  state: {
    msg: 'hello Vuex',
    count: 0
  },
  mutations: {
    changeMsg (state, payload) {
      state.msg = payload
    },
    addCount (state) {
      state.count += 1
    },
    getCount (state, payload) {
      state.count = payload
    }
  },
  actions: {
    // actions方法参数
    // 参数1 执行的上下文对象(相当于this.$store  store的运行实例)
    getCount (context) {
      axios.get("http://127.0.0.1:4523/m1/1304797-0-default/getCount").then((res) => {
        context.commit("getCount", res.data.count)
        // 接口调用成功回调
      }).catch((error) => {
        // 接口调用失败毁掉
        console.log(error);
      });
    }
  }
})


new Vue({
  render: h => h(App),
  store
}).$mount('#app')

需要注意的是actions都有一个参数context,执行的上下文对象,也就是直接指向this.$store child.vue使用this.$store.dispatch('getCount')可以直接调用actions中的方法\

<template>
  <div>
    <!-- 默认的第一个参数是 事件参数对象 如果不定义则传递时间参数对象去了 -->
    <!-- 如果还是要传递事件参数对象 可以传递事件参数对象的实参 $event -->
    <button @click="changeMsg('点赞转发收藏三连')">修改字符串</button>
    <button @click="addCount">+1</button>
    <button @click="show">展示mutation</button>
    <button @click="getCount">获取count值</button>
  </div>
</template>

<script>
import { mapMutations } from 'vuex'
export default {
  methods: {
    ...mapMutations(['addCount', 'changeMsg']),
    // changeMSg () {
    //   // 调用mutations, 提交修改
    //   // 参数1 mutations名称
    //   // 参数2 载荷
    //   this.$store.commit("changeMSg", '点赞转发收藏三连')
    // },
    // addCount () {
    //   this.$store.commit("addCount")
    // }
    show () {
      console.log(this);
    },
    getCount () {
      this.$store.dispatch('getCount')
    }
  }
}
</script>

<style>
</style>

效果如图

动画.gif

调用aciton的时候我们依旧可以传递参数,即第一个参数为上下文对象,第二个参数为自定义参数params(一般这样定义,自己命名也可)

5.6.4 利用mapActions便携使用action

同上,actions也有一个辅助函数mapActions,同样是放在method中,格式与mapMutations差不多

<template>
  <div>
    <!-- 默认的第一个参数是 事件参数对象 如果不定义则传递时间参数对象去了 -->
    <!-- 如果还是要传递事件参数对象 可以传递事件参数对象的实参 $event -->
    <button @click="changeMsg('点赞转发收藏三连')">修改字符串</button>
    <button @click="addCount">+1</button>
    <button @click="show">展示mutation</button>
    <button @click="getCount">获取count值</button>
  </div>
</template>

<script>
import { mapMutations, mapActions } from 'vuex'
export default {
  methods: {
    ...mapMutations(['addCount', 'changeMsg']),
    // changeMSg () {
    //   // 调用mutations, 提交修改
    //   // 参数1 mutations名称
    //   // 参数2 载荷
    //   this.$store.commit("changeMSg", '点赞转发收藏三连')
    // },
    // addCount () {
    //   this.$store.commit("addCount")
    // }
    show () {
      console.log(this);
    },
    // getCount () {
    //   this.$store.dispatch('getCount')
    // }
    ...mapActions(['getCount'])
  }
}
</script>

<style>
</style>

动画.gif

5.7 vuex基础-getters

getters类似于vue中的计算属性,当我们需要基于state派生出一些状态,这些状态是依赖于state的,此时就可以用到getters\

栗子:当我们需要基于用户年龄,来分辨是否成年

  • state中定义年龄,一个数字类型
  state: {
    msg: 'hello Vuex',
    count: 0,
    age: 17
  },
  • 定义getters
  getters: {
    adult: state => state.age >= 18 ? '成年' : '未成年'
  },
  • 直接获取
 <div>是否成年? {{$store.getters.adult}}</div>

全部代码为 main.js

import Vue from 'vue'
import App from './App.vue'
import Button from './components/button.vue'
import Vuex from 'vuex'
// 引入axios
import axios from "axios"

Vue.use(Vuex) // 注册Vuex的功能

Vue.config.productionTip = false

// 方式1 全局过滤器
// 任意vue文件中均可直接使用
// 语法: Vue.filter("过滤器名", 值 => 处理后结果)

Vue.filter("reverse", (val, s) => val.split("").reverse().join(s))

Vue.component("Button", Button)

const store = new Vuex.Store({
  state: {
    msg: 'hello Vuex',
    count: 0,
    age: 17
  },
  getters: {
    adult: state => state.age >= 18 ? '成年' : '未成年'
  },
  mutations: {
    changeMsg (state, payload) {
      state.msg = payload
    },
    addCount (state) {
      state.count += 1
    },
    getCount (state, payload) {
      state.count = payload
    }
  },
  actions: {
    // actions方法参数
    // 参数1 执行的上下文对象(相当于this.$store  store的运行实例)
    getCount (context) {
      axios.get("http://127.0.0.1:4523/m1/1304797-0-default/getCount").then((res) => {
        context.commit("getCount", res.data.count)
        // 接口调用成功回调
      }).catch((error) => {
        // 接口调用失败毁掉
        console.log(error);
      });
    }
  }
})


new Vue({
  render: h => h(App),
  store
}).$mount('#app')

App.vue

<template>
  <div>
    <div>直接获取state状态{{ state.msg }}</div>
    <div>直接获取state状态{{ state.msg }}</div>
    <div>直接获取state状态{{ count }}</div>
    <div>直接获取state状态{{ msg }}</div>
    <button @click="showDia">你好vuex</button>
    <child></child>
    <div>是否成年? {{$store.getters.adult}}</div>
  </div>
</template>

<script>
import { mapState } from 'vuex'
import child from './components/child.vue'
export default {
  components: { child },
  created () {
    console.log(this)
  },
  methods: {
    showDia () {
      alert(this.$store.state.msg)
    },
  },
  computed: {
    state () {
      return this.$store.state
    },
    msg () {
      return this.$store.state.msg
    },
    ...mapState(['count', 'msg'])
  }
}
</script>

<style>
</style>

效果如图

image.png

5.7.1 利用mapGetters便携使用getters

mapState类似,都是放置与计算属性中,其余一样

  • 引入mapGetters
  • 在计算属性中放入...mapGetters(['adult'])

App.vue

<template>
  <div>
    <div>直接获取state状态{{ state.msg }}</div>
    <div>直接获取state状态{{ state.msg }}</div>
    <div>直接获取state状态{{ count }}</div>
    <div>直接获取state状态{{ msg }}</div>
    <button @click="showDia">你好vuex</button>
    <child></child>
    <div>是否成年? {{$store.getters.adult}}</div>
    <div>是否成年? {{adult}}</div>
  </div>
</template>

<script>
import { mapState, mapGetters } from 'vuex'
import child from './components/child.vue'
export default {
  components: { child },
  created () {
    console.log(this)
  },
  methods: {
    showDia () {
      alert(this.$store.state.msg)
    },
  },
  computed: {
    state () {
      return this.$store.state
    },
    msg () {
      return this.$store.state.msg
    },
    ...mapState(['count', 'msg']),
    ...mapGetters(['adult'])
  }
}
</script>

<style>
</style>

image.png 当作是vuex的计算属性即可

5.8 vuex的模块化-Module

在我们之前学习中,所有的数据更新还有操作都是放在main.js中的store中的,这有一个问题,就是当我们项目越来越大的时候,我们就越难以维护,所以vuex开发了一个模块化的Module来解决上面存在的一些问题,可以将一个数据拆分到一个个模块中,之前学习的内容在任意模块都适用

  • 在state同级目录下定义modules
  • 书写内部的需要存储的值

main.js

import Vue from 'vue'
import App from './App.vue'
import Button from './components/button.vue'
import Vuex from 'vuex'
// 引入axios
import axios from "axios"

Vue.use(Vuex) // 注册Vuex的功能

Vue.config.productionTip = false

// 方式1 全局过滤器
// 任意vue文件中均可直接使用
// 语法: Vue.filter("过滤器名", 值 => 处理后结果)

Vue.filter("reverse", (val, s) => val.split("").reverse().join(s))

Vue.component("Button", Button)

const store = new Vuex.Store({
  state: {
    msg: 'hello Vuex',
    count: 0,
    age: 17
  },
  getters: {
    adult: state => state.age >= 18 ? '成年' : '未成年'
  },
  mutations: {
    changeMsg (state, payload) {
      state.msg = payload
    },
    addCount (state) {
      state.count += 1
    },
    getCount (state, payload) {
      state.count = payload
    }
  },
  actions: {
    // actions方法参数
    // 参数1 执行的上下文对象(相当于this.$store  store的运行实例)
    getCount (context) {
      axios.get("http://127.0.0.1:4523/m1/1304797-0-default/getCount").then((res) => {
        context.commit("getCount", res.data.count)
        // 接口调用成功回调
      }).catch((error) => {
        // 接口调用失败毁掉
        console.log(error);
      });
    }
  },
  // 放置子模块的属性
  modules: {
    behavior: {
      state: {
        behavior: ['点赞', '转发', '三连']
      },
    },
    user: {
      state: {
        name: '编程狂徒张三'
      },
    }
  }
})


new Vue({
  render: h => h(App),
  store
}).$mount('#app')

  • 此时创建一个新的组件grandson.vue作为孙子组件测试

image.png

child组件中引入grandson.vue组件

<template>
  <div>
    <!-- 默认的第一个参数是 事件参数对象 如果不定义则传递时间参数对象去了 -->
    <!-- 如果还是要传递事件参数对象 可以传递事件参数对象的实参 $event -->
    <button @click="changeMsg('点赞转发收藏三连')">修改字符串</button>
    <button @click="addCount">+1</button>
    <button @click="show">展示mutation</button>
    <button @click="getCount">获取count值</button>
    <grandson></grandson>
  </div>
</template>

<script>
import { mapMutations, mapActions } from 'vuex'
import grandson from './grandson.vue';
export default {
  components: { grandson },
  methods: {
    ...mapMutations(['addCount', 'changeMsg']),
    // changeMSg () {
    //   // 调用mutations, 提交修改
    //   // 参数1 mutations名称
    //   // 参数2 载荷
    //   this.$store.commit("changeMSg", '点赞转发收藏三连')
    // },
    // addCount () {
    //   this.$store.commit("addCount")
    // }
    show () {
      console.log(this);
    },
    // getCount () {
    //   this.$store.dispatch('getCount')
    // }
    ...mapActions(['getCount'])
  }
}
</script>

<style>
</style>

grandson.vue组件

<template>
  <div>
    <div>用户名称: {{$store.state.user.name}}</div>
    <ul v-for="(item,index) in $store.state.behavior.behavior" :key="index">
      <li>{{item}}</li>
    </ul>
  </div>
</template>

<script>
export default {

}
</script>

<style>
</style>

image.png

5.8.1 利用getters实现模块值获取简写

用直接获取写起来比较麻烦,我们可以用getters来进行一个代理简写

  getters: {
    adult: state => state.age >= 18 ? '成年' : '未成年',
    behavior: state => state.behavior.behavior,
    name: state => state.user.name,
  },

完整main.js如下

import Vue from 'vue'
import App from './App.vue'
import Button from './components/button.vue'
import Vuex from 'vuex'
// 引入axios
import axios from "axios"

Vue.use(Vuex) // 注册Vuex的功能

Vue.config.productionTip = false

// 方式1 全局过滤器
// 任意vue文件中均可直接使用
// 语法: Vue.filter("过滤器名", 值 => 处理后结果)

Vue.filter("reverse", (val, s) => val.split("").reverse().join(s))

Vue.component("Button", Button)

const store = new Vuex.Store({
  state: {
    msg: 'hello Vuex',
    count: 0,
    age: 17
  },
  getters: {
    adult: state => state.age >= 18 ? '成年' : '未成年',
    behavior: state => state.behavior.behavior,
    name: state => state.user.name,
  },
  mutations: {
    changeMsg (state, payload) {
      state.msg = payload
    },
    addCount (state) {
      state.count += 1
    },
    getCount (state, payload) {
      state.count = payload
    }
  },
  actions: {
    // actions方法参数
    // 参数1 执行的上下文对象(相当于this.$store  store的运行实例)
    getCount (context) {
      axios.get("http://127.0.0.1:4523/m1/1304797-0-default/getCount").then((res) => {
        context.commit("getCount", res.data.count)
        // 接口调用成功回调
      }).catch((error) => {
        // 接口调用失败毁掉
        console.log(error);
      });
    }
  },
  // 放置子模块的属性
  modules: {
    behavior: {
      state: {
        behavior: ['点赞', '转发', '三连']
      },
    },
    user: {
      state: {
        name: '编程狂徒张三'
      },
    }
  }
})


new Vue({
  render: h => h(App),
  store
}).$mount('#app')

grandson.vue

<template>
  <div>
    <div>用户名称: {{$store.state.user.name}}</div>
    <!-- <ul v-for="(item,index) in $store.state.behavior.behavior" :key="index">
      <li>{{item}}</li>
    </ul>-->
    <!-- 简写 -->
    <div>用户名称: {{name}}</div>
    <ul v-for="(item,index) in behavior" :key="index">
      <li>{{item}}</li>
    </ul>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
  computed: {
    ...mapGetters(['name', 'behavior'])
  }
}
</script>

<style>
</style>

5.8.2 模块化中的命名空间

namespaced(命名空间),默认情况下,模块内部中的acitons,mutations是注册在全局命名空间下的,全局命名空间下每个模块的mutationsactions可以全局调用 即子模块的方法直接可以使用this.$store.commit(changeName,'王五')调用,当我们想要保持模块的私密性,就可以用namespaced给我们的模块加一个锁,给定一个布尔值的true`即可

  user: {
      namespaced: true,
      state: {
        name: '编程狂徒张三'
      },
      methods: {
        changeName (name) {
          state.name = name
        }
      }
    }

此时this.$store.commit(changeName,'王五')调用将会失败,当加入了这个属性我们有三个解决方案

  1. 调用时加入路径
this.$store.commit("user/changeName", '王五')
  1. 使用辅助函数加上路径
 
 ...mapMutations(['user/changeName'])
     changeNameMutations (name) {
      this['user/changeName'](name)
    }
  1. 创建基于某个命名空间的辅助函数createNamespacedHelpers
import { mapState, mapGetters, createNamespacedHelpers } from 'vuex'
const { mapMutations } = createNamespacedHelpers('user')
    ...mapMutations(['changeName']),

三种方法都可以推荐是配合1与3使用

<template>
  <div>
    <div>直接获取state状态{{ state.msg }}</div>
    <div>直接获取state状态{{ state.msg }}</div>
    <div>直接获取state状态{{ count }}</div>
    <div>直接获取state状态{{ msg }}</div>
    <button @click="showDia">你好vuex</button>
    <child></child>
    <div>是否成年? {{$store.getters.adult}}</div>
    <div>是否成年? {{adult}}</div>
    <button @click="changeName">改名称为王五</button>
    <button @click="changeNameMutations('王五')">改名称为王五</button>
    <button @click="changeName('王五')">改名称为王五</button>
  </div>
</template>

<script>
// import { mapMutations, mapState, mapGetters, createNamespacedHelpers } from 'vuex'
import { mapState, mapGetters, createNamespacedHelpers } from 'vuex'
const { mapMutations } = createNamespacedHelpers('user')
import child from './components/child.vue'
export default {
  components: { child },
  created () {
    console.log(this)
  },
  methods: {
    // ...mapMutations(['user/changeName']),
    ...mapMutations(['changeName']),
    showDia () {
      alert(this.$store.state.msg)
    },
    // changeName () {
    //   this.$store.commit("user/changeName", '王五')
    // },
    // changeNameMutations (name) {
    //   this['user/changeName'](name)
    // },
  },
  computed: {
    state () {
      return this.$store.state
    },
    msg () {
      return this.$store.state.msg
    },
    ...mapState(['count', 'msg']),
    ...mapGetters(['adult'])
  }
}
</script>

<style>
</style>

image.png

6.0 vuex模块化构建

上面我们已经学习完了vuex的基本概念,实际开发中是不会将所有的代码放在同一文件中的,当vuex的存储以及操作都放在main.js中项目越来越大那么将变得难以维护已经难以阅读,知道命名空间以及vuex模块化后我们要做的就是将原本全写在main.js中的代码分模块,分功能的存储在不同的文件中,通过模块化导入的技术实现vuex模块化建设.

6.1 创建一个船新版本的vue

vue create vuex_demo

此时我们可以选择构建脚手架的时候就直接安装vuex

image.png

空格 勾选vuex image.png 选择2.x image.png eslint选择标准校验模式 image.png 将提交的时候尝试校验并修复勾选上

image.png 默认回车 image.png 这里不存储为一个模式 输入N image.png 此时可以发现我们src目录下自动创建了一个store文件夹,是用于vuex的文件夹 image.png 关闭eslint校验,在vue.config.js文件中

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  lintOnSave: false // 关闭eslint校验
})

启动服务

yarn serve

ok此时我们可以发现模块化vuex已经初具规模,来我们看看vue官方为我们做了哪些操作,来吧视线对准store目录下的index.js

import Vue from 'vue' //引入vue
import Vuex from 'vuex' // 引入vuex

Vue.use(Vuex) // 将vuex挂载在vue上
// 导出store
export default new Vuex.Store({
  state: {
  },
  getters: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

image.png

image.png

效果都是一样的.模块化带来的好处便是我们更便于维护阅读,当代码成千上万行的时候定位错误将变得困难,模块化可以将功能分装在不同模块中,某一个模块出错通过报错我们可以更快定位错误,进行维护

6.2 vuex模块化实践

6.2.1 准备工作

上面我们已经完成了创建了一个自带vuex的vue项目,现在我们做一些准备让实践更顺畅

image.png

  • 关闭eslint校验(vue.config.js文件中)
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  lintOnSave: false
})
  • App.vue多余代码清除
<template>
  <div>根文件</div>
</template>

<script>

export default {
  name: 'App',

}
</script>

<style>
</style>

  • assets与components文件夹中多余文件删除

  • 启动项目


cd vuex_demo

yarn serve

image.png

image.png

这样下来准备工作就做好了,接下来我们会通过一个案例来逐步了解如何如何分业务模块化的使用vuex

6.2.2 实践

  • store文件夹中创建modules文件夹用于储存各业务模块

image.png

  • 分别创建三个子模块,消费者consumer.js,商店shop.js,购物车shoppingCart.js

image.png

  • store文件夹下的 index.js入口文件中引入模块
import Vue from 'vue'
import Vuex from 'vuex'

import shop from './modules/shop'
import consumer from './modules/consumer'
import shoppingCart from './modules/shoppingCart'


Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  getters: {

  },
  mutations: {
  },
  actions: {
  },
  modules: {
    shop,
    consumer,
    shoppingCart
  }
})

本案例实现的的一个类似于游戏商店的功能,并不是功能的最简写,为了能够尽可能多的用到vuex,案例的目的也是为了巩固vuex的学习

实现效果如下

动画.gif

分析一下需求

  • 商店模块,有商店数据,当我们完成购买的时候库存会减少
  • 购物车模块应该有个数组来接收我们存在购物车中的数据,点击添加购物车会在数组添加一个该商品,购买后会清除
  • 余额模块当我们钱不够花的时候要有弹窗并且不做操作,当够的时候是先花钱->减少库存->清空购物车

比较简单哈,主要是为了熟悉vuex实际开发中的应用

  • shop模块 state中存放好商品数据goods,以及一个减少库存reduceStock方法
  • 需要注意reduceStock方法,传递一个商品id以及购买的数量用一个对象包裹传递过来
export default {
  namespaced: true,
  state: {
    goods: [
      {
        id: 1,
        // 名称
        name: '红药水',
        // 库存
        stock: 10,
        // 价格
        price: 50,
        // 图片
        src: 'https://img0.baidu.com/it/u=3176039336,1373442311&fm=253&fmt=auto&app=138&f=JPEG?w=417&h=452'
      },
      {
        id: 2,
        name: '蓝药水',
        stock: 10,
        price: 50,
        src: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Ffile.laisj.cn%2Feditor%2Fv3%2Fimage%2F20171129%2F1511935972900243.gif&refer=http%3A%2F%2Ffile.laisj.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1661415323&t=556690764463c9e81ba0a5547693cb15'
      },
      {
        id: 3,
        name: '复活币',
        stock: 10,
        price: 100,
        src: 'https://img0.baidu.com/it/u=838846088,3482963313&fm=253&fmt=auto&app=138&f=JPEG?w=236&h=267'
      },
    ]
  },
  mutations: {
    // 减少库存
    reduceStock (state, payload) {
      state.goods.forEach(item => {
        if (item.id == payload.id) {
          item.stock = Number(item.stock - payload.count)
        }
      })
    }
  },
  actions: {

  }
}

store文件入口文件index.js中写一个快捷获取方法

 getters: {
    goods: state => state.shop.goods,
  },
  • components文件夹内创建一个shop组件

image.png

  • 该组件主要实现商店商品的展示,添加商品
  • 添加商品方法命名为pushMyCart,在shoppingCart模块中实现,点击添加则传递整个商品对象给方法

shop.vue

<template>
  <div>
    <div class="shopBox">
      <div class="item" v-for="(item,index) in goods" :key="item.id">
        <div>{{item.name}}</div>
        <img :src="item.src" :alt="item.name" class="img" />
        <div class="msg">
          <div>
            价格:
            <span class="price">${{item.price}}</span>
          </div>
          <div>
            库存:
            <span class="price">{{item.stock}}</span>
          </div>
        </div>
        <div class="btn" @click="addCart(index)">加入购物车</div>
      </div>
    </div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
  computed: {
    ...mapGetters(['goods'])
  },
  methods: {
    addCart (index) {
      this.$store.commit("shoppingCart/pushMyCart", this.goods[index])
    }
  }
}
</script>

<style  scoped>
.shopBox {
  display: flex;
  justify-content: space-around;
  width: 80%;
  margin: 20px auto;
  border-radius: 20px;
  height: 200px;
}

.item {
  display: flex;
  align-items: center;
}

.msg {
  margin-right: 20px;
}

.price {
  color: red;
}

.img {
  width: 50px;
  height: 50px;
  border-radius: 20px;
  margin: 0 20px;
}

.btn {
  padding: 10px 40px;
  border-radius: 10px;
  background-color: #0276e8;
  color: #fff;
  font-size: 16px;
  text-align: center;
  line-height: 25px;
  cursor: pointer;
}
</style>>

  • shoppingCart模块,myCart用来储存购物车数据,pushMyCart用来添加购物车,myCartInit用来重置购物车
  • 我们需要考虑两种情况,一种是添加购物车中已有的商品,二是添加新的商品
  • 添加新商品直接给对象赋值一个count属性,将数组push到数组中即可
  • 添加已有商品,这里需要注意复杂对象的修改,vue未必能检测到,对于复杂对象,我们最好利用展开运算符,使vue强制刷新一下数据,避免数据不一致

shoppingCart.js

export default {
  namespaced: true,
  state: {
    myCart: []
  },
  mutations: {
    pushMyCart (state, payload) {
      let obj = payload
      let isInArr = state.myCart.some(item => item.id == obj.id)
      if (isInArr) {
        // forEach会修改,但是vue监听不道复杂对象的变化
        // 利用展开运算符配合filter来修改,这样每次添加vue都能监听到,因为每次都会创建一个新的复杂对象,故这样便可以监听到
        let list = state.myCart.map(item => {
          if (item.id == obj.id) {
            if (item.count >= obj.stock) {
              return item
            }
            item.count++
            return item
          } else {
            return item
          }
        })
        state.myCart = [...list]
      } else {
        obj.count = 1
        state.myCart.push(obj)
      }
    },
    myCartInit (state) {
      state.myCart = []
    }
  },
  actions: {

  }
}

此处也是写一些快捷导入 balance是余额,后续会写,后面就不啰嗦了,一起先写在store的index.js文件夹中吧 完整的index.js

import Vue from 'vue'
import Vuex from 'vuex'

import shop from './modules/shop'
import consumer from './modules/consumer'
import shoppingCart from './modules/shoppingCart'


Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  getters: {
    goods: state => state.shop.goods,
    myCart: state => state.shoppingCart.myCart,
    balance: state => state.consumer.balance
  },
  mutations: {
  },
  actions: {
  },
  modules: {
    shop,
    consumer,
    shoppingCart
  }
})

  • shoppingCart组件用来展示已经添加到购物车的数据,以及购买购物车中的商品
  • 购买事件做四件事,判断余额是否足够购买->降低商品库存->消费余额->清空购物车

shoppingCart.vue

<template>
  <div class="mycart">
    <h2>购物车</h2>
    <div class="item" v-for="item in myCart" :key="(item.id)">
      <div class="name">{{item.name}}</div>
      <div class="count">{{item.count}}</div>
      <div class="price">{{item.price}}</div>
    </div>
    <span class="btn" @click="buy" v-if="myCart.length>0">购买</span>
  </div>
</template>

<script>
import { mapGetters, createNamespacedHelpers } from 'vuex'
const { mapMutations } = createNamespacedHelpers('consumer')
export default {
  computed: {
    ...mapGetters(['myCart', 'balance'])
  },
  methods: {
    ...mapMutations(['purchase']),
    buy () {
      // 消费余额
      let num = 0
      this.myCart.map(item => {
        num += Number(item.price * item.count)
      })
      if (this.balance > num) {
        // 降低商店库存
        this.myCart.map(item => {
          this.$store.commit('shop/reduceStock', {
            id: item.id,
            count: item.count
          })
        })
        this.purchase(num)
        // 清空购物车
        this.$store.commit('shoppingCart/myCartInit')
      } else {
        alert('余额不足,暂时无法购买')
      }

    }
  }
}
</script>

<style scoped>
.mycart {
  display: flex;
  flex-direction: column;
}

.item {
  display: flex;
  align-items: center;
}

.count {
  margin: 0 20px;
  color: rgb(39, 39, 150);
}

.price {
  color: red;
}

.btn {
  width: 200px;
  padding: 10px 20px;
  border-radius: 10px;
  background-color: #0276e8;
  color: #fff;
  font-size: 16px;
  text-align: center;
  line-height: 25px;
  cursor: pointer;
  margin-top: 20px;
}
</style>
  • consumer模块作为消费者模块就做两件事,存储余额,消费
  • purchase为消费事件,balance为余额

consumer.js

export default {
  namespaced: true,
  state: {
    balance: 666
  },
  mutations: {
    purchase (state, num) {
      state.balance -= num
    }
  },
  actions: {

  }
}
  • consumer组件更简单,只需要展示余额即可

consumer.vue

<template>
  <div>
    <h2>余额:{{balance}}</h2>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
  computed: {
    ...mapGetters(['balance'])
  }

}
</script>

<style>
</style>

App.vue文件中引入并使用组件

<template>
  <div>
    <shop></shop>
    <div class="center">
      <shopping-cart></shopping-cart>
      <consumer></consumer>
    </div>
  </div>
</template>

<script>
import Consumer from './components/consumer.vue'
import shop from './components/shop.vue'
import ShoppingCart from './components/shoppingCart.vue'
export default {
  components: { shop, ShoppingCart, Consumer },
  name: 'App',

}
</script>

<style>
.center {
  width: 80%;
  margin: 20px auto;
  display: flex;
  justify-content: space-around;
}
</style>

这样一个商店的案例就完成了,我刻意的将大部分属性都分模块了,其实实际开发不用分这么细,实践也是为了让大家熟悉一下当我们开启命名空间的模块中如何使用vuex,注意我的用法即可,详细的在之前基础里面已经讲很细了,个人理解,如有不对请指出哦

来看看我的其他章节吧,正在长更中

从0到Vue3企业项目实战【01.Vue的基本概念与学习指南】 - 掘金 (juejin.cn)

从0到Vue3企业项目实战【02.了解并理解Vue指令以及虚拟Dom】 - 掘金 (juejin.cn)

从0到Vue3企业项目实战【03.vue基本api入门】 - 掘金 (juejin.cn)

从0到Vue3企业项目实战【04.从vue组件通讯到eventBus以及vuex(附mock接口与axios简单实践)】 - 掘金 (juejin.cn)

从0到Vue3企业项目实战【05.vue生命周期】 - 掘金 (juejin.cn)

从0到Vue3企业项目实战【06.refref与nextTick使用】 - 掘金 (juejin.cn)

从0到Vue3企业项目实战【07.vue动态组件,组件缓存,组件插槽,子组件直接修改props,自定义指令看这一篇就够了】 - 掘金 (juejin.cn)

从0到Vue3企业项目实战【08.Vue路由基础】 - 掘金 (juejin.cn)

从0到Vue3企业项目实战【09.Vue路由进阶】 - 掘金 (juejin.cn)