vuex简单使用、监听state值变化(设置主题色)

3,637 阅读5分钟

vuex主要是vue的状态管理,如果我们在项目中只通过传参、缓存等方式来同步data中的值,一旦项目变得大了页面多并且很复杂的时候就会变得很难维护和管理。vuex就把我们频繁使用的值进行集中管理,可以在整个项目中共同使用

  • state:存储状态(变量)。使用:$sotre.state.xxx

  • getters:可以理解为state的计算属性。加工state成员给外界。使用:$sotre.getters.functionName()

  • mutations:修改状态,并且是同步的。可以用来修改state中的状态。使用:$store.commit(state,payload)

  • actions:异步操作。在组件中使用是$store.dispath(context,payload)

  • modules:store的子模块,为了开发大型项目,方便状态管理而使用的。

安装vue官方的调试工具插件:VueDevtools,调试起来很方便

1、安装vuex、初始化

npm i vuex -s

1、根目录下新建store文件夹,里面再新建store.js并对vuex进行初始化,项目大的时候,state、getters、mutations、actions每个都是一个单独的文件,易于维护

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

Vue.use(Vuex)
export default new Vuex.Store({
    state: {
    
    },
    getters: {
        
    },
    mutations: {
        
    },
    actions: {
        
    }
})

2、将vuex挂载到全局main.js中

import Vue from 'vue'
import store from './store'
import router from './router'
import { Button, ConfigProvider } from 'ant-design-vue'
import 'ant-design-vue/dist/antd.min.css'
import app from './src/App'
Vue.config.productionTip = false

Vue.use(Button)
Vue.use(ConfigProvider)

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

2、State

我们可以简单的的把他理解为vue组件中的data和小程序中的globalData,在state里面定义我们想要共享的一些数据

用户登录之后后台返回给我们用户信息,然后我们把他存放到vuex的state中(这里为了方便就直接再state写入了,到时候可以用this.$store.commit(state,payload)来向state中注入共享的数据)

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

Vue.use(Vuex)
export default new Vuex.Store({
    state: {
        userInfo: {
            userId: "1286507476287508480",
            name: "测试姓名",
            nickname: "pigs",
            openId: "ogxJ55E2WqZ2-vyK2mlDtEXG28A4shvruybpohpsde",
            portraitUrl: "https://proxy01.hnchengyue.com/image/portrait/1286507476253954048.jpg?v=4b487d14-9580-443b-9632-fa9615a2812b",
            qrCode: "",
            regNo: null,
            sex: "男",
            birthday: null,
            employeeState: 1,
            gmtCreate: null,
            gmtModified: null,
        },
        commonInfo: 'commonInfo is a string'
    },
    getters: {
        
    },
    mutations: {
        
    },
    actions: {
        
    }
})

在组件中这样使用

<template>
  <div>
  	login页面
    <div>这是VueX中的信息:{{$store.state.userInfo.name}}</div>
  </div>
</template>

<script>
import { mapState } from 'vuex'
  import httpUtil from '../utils/request'
  export default {
    data () {
      return {

      }
    },
    mounted() {
    	console.log('这是vuex中的userInfo:',this.$store.state.userInfo)
    }
  }

</script>

在页面中也可以获取到vuex中的用户信息

在这里插入图片描述

VueDevtools中的数据也是正确的

在这里插入图片描述

当我们的组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余,为了解决这个问题,我们可以使用mapState的辅助函数来帮助我们生成计算属性,mapState接收的是一个数组

import { mapState } from 'store/index.js'

...mapState(['aaa','bbb'])

3、Getters

  1. 如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。 
  2. Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。 
  3. getters接收2个参数,第一个参数是:当前VueX对象中的状态对象;第二个参数是:当前getters对象,用于将getters下的其他getter拿来用

现在我们除了userInfo又加了一条共享的数据:commonInfo,这条数据我们就可以通过getters来访问到

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

Vue.use(Vuex)
export default new Vuex.Store({
    state: {
        userInfo: {
            userId: "1286507476287508480",
            name: "测试姓名",
            nickname: "pigs",
            openId: "ogxJ55E2WqZ2-vyK2mlDtEXG28A4shvruybpohpsde",
            portraitUrl: "https://proxy01.hnchengyue.com/image/portrait/1286507476253954048.jpg?v=4b487d14-9580-443b-9632-fa9615a2812b",
            qrCode: "",
            regNo: null,
            sex: "男",
            birthday: null,
            employeeState: 1,
            gmtCreate: null,
            gmtModified: null,
        },
        commonInfo: 'commonInfo is a string'
    },
    getters: {
        getUserInfo(state) {
            return state.userInfo;
        },
        getCommonInfo(state,payload){
            return `共享的数据:${state.commonInfo}`
        }
    }
})

