前端框架Vue(六)


Vuex状态管理库

基本介绍

Vuex,是一个专为Vue.js应用程序开发的状态管理模式+库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

什么情况下应该使用Vuex?

  • 页面有多个需要共享的状态,引入vuex,便于维护(非父子通信);
  • 缓存部分异步数据,减少后端服务的访问;

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。
Vuex的状态存储是响应式的。当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会相应地得到高效更新;不能直接改变store中的状态,改变store中的状态的唯一途径是显式地提交(commit)mutation,这样可以方便跟踪每一个状态的变化。

上一篇文章介绍的案例中,底部导航在进入detail页面后没有隐藏,这里通过Vuex来实现进入detail页隐藏底部导航。

VOA模式

新建src/VueRouter/store/index.js 状态管理js文件

import { createStore } from "vuex";

const store = createStore({
  state(){
    return{
      isTabbarShow:true
    }
  }
})

export default store

在main.js中引入并进行注册

import { createApp } from 'vue'
import './style.css'
import App from './VueRouter/App.vue'
import router from './VueRouter/router' // 导入路由
import store from './VueRouter/store'   // 导入状态
import ElementPlus from 'element-plus'  // 引入element plus组件库
import 'element-plus/dist/index.css'  // 引入element plus样式表

let app = createApp(App)

app.use(ElementPlus) // 加载Elementpuls
app.use(router) // 注册路由插件
app.use(store) // 注册vuex状态插件
app.mount("#app")

修改Detail.vue

<template>
  <div>
    <button @click="plBackClick">返回</button>
    detail
  </div>
</template>

<script>
export default {
  // 挂载前隐藏
  beforeMount(){
    this.$store.state.isTabbarShow = false
  },
  beforeUnmount(){
    // 卸载前显示
    this.$store.state.isTabbarShow = true
  },
  mounted(){
    console.log('接收上一个页面传来的参数',this.$route.params.plid)
  },
  methods:{
    plBackClick(){
      this.$router.back()   // 返回  this.$router.forward()
    }
  }
}
</script>

修改App.vue如下:

<template>
  <div>
    <!-- 通过插槽插入子组件路由 -->
    <router-view></router-view>
    <!-- 增加导航 -->
    <!-- 增加v-show判断底部导航是否显示 -->
    <Tabbar v-show="$store.state.isTabbarShow"></Tabbar> 
  </div>
</template>

<script>
import Tabbar from './components/Tabbar.vue';
export default {
  components:{
    Tabbar
  },
  mounted(){
    console.log(this.$store.state.isTabbarShow)
  }
}
</script>

<style>
*{
  margin: 0;
  padding: 0;
}
ul{
  list-style: none;
}
</style>

上面的例子中是在组件中直接修改的状态,this.$store.state.isTabbarShow = false是不推荐的方法,而是需要通过提交(commit)mutation操作。

Mutation