在组件中这样使用:通过this.$store.getters.xxx访问

<template>
  <div>
  	login页面
    <div>这是VueX中的信息:{{$store.state.userInfo.name}}</div>
  </div>
</template>

<script>
import { mapState } from 'vuex'
  import httpUtil from '../utils/request'
  export default {
    data () {
      return {

      }
    },
    mounted() {
    	console.log('这是vuex中的userInfo:',this.$store.state.userInfo)
    	//或者console.log('这是vuex中的userInfo:',this.$store.getters.getUserInfo)
    }
  }

</script>

在这里插入图片描述

在这里插入图片描述

Getters中的第二个参数可以将getters下的其他getter拿来用

getters:{
    getUserInfo(state){
        return `姓名:${state.userInfo.name}`
    },
    getCommonInfo(state,payload){
        return `姓名:${payload.getUserInfo};数据${state.commonInfo}`
    }  
}

在这里插入图片描述

4、Mutation

如果有个用户修改了他的姓名,现在我们要同步state中的数据就可以用Mutation来进行操作,也可以用来删除、添加

注意:

  • 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation
  • mutation是一个同步的操作

每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state(当前VueX对象中的state) 作为第一个参数;payload(该方法在被调用时传递参数使用的)作为第二个参数

在login页面中有一个按钮,现在我们想点击按钮的时候更改userInfo的name字段

<template>
  <div>login页面
    <div>这是VueX中的信息:{{$store.state.userInfo.name}}</div>
    <a-button type="primary" @click="onClickBtn">
      Primary
    </a-button>
  </div>
</template>

<script>
  export default {
    data () {
      return {

      }
    },
    methods: {
      onClickBtn(){
        let userInfo = this.$store.state.userInfo
        userInfo.name = '新的测试名字'
        this.$store.commit('setUserInfo',{
          userInfo: userInfo
        })
        console.log('修改后的state数据:',this.$store.state.userInfo)
      }
    }
  }

</script>


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

Vue.use(Vuex)
export default new Vuex.Store({
    state: {
        userInfo: {
            userId: "1286507476287508480",
            name: "测试姓名",
            nickname: "pigs",
            openId: "ogxJ55E2WqZ2-vyK2mlDtEXG28A4shvruybpohpsde",
            portraitUrl: "https://proxy01.hnchengyue.com/image/portrait/1286507476253954048.jpg?v=4b487d14-9580-443b-9632-fa9615a2812b",
            qrCode: "",
            regNo: null,
            sex: "男",
            birthday: null,
            employeeState: 1,
            gmtCreate: null,
            gmtModified: null,
        },
        userId: '1212121',
        commonInfo: 'commonInfo is a string'
    },
    getters: {
        getUserInfo(state) {
            return state.userInfo;
        },
        getCommonInfo(state,payload){
            console.log("getters中的payload数据:",payload)
            // return `姓名:${payload.getUserInfo};数据${state.commonInfo}`
            return `共享的数据:${state.commonInfo}`
        }
    },
    mutations: {
        setUserInfo(state, payload) {
            console.log('mutations中的',payload.userInfo)
            state.userInfo = payload.userInfo
        }
    }
})

在这里插入图片描述

在这里插入图片描述

新增数据

Vue.set(state.userInfo,"age",15)

删除数据

Vue.delete(state.userInfo,'openId')

5、Actions

Action 类似于 mutation,不同在于:

Action 提交的是 mutation,而不是直接变更状态 Action 可以包含任意异步操作 

想要异步地更改状态,就需要使用action。action并不直接改变state,而是发起mutation。

Actions中的方法有两个默认参数

  • context 上下文(相当于箭头函数中的this)对象

  • payload 挂载参数

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

    Vue.use(Vuex) export default new Vuex.Store({ state: { userInfo: { userId: "1286507476287508480", name: "测试姓名", nickname: "pigs", openId: "ogxJ55E2WqZ2-vyK2mlDtEXG28A4shvruybpohpsde", portraitUrl: "proxy01.hnchengyue.com/image/portr…", qrCode: "", regNo: null, sex: "男", birthday: null, employeeState: 1, gmtCreate: null, gmtModified: null, }, userId: '1212121', commonInfo: 'commonInfo is a string' }, getters: { getUserInfo(state) { return state.userInfo; }, getCommonInfo(state,payload){ console.log("getters中的payload数据:",payload) // return 姓名:${payload.getUserInfo};数据${state.commonInfo} return 共享的数据:${state.commonInfo} } }, mutations: { setUserInfo(state, payload) { state.userInfo = payload.userInfo } }, actions: { aFun(context, payload){ console.log(context) console.log('actions中的',payload.userInfo) setTimeout(() => { context.commit('setUserInfo', payload) }, 2000) } } })

在组件中这样使用

<template>
  <div>login页面
    <div>这是VueX中的信息:{{$store.state.userInfo.name}}</div>
    <a-button type="primary" @click="onClickBtn">
      Primary
    </a-button>
  </div>
</template>

<script>
import { mapState } from 'vuex'
  import httpUtil from '../utils/request'
  export default {
    data () {
      return {

      }
    },
    mounted() {
      this.$store.dispatch('aFun',{
         userInfo: '哈哈哈哈哈'
      })
    },
  }

</script>
<style scoped>
</style>

在这里插入图片描述

在这里插入图片描述

我们可以用Promise或者async、await来封装一下

promise

actions: {
        aFun(context, payload){
            console.log(context)
            console.log('actions中的',payload.userInfo)
            return new Promise((resolve, reject) => {
                context.commit('setUserInfo', payload)
                resolve()
            })
        }
    }

async、await方式

假设 getData() 返回的是 Promise

actions: {
        async aFun ({ commit }) {
		    commit('setUserInfo', await getData())
		}
    }

6、Module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿 为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割

models: {
        modelA: {
            state:{},
            getters:{},
            actions: {}
        },

        modelB: {
            state:{},
            getters:{},
            actions: {}
        }
    }

在对应的页面中调用modelA的状态

this.$store.state.modelA

提交或者dispatch某个方法和以前一样,会自动执行所有模块内的对应type的方法

this.$store.commit('setUserInfo')
this.$store.dispatch('aFun')

对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。

models:{
    modelA:{
        state:{key:5},
        mutations:{
            editKey(state){
                state.key = 9
            }
        }
    }
}

同样,对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState

models:{
    modelA:{
    	state:{count:5},
        actions: {
		    incrementIfOddOnRootSum ({ state, commit, rootState }) {
		      if ((state.count + rootState.count) % 2 === 1) {
		        commit('increment')
		      }
		    }
	  }
    }
}

getters中方法的第三个参数是根节点状态

models:{
    modelA:{
        state:{key:5},
        getters:{
            getKeyCount(state,getter,rootState){
                return  rootState.key + state.key
            }
        },
        ....
    }
}

命名空间

默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。 

如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,

      // 模块内容(module assets)
      state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // 嵌套模块
      modules: {
        // 继承父模块的命名空间
        myPage: {
          state: () => ({ ... }),
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // 进一步嵌套命名空间
        posts: {
          namespaced: true,

          state: () => ({ ... }),
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

7、辅助函数:mapState、mapMutations、mapGetters等等等

mapState辅助函数

注意:mapState的属性的时候,一定要和state的属性值相对应,也就是说 state中定义的属性值叫add,那么mapState就叫add,如果我们改成add2的话,就获取不到add的值了,并且add2的值也是 undefined

<script>
  import { mapState } from 'vuex';
  export default {
    data() {
      return {
        
      }
    },
    methods: {
      
    },
    computed: {
        ...mapState({
          add: state => state.add,
          counts: state => state.counts
        })
    },
    mounted() {
      console.log(this.add)
      console.log(this.counts)
    }
  }
</script>

mapGetters 辅助函数

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性

import { mapGetters } from 'vuex'

export default {
  // ...
  computed: {
  // 使用对象展开运算符将 getter 混入 computed 对象中
    ...mapGetters([
      'getUserInfo',
      'getCommonInfo',
      // ...
    ])
  }
}

mapActions 辅助函数

import { mapActions } from 'vuex'

export default {
  // ...
  computed: {
  // mapActions 使用方法一 将 aFun() 映射为 this.$store.dispatch('aFun')
  ...mapActions(['aFun')

  //第二种方法
  ...mapActions({
         'aFun': 'aFun'
 })


}

mapMutations辅助函数

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations([
      'setUserInfo', // 将 `this.setUserInfo()` 映射为 `this.$store.commit('setUserInfo')`
	  'setCommonInfo',
    ]),
  }
}

8、规范目录结构

  1. 应用层级的状态应该集中到单个 store 对象中。
  2. 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
  3. 异步逻辑都应该封装到 action 里面。

如果 store 文件太大,只需将 action、mutation 和 getter 分割到单独的文件。这也是上面提到的

store
    ├── index.js          # 我们组装模块并导出 store 的地方
    ├── actions.js        # 根级别的 action
    ├── mutations.js      # 根级别的 mutation
    └── modules
        ├── cart.js       # 购物车模块
        └── products.js   # 产品模块

9、利用vuex实现简单的更换主题色小案例

在某些网站或者app中应该都见过,点击某个色块的时候,全局的某个地方的背景颜色都会变成一样的,就像下方我自己做的一个项目。话不多说上代码

项目是uniapp框架写的,其实和vue都差不多

在这里插入图片描述

首先俺们在vuex的state中定义好默认的主题色,很多时候都是从后台获取全局的配置,代码里面就简单的写死了,方便测试,getters和mutations也提前写好

在这里插入图片描述

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

Vue.use(Vuex)

const extConfig = uni.getExtConfigSync ? uni.getExtConfigSync() : {};

const store = new Vuex.Store({
	state: {
		shopName:'首页',
		phone:extConfig.phone,
		appid: extConfig.appid,
		token: extConfig.token,
		themeBgColor: extConfig.themeBgColor || '#00b000',//全局主题颜色
	},
	getters: {
		getThemeBgColor: (state, theme) => {
			console.log(state);
			console.log(theme); 
			return state.themeBgColor
		}
	},
	mutations: {
		setMerchant(merchant){
			state.merchant = merchant
		},
		setThemeBgColor: (state, theme) => {
			console.log(state, theme);
			state.themeBgColor = theme.themeBgColor
		}
	}
})

export default store

在main.js中引用vuex

在这里插入图片描述

在我的页面进行更换主题色

<script>
	var that;
	import { mapGetters } from 'vuex'
	import uniPopup  from '../../components/uni-popup/uni-popup.vue'
	export default {
		data() {
			return {
				globalTheme: '',
				// 主题色
				themeList: [
					{ text: 'Dim', color: '#41555d' },
					{ text: 'Grey', color: '#808080' },
					{ text: 'Dark', color: '#161a23' },
					{ text: 'Water', color: '#88ada6' },
					{ text: 'Blush', color: '#f9906f' },
					{ text: 'Orange', color: '#ff8936' },
					{ text: 'Indigo', color: '#27547d' },
					{ text: 'Volcano', color: '#fa541c' },
					{ text: 'Verdant', color: '#519a73' },
					{ text: 'PinkGold', color: '#f2be45' },
					{ text: 'Verdigris', color: '#549688' },
					{ text: 'DeepBlack', color: '#3d3b4f' },
					{ text: 'DarkGreen', color: '#75878a' },
					{ text: 'IndigoBlue', color: '#177cb0' },
					{ text: 'BluishGreen', color: '#48c0a3' },
				]
			}
		},
		components: { uniPopup },
		computed: {
			getGlobalTheme() {
				return this.$store.state.themeBgColor
			}
		},
		watch: {
			getGlobalTheme(val){
				console.log(val);
				this.globalTheme = val
				this.$store.commit('setThemeBgColor',{
					themeBgColor: val
				})
				uni.setNavigationBarColor({
					frontColor: '#ffffff',
				    backgroundColor: val,
				})
			}
		},
		methods: {
			//打开主题色弹出层
			openThemeModal(){
				this.$refs.popup.open()
			},
			//更换主题颜色
			onChangeTheme(item){
				this.themeBgColor = item.color
				this.$store.commit('setThemeBgColor',{
					themeBgColor: item.color
				})
				this.$refs.popup.close()
			}
		}
	}
</script>

更换主题色后需要监听vuex中state值的变化并给navbar设置我们选中的颜色。利用computedwatch来监听

  1. 首先我们在computed中写一个计算属性,返回状态管理中state的themeBgColor

  2. 然后在watch中监听这个计算属性的变化,对其重新赋值。

在购物车页面获取修改后的主题色

放在onShow生命周期里面进行监听

onShow() {
			console.log(this.$store.state.themeBgColor);
			uni.setNavigationBarColor({
				frontColor: '#ffffff',
			    backgroundColor: this.$store.state.themeBgColor,
			})
},
computed: {
			getGlobalTheme() {
				return this.$store.state.themeBgColor
			}
},
watch: {
			getGlobalTheme(val){
				console.log("购物车:",val);
				this.globalTheme = val
				// uni.setNavigationBarColor({
				// 	frontColor: '#ffffff',
				//     backgroundColor: val,
				// })
			}
},

10、利用vuex实现简单的音乐播放器全屏控制

上面案例我们vuex中的state、getters等等都是放在一个js文件里面的,现在我们把他们单独的分离出来,先看下效果,退出的动画还没来得及写

点击下方的小播放器也是可以展开的

只是简单的利用vuex控制一个音乐播放器的是否全屏、是否播放

index.js代码

创建好文件后先导入进来,不要忘记挂载到main.js

import Vue from 'vue'
import Vuex from 'vuex'
import * as getters from './getters.js'
import mutations from './mutations.js'
import state from './state.js'
import actions from './actions.js'

Vue.use(Vuex)
//考虑到有很多种方式进入到播放页面,所以我们用vuex来进行管理
const store = new Vuex.Store({
	// namespaced: true,
	state,
	getters,
	actions,
	mutations
})

export default store

state.js代码

对state进行一个init

import { playMode } from '../utils/playConfig.js'

const state = {
	isPlaying: false,//是否播放
	isFullScreen: false,//是否全屏
	playingList: [],//播放的列表,方便进行歌曲切换
	sequenceList: [],//顺序列表
	playingMode: playMode.sequence,//播放模式,默认是顺序播放
	curPlayMusicIndex: -1,//当前播放音乐的索引
}

export default state

getters.js代码

在getters中,我们把数据返回出去,最后通过mapGetters来获取

export const isPlaying = state => state.isPlaying

export const isFullScreen = state => state.isFullScreen

export const playingList = state => state.playingList

export const sequenceList = state => state.sequenceList

export const playingMode = state => state.playingMode

export const curPlayMusicIndex = state => state.curPlayMusicIndex

//当前正在播放的音乐
export const curPlayMusic = (state) => {
	return state.playingList[state.curPlayMusicIndex] || {}
}

mutations.js代码

import * as types from './mutationsTypes.js'

const mutations = {
	[types.setIsPlaying](state, flag) {
		state.isPlaying = flag
	},
	[types.setIsFullScreen](state, flag) {
		state.isFullScreen = flag
	},
	[types.setPlayingList](state, list) {
		state.playingList = list
	},
	[types.setSequenceList](state, list) {
		state.sequenceList = list
	},
	[types.setPlayingMode](state, mode) {
		state.playingMode = mode
	},
	[types.setCurPlayMusicIndex](state, index) {
		state.curPlayMusicIndex = index
	},
}

export default mutations

mutationsTypes只是一个type类型的文件

export const setIsPlaying = 'setIsPlaying'

export const setIsFullScreen = 'setIsFullScreen'

export const setPlayingList = 'setPlayingList'

export const setSequenceList = 'setSequenceList'

export const setPlayingMode = 'setPlayingMode'

export const setCurPlayMusicIndex = 'setCurPlayMusicIndex'

actions代码

所有的提交操作都交给actions来操作

import * as mutationsTypes from './mutationsTypes.js'

const actions = {
	selectPlayMusic:({commit, state}, {playingList, index})=> {
		// console.log(mutationsTypes)
		//提交顺序操作、播放列表、当前播放音乐索引、是否全屏的操作
		commit(mutationsTypes.setSequenceList, playingList)
		commit(mutationsTypes.setPlayingList, playingList)
		commit(mutationsTypes.setCurPlayMusicIndex, index)
		commit(mutationsTypes.setIsFullScreen, true)
		commit(mutationsTypes.setIsPlaying, true)
	}
}

export default actions

最后挂载到全局

import Vue from 'vue'
import App from './App'
import { httpUrl } from 'utils/config.js'
import toast from 'utils/toast.js'
import Util from 'utils/utils.js'
import store from 'store/index.js'

Vue.config.productionTip = false
Vue.prototype.$httpUrl = httpUrl
Vue.prototype.$toast = toast
Vue.prototype.$Util = Util
Vue.prototype.$store = store


App.mpType = 'app'

const app = new Vue({
    ...App,
	store
})
app.$mount()

播放组件代码

<script>
    import { mapGetters, mapMutations } from 'vuex'
    export default {
        data() {
	    return {
				
	    }
	},
        methods: {
            //隐藏主播放器
	    onClickHidePlayer(){
		this.isShowPlayer = false
		this.setIsFullScreen(false)
	    },
	    //打开主播放器
	    onClickOpenPlayer(){
		this.setIsFullScreen(true)
                //调用播放方法
		this.createAudioPlay()
	    },
            ...mapMutations([
                'setIsFullScreen'
	    ])
        },
        //监听getters
        computed: {
	    ...mapGetters([
		'isFullScreen', 'playingList', 'curPlayMusic'
	    ])
	},
    }
</script>