更改Vuex的store中的状态的唯一方法是提交mutation。Vuex中的mutation非常类似于事件:每个mutation都有一个字符串的事件类型(type)和一个回调函数(handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受state作为第一个参数。

修改index.js

import { createStore } from "vuex";

const store = createStore({
  state(){
    return{
      isTabbarShow:true
    }
  },
  // 修改状态的位置
  mutations:{
    showTabbar(state){
      state.isTabbarShow = true
    },
    hideTabbar(state){
      state.isTabbarShow = false
    }
  }
})

export default store

修改Detail.vue的内容:

<template>
  <div>
    <button @click="plBackClick">返回</button>
    detail
  </div>
</template>

<script>
export default {
  beforeMount(){
    // this.$store.state.isTabbarShow = false
    this.$store.commit("hideTabbar") // commit()的内容是在index.js中的mutations
  },
  beforeUnmount(){
    // this.$store.state.isTabbarShow = true
    this.$store.commit("showTabbar") // commit()的内容是在index.js中的mutations
  },
  mounted(){
    console.log('接收上一个页面传来的参数',this.$route.params.plid)
  },
  methods:{
    plBackClick(){
      this.$router.back()   // 返回  this.$router.forward()
    }
  }
}
</script>

可以向store.commit传入额外的参数,即mutation的载荷(payload)

增加其他参数

修改index.js

import { createStore } from "vuex";

const store = createStore({
  state(){
    return{
      isTabbarShow:true
    }
  },
  // 修改状态的位置
  mutations:{
    // showTabbar(state){
    //   state.isTabbarShow = true
    // },
    // hideTabbar(state){
    //   state.isTabbarShow = false
    // }
    changeTabbar(state, payload){
      state.isTabbarShow = payload
    }
  }
})

export default store

修改Detail.vue

<template>
  <div>
    <button @click="plBackClick">返回</button>
    detail
  </div>
</template>

<script>
export default {
  beforeMount(){
    // this.$store.state.isTabbarShow = false
    // this.$store.commit("hideTabbar") // commit()的内容是在index.js中的mutations
    this.$store.commit("changeTabbar",false)
  },
  beforeUnmount(){
    // this.$store.state.isTabbarShow = true
    // this.$store.commit("showTabbar") // commit()的内容是在index.js中的mutations
    this.$store.commit("changeTabbar",true)
  },
  mounted(){
    console.log('接收上一个页面传来的参数',this.$route.params.plid)
  },
  methods:{
    plBackClick(){
      this.$router.back()   // 返回  this.$router.forward()
    }
  }
}
</script>

Action

Action类似于mutation,不同在于:

  • Action提交的是mutation,而不是直接变更状态;
  • Action可以保护任意异步操作;

修改Recommend.vue,Action通过store.dispatch方法触发

<template>
  <div>
    <ul>
      <li v-for="data in $store.state.recommendList" :key="data.cinemaId">
        {{data.name}}
      </li>
    </ul>
  </div>
</template>
<script>
export default{
  mounted(){
    if(this.$store.state.recommendList.length===0){
      this.$store.dispatch("getRecommendList")
    }else{
      console.log("缓存")
    }
  }
}
</script>
<style scoped lang="scss">
ul{
  li{
    padding:10px;
  }
}
</style>

在store/index.js中增加Action

import { createStore } from "vuex";

import axios from "axios";

const store = createStore({
  state(){
    return{
      isTabbarShow:true,
      recommendList:[]
    }
  },
  // 修改状态的位置,同步
  mutations:{
    // showTabbar(state){
    //   state.isTabbarShow = true
    // },
    // hideTabbar(state){
    //   state.isTabbarShow = false
    // }
    changeTabbar(state, payload){
      state.isTabbarShow = payload
    },
    changeRecommendList(state, payload){
      state.recommendList = payload
    }
  },
  // 同步+异步
  actions:{
    async getRecommendList(store){
      let res = await axios({
        url:"https://m.maizuo.com/gateway?cityId=110100&ticketFlag=1&k=112132",
        headers:{
          'X-Client-Info':'{"a":"3000","ch":"1002","v":"5.2.1","e":"16931924276063063597842433"}',
          'X-Host':'mall.film-ticket.cinema.list'
        }
      })
      // console.log(res.data.data.cinemas)
      // 提交mutation
      store.commit("changeRecommendList",res.data.data.cinemas)
    }
  }
})

export default store

Getter

在Recommend.vue中增加列表筛选功能

<template>
  <div>
    <select v-model="type">
      <option :value="1">App订票</option>
      <option :value="0">前台兑换</option>
    </select>
    <ul>
      <li v-for="data in filterRecommendList" :key="data.cinemaId">
        {{data.name}}
      </li>
    </ul>
  </div>
</template>
<script>
export default{
  data(){
    return{
      type:1
    }
  },
  mounted(){
    if(this.$store.state.recommendList.length===0){
      this.$store.dispatch("getRecommendList")
    }else{
      console.log("缓存")
    }
  },
  computed:{
    filterRecommendList(){
      return this.$store.state.recommendList.filter(item=>item.eTicketFlag===this.type)
    }
  }
}
</script>
<style scoped lang="scss">
ul{
  li{
    padding:10px;
  }
}
</style>

这里使用到了计算属性computed,这里将computed的方法迁移到store/index.js中

import { createStore } from "vuex";

import axios from "axios";

const store = createStore({
  state(){
    return{
      isTabbarShow:true,
      recommendList:[]
    }
  },
  // 修改状态的位置,同步
  mutations:{
    // showTabbar(state){
    //   state.isTabbarShow = true
    // },
    // hideTabbar(state){
    //   state.isTabbarShow = false
    // }
    changeTabbar(state, payload){
      state.isTabbarShow = payload
    },
    changeRecommendList(state, payload){
      state.recommendList = payload
    }
  },
  // 同步+异步
  actions:{
    async getRecommendList(store){
      let res = await axios({
        url:"https://m.maizuo.com/gateway?cityId=110100&ticketFlag=1&k=112132",
        headers:{
          'X-Client-Info':'{"a":"3000","ch":"1002","v":"5.2.1","e":"16931924276063063597842433"}',
          'X-Host':'mall.film-ticket.cinema.list'
        }
      })
      // console.log(res.data.data.cinemas)
      // 提交mutation
      store.commit("changeRecommendList",res.data.data.cinemas)
    }
  },
  // store的计算属性 (computed)
  getters:{
    filterRecommendList(state){
      return (type)=>{
        return state.recommendList.filter(item=>item.eTicketFlag===type)
      }
    }
  }
})

export default store

对应修改Recommend.vue

<template>
  <div>
    <select v-model="type">
      <option :value="1">App订票</option>
      <option :value="0">前台兑换</option>
    </select>
    <ul>
      <li v-for="data in $store.getters.filterRecommendList(type)" :key="data.cinemaId">
        {{data.name}}
      </li>
    </ul>
  </div>
</template>
<script>
export default{
  data(){
    return{
      type:1
    }
  },
  mounted(){
    if(this.$store.state.recommendList.length===0){
      this.$store.dispatch("getRecommendList")
    }else{
      console.log("缓存")
    }
  },
  // computed:{
  //   filterRecommendList(){
  //     return this.$store.state.recommendList.filter(item=>item.eTicketFlag===this.type)
  //   }
  // }
}
</script>
<style scoped lang="scss">
ul{
  li{
    padding:10px;
  }
}
</style>

辅助函数

mapState

修改App.vue,增加computed引入mapState取代this.$store.state.isTabbarShow这种复杂的写法。

<template>
  <div>
    <!-- 通过插槽插入子组件路由 -->
    <router-view></router-view>
    <!-- 增加导航 -->
    <!-- 增加v-show判断 -->
    <Tabbar v-show="isTabbarShow"></Tabbar> 
  </div>
</template>

<script>
import { mapState } from 'vuex';
import Tabbar from './components/Tabbar.vue';
export default {
  components:{
    Tabbar
  },
  mounted(){
    console.log(this.$store.state.isTabbarShow)
    console.log(this.$store.state.recommendList)
  },
  // computed:{
  //   isTabbarShow(){
  //     return this.$store.state.isTabbarShow
  //   }
  // }
  computed:{
    ...mapState(['isTabbarShow'])
  }
}
</script>

<style>
*{
  margin: 0;
  padding: 0;
}
ul{
  list-style: none;
}
</style>

其中的

computed:{
  ...mapState(['isTabbarShow'])
}

等价于

computed:{
  isTabbarShow(){
    return this.$store.state.isTabbarShow
  }
}

同样的方式修改Recommend.vue

<template>
  <div>
    <select v-model="type">
      <option :value="1">App订票</option>
      <option :value="0">前台兑换</option>
    </select>
    <ul>
      <li v-for="data in $store.getters.filterRecommendList(type)" :key="data.cinemaId">
        {{data.name}}
      </li>
    </ul>
  </div>
</template>
<script>
import { mapState } from 'vuex';

export default{
  data(){
    return{
      type:1
    }
  },
  mounted(){
    // if(this.$store.state.recommendList.length===0){
    if(this.recommendList.length===0){
      this.$store.dispatch("getRecommendList")
    }else{
      console.log("缓存")
    }
  },
  computed:{
    ...mapState(["recommendList"])
  }
  // computed:{
  //   filterRecommendList(){
  //     return this.$store.state.recommendList.filter(item=>item.eTicketFlag===this.type)
  //   }
  // }
}
</script>
<style scoped lang="scss">
ul{
  li{
    padding:10px;
  }
}
</style>

mapMutations

修改Detail.vue,增加computed引入mapMutations取代this.$store.commit("changeTabbar",false)这种复杂的写法。

<template>
  <div>
    <button @click="plBackClick">返回</button>
    detail
  </div>
</template>

<script>
import { mapMutations } from 'vuex'
export default {
  beforeMount(){
    // this.$store.state.isTabbarShow = false
    // this.$store.commit("hideTabbar") // commit()的内容是在index.js中的mutations
    // this.$store.commit("changeTabbar",false)
    this.changeTabbar(false)
  },
  beforeUnmount(){
    // this.$store.state.isTabbarShow = true
    // this.$store.commit("showTabbar") // commit()的内容是在index.js中的mutations
    // this.$store.commit("changeTabbar",true)
    this.changeTabbar(true)
  },
  mounted(){
    console.log('接收上一个页面传来的参数',this.$route.params.plid)
  },
  methods:{
    ...mapMutations(['changeTabbar']),
    plBackClick(){
      this.$router.back()   // 返回  this.$router.forward()
    }
  }
}
</script>

mapActions

修改Recommend.vue,增加methods引入mapActions取代$store.dispatch()这种复杂的写法。

<template>
  <div>
    <select v-model="type">
      <option :value="1">App订票</option>
      <option :value="0">前台兑换</option>
    </select>
    <ul>
      <li v-for="data in $store.getters.filterRecommendList(type)" :key="data.cinemaId">
        {{data.name}}
      </li>
    </ul>
  </div>
</template>
<script>
import { mapActions, mapState } from 'vuex';

export default{
  data(){
    return{
      type:1
    }
  },
  mounted(){
    // if(this.$store.state.recommendList.length===0){
    if(this.recommendList.length===0){
      // this.$store.dispatch("getRecommendList")
      this.getRecommendList()
    }else{
      console.log("缓存")
    }
  },
  methods:{
    ...mapActions(["getRecommendList"])
  },
  computed:{
    ...mapState(["recommendList"])
  }
  // computed:{
  //   filterRecommendList(){
  //     return this.$store.state.recommendList.filter(item=>item.eTicketFlag===this.type)
  //   }
  // }
}
</script>
<style scoped lang="scss">
ul{
  li{
    padding:10px;
  }
}
</style>

mapGetters

修改Recommend.vue,增加computed引入mapGetters取代$store.getters.filterRecommendList(type)这种复杂的写法。

<template>
  <div>
    <select v-model="type">
      <option :value="1">App订票</option>
      <option :value="0">前台兑换</option>
    </select>
    <ul>
      <!-- <li v-for="data in $store.getters.filterRecommendList(type)" :key="data.cinemaId"> -->
      <li v-for="data in filterRecommendList(type)" :key="data.cinemaId">
        {{data.name}}
      </li>
    </ul>
  </div>
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';

export default{
  data(){
    return{
      type:1
    }
  },
  mounted(){
    // if(this.$store.state.recommendList.length===0){
    if(this.recommendList.length===0){
      // this.$store.dispatch("getRecommendList")
      this.getRecommendList()
    }else{
      console.log("缓存")
    }
  },
  methods:{
    ...mapActions(["getRecommendList"])
  },
  computed:{
    ...mapState(["recommendList"]),
    ...mapGetters(["filterRecommendList"])
  }
  // computed:{
  //   filterRecommendList(){
  //     return this.$store.state.recommendList.filter(item=>item.eTicketFlag===this.type)
  //   }
  // }
}
</script>
<style scoped lang="scss">
ul{
  li{
    padding:10px;
  }
}
</style>

Module

当应用比较庞大时,store对象就有可能变得很臃肿。为了解决这个问题,Vuex允许将store分割成模块(module)。每个模块拥有自己的state、mutation、action、getter等,甚至是嵌套子模块。

可以看到示例中store/index.js中定义了两个状态isTabbarShowrecommendList,这里分割成两个module

const store = createStore({
  state(){
    return{
      isTabbarShow:true,
      recommendList:[]
    }
  }

在src/VueRouter/store/下新建module目录,分别创建RecommendModule.js和TabbarModule.js,并在这两个文件中开启命名空间

RecommendModule.js

import axios from "axios";

const RecommendModule = {
  namespaced: true, // 开启命名空间
  state(){
    return{
      recommendList:[]
    }
  },
  mutations:{
    changeRecommendList(state, payload){
      state.recommendList = payload
    }
  },
  actions:{
    async getRecommendList(store){
      let res = await axios({
        url:"https://m.maizuo.com/gateway?cityId=110100&ticketFlag=1&k=112132",
        headers:{
          'X-Client-Info':'{"a":"3000","ch":"1002","v":"5.2.1","e":"16931924276063063597842433"}',
          'X-Host':'mall.film-ticket.cinema.list'
        }
      })
      // console.log(res.data.data.cinemas)
      // 提交mutation
      store.commit("changeRecommendList",res.data.data.cinemas)
    }
  },
  // store的计算属性 (computed)
  getters:{
    filterRecommendList(state){
      return (type)=>{
        return state.recommendList.filter(item=>item.eTicketFlag===type)
      }
    }
  }
}

export default RecommendModule

TabbarModule.js

const TabbarModule = {
  namespaced: true, // 开启命名空间
  state() {
    return {
      isTabbarShow: true,
    }
  },
  mutations:{
    changeTabbar(state, payload){
      state.isTabbarShow = payload
    }
  }
}
  

export default TabbarModule

修改index.js

import { createStore } from "vuex";
import TabbarModule from "./module/TabbarModule"
import RecommendModule from "./module/RecommendModule"

const store = createStore({
  modules:{
    TabbarModule,
    RecommendModule
  }
})

export default store

修改App.vue

<template>
  <div>
    <!-- 通过插槽插入子组件路由 -->
    <router-view></router-view>
    <!-- 增加导航 -->
    <!-- 增加v-show判断 -->
    <Tabbar v-show="isTabbarShow"></Tabbar> 
  </div>
</template>

<script>
import { mapState } from 'vuex';
import Tabbar from './components/Tabbar.vue';
export default {
  components:{
    Tabbar
  },
  mounted(){
    console.log(this.$store.state.TabbarModule.isTabbarShow)
    console.log(this.$store.state.RecommendModule.recommendList)
  },
  // computed:{
  //   isTabbarShow(){
  //     return this.$store.state.isTabbarShow
  //   }
  // }
  computed:{
    ...mapState('TabbarModule',['isTabbarShow'])
  }
}
</script>

<style>
*{
  margin: 0;
  padding: 0;
}
ul{
  list-style: none;
}
</style>

修改Recommend.vue

<template>
  <div>
    <select v-model="type">
      <option :value="1">App订票</option>
      <option :value="0">前台兑换</option>
    </select>
    <ul>
      <!-- <li v-for="data in $store.getters.filterRecommendList(type)" :key="data.cinemaId"> -->
      <li v-for="data in filterRecommendList(type)" :key="data.cinemaId">
        {{data.name}}
      </li>
    </ul>
  </div>
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';

export default{
  data(){
    return{
      type:1
    }
  },
  mounted(){
    // if(this.$store.state.recommendList.length===0){
    if(this.recommendList.length===0){
      // this.$store.dispatch("getRecommendList")
      this.getRecommendList()
    }else{
      console.log("缓存")
    }
  },
  methods:{
    ...mapActions('RecommendModule',["getRecommendList"])
  },
  computed:{
    ...mapState('RecommendModule',["recommendList"]),
    ...mapGetters('RecommendModule',["filterRecommendList"])
  }
  // computed:{
  //   filterRecommendList(){
  //     return this.$store.state.recommendList.filter(item=>item.eTicketFlag===this.type)
  //   }
  // }
}
</script>
<style scoped lang="scss">
ul{
  li{
    padding:10px;
  }
}
</style>

VCA下的Vuex

由于组合式API中不能使用this,所有类似this.$store在VCA下可以通过调用useStore函数来实现,与this.$store等效。

App.vue

<template>
  <div>
    <!-- 通过插槽插入子组件路由 -->
    <router-view></router-view>
    <!-- 增加导航 -->
    <Tabbar v-show="store.state.TabbarModule.isTabbarShow"></Tabbar>
    <!-- <Tabbar></Tabbar> -->
  </div>
</template>

<script setup>
import Tabbar from './components/Tabbar.vue';
import { useStore } from 'vuex';
const store = useStore()  // 相当于this.$store
// export default {
//   components:{
//     Tabbar
//   }
// }
</script>

<style>
*{
  margin: 0;
  padding: 0;
}
ul{
  list-style: none;
}
</style>

Detail.vue

<template>
  <div>
    <button @click="plBackClick">返回</button>
    detail
  </div>
</template>
<script setup>
import {onMounted, onBeforeMount,onBeforeUnmount} from 'vue'
import { useRoute,useRouter } from 'vue-router'
import { storeKey, useStore } from 'vuex';
const route = useRoute()
const router = useRouter()
const store = useStore()

onBeforeMount(()=>{
  store.commit("TabbarModule/changeTabbar", false)
})

onBeforeUnmount(()=>{
  store.commit("TabbarModule/changeTabbar", true)
})


onMounted(()=>{
  console.log('接收上一个页面传来的参数',route.params.plid)
})
const plBackClick = ()=>{
  router.back() // 返回  router.forward()
}
</script>

Recommend.vue

<template>
  <div>
    <select v-model="type">
      <option :value="1">App订票</option>
      <option :value="0">前台兑换</option>
    </select>
    <ul>
      <li v-for="data in store.getters['RecommendModule/filterRecommendList'](type)" :key="data.cinemaId">
        {{data.name}}
      </li>
    </ul>
  </div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { useStore } from 'vuex'

const type =ref(1)
const store = useStore()

onMounted(()=>{
  if(store.state.RecommendModule.recommendList.length===0){
      // store.getRecommendList()
      store.dispatch("RecommendModule/getRecommendList")
    }else{
      console.log("缓存")
    }
})

</script>
<style scoped lang="scss">
ul{
  li{
    padding:10px;
  }
}
</style>

Vuex持久化插件

vuex可以进行全局的状态管理,但刷新后数据就会丢失掉,而有时候我们需要进行本地存储而使数据状态持久化。这里使用vuex-persistedstate来实现存储到localStorage中。

npm install --save vuex-persistedstate

修改src/VueRouter/store/index.js

import { createStore } from "vuex";
import TabbarModule from "./module/TabbarModule"
import RecommendModule from "./module/RecommendModule"
import createPersistedState from 'vuex-persistedstate'

const store = createStore({
  // plugins:[createPersistedState()],  // 全部保存
  plugins:[createPersistedState({
    // reducer:(state)=>state.TabbarModule.isTabbarShow  //只保存isTabbarShow的状态
    reducer:(state)=>{
      return {
        isTabbarShow:state.TabbarModule.isTabbarShow
      }
    }

  })],
  modules:{
    TabbarModule,
    RecommendModule
  }
})

export default store

文章作者: 老百
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 老百 !
 上一篇
前端框架Vue(七) 前端框架Vue(七)
Vue 是一款用于构建用户界面的 JavaScript 框架。本小节介绍Pinia,Pinia是Vue的专属状态管理库,它允许跨组件或页面共享状态,对组合式API有更好的支持。
2023-09-07
下一篇 
前端框架Vue(五) 前端框架Vue(五)
Vue 是一款用于构建用户界面的 JavaScript 框架。本小节介绍Vue Router,是Vue.js的官方路由,与Vue.js深度集成,让用Vue.js构建单页面应用变得轻而易举。
2023-08-24
  目录