【VUE3】如何使用VUEX的基本套路和文件架构
文件目录
之前介绍过《如何在项目中快速的搭建并使用VUEX?》那个是在vue2环境下。今天来说一下如何在vue3中搭建vuex文件目录和使用。
src
|-- store
|-- actions.js 对多个mutation的提交操作进行封装
|-- mutation.js 定义修改的操作相当于set
|-- getters.js 获取vuex-state中的值
|-- state.js 定义基本的属性
|-- index.js 集合所有文件
|-- main.js
|-- components
|-- demo.vue
需要先安装vuex,参考https://vuex.vuejs.org/zh/installation.html
再来就是了解一下vuex的核心概念:https://vuex.vuejs.org/zh/guide/state.html#单一状态树
可以吧vuex理解成为一个前端的数据库,用于存放临时的数据:服务器->发送数据->vuex接收到后->按照state中的设定进行保存,并定义一系列mutation进行提交、修改state中的字段,以及定义getters获取state中字段的值,最后还定义了actions封装多个mustation操作,只用一个操作就可以修改多个mutation。
文件定义以及使用
以我目前正进行的一个项目为例子进行说明。
state.js
首先定义各种状态,并给各种状态赋予一个默认的值。
// 定义全局的基础数据 定义播放器的基本状态
import { PLAY_MODE } from '@/assets/js/constant'
const state = {
// 原始播放列表 歌曲列表
sequenceList: [],
// 真正的播放列表,因为有可能歌曲是顺序 逆序 倒序
playlist: [],
// 是否正在播放
playing: false,
// 播放模式 定义到常量文件中
playMode: PLAY_MODE.sequence,
// 当前播放的歌曲
currentIndex: 0,
// 播放器的状态 全屏还是收缩
fullScreen: false
}
export default state
其中PLAY_MODE仅仅是常量,代表播放状态:
export const PLAY_MODE = {
// 顺序播放
sequence: 0,
// 循环播放
loop: 1,
// 随机播放
random: 2
}
getters.js
接下来定义状态state各个字段的获取方法,以一个为例。
// 定义一些获取数据的方法
// state中有currentIndex了 那么这里就可以获取当前播放的歌曲
// getters函数第一个参数就可以拿到state 通过当前播放列表拿到当前歌曲
export const currentSong = (state) => {
// 当取值不存在时,返回一个空对象 避免程序报错
return state.playlist[state.currentIndex] || {}
}
getters函数中第一个参数,就可以拿到state对象。固定用法。
mutations.js
有了get,怎么能没有post。这里定义一些修改state的操作方法。
// 提交数据 对state数据做修改
const mutations = {
// 修改播放状态 第一个参数是state, 第二个参数是要修改为的值
setPlayingState(state, playing) {
state.playing = playing
},
// 设置顺序(原始)播放列表
setSequenceList(state, list) {
state.sequenceList = list
},
// 设置播放列表 当有随机播放情况的时候,playlist和sequencelist就会不同
setPlaylist(state, list) {
state.playlist = list
},
// 设置播放模式
setPlayMode(state, mode) {
state.playMode = mode
},
// 设置当前播放索引
setCurrentIndex(state, index) {
state.currentIndex = index
},
// 设置播放器全屏或者折叠模式
setFullScreen(state, fullScreen) {
state.fullScreen = fullScreen
}
}
export default mutations
同样的,mutations方法中的第一个参数固定是state对象。第二个参数就是要赋予的值。
mutations方法中的方法要与state中的字段属性一一对应。
actions.js
正常情况下,我们一个操作,可能会同时修改多个state中的属性,那么就意味着一个操作执行多个mutation中的方法。所以定义一个action将多个方法封装成一个函数。
// 定义动作 对一个操作 可以操作多个mutations的一个封装
import { PLAY_MODE } from '@/assets/js/constant'
import { shuffle } from '@/assets/js/util'
// 定义选择播放的动作 action第一个参数是一个对象可以直接结构 第二个参数是他的参数
// { list, index } 传入一个当前的列表 和 点击的索引
// commit是提交的意思 提交mutation中定义的函数
export function selectPlay({ commit, state }, { list, index }) {
// 点击的时候顺序播放
commit('setPlayMode', PLAY_MODE.sequence)
commit('setSequenceList', list)
// 当点击播放的时候,那么播放状态一定是true
commit('setPlayingState', true)
// 播放的时候会全屏播放
commit('setFullScreen', true)
// 当不随机播放的时候,播放列表和顺序播放列表相同
commit('setPlaylist', list)
// 设置播放索引
commit('setCurrentIndex', index)
}
// 实现随机播放 这里的参数就不需要一个对象 直接一个参数即可
// 洗牌算法https://rosettacode.org/wiki/Knuth_shuffle
export function randomPlay ({ commit }, list) {
// 点击的时候顺序播放
commit('setPlayMode', PLAY_MODE.random)
commit('setSequenceList', list)
// 当点击播放的时候,那么播放状态一定是true
commit('setPlayingState', true)
// 播放的时候会全屏播放
commit('setFullScreen', true)
// 当不随机播放的时候,播放列表和顺序播放列表相同
commit('setPlaylist', shuffle(list))
// 设置播放索引,洗牌后播放第一首歌
commit('setCurrentIndex', 0)
}
这里请忽略shuffle乱序方法,有兴趣的可以点击查看。
这里说一下action中的参数。
- 第一个参数{ commit, state } 中的commit()是提交方法。commit('setCurrentIndex',0)就是自动执行mutations.js中的setCurrentIndex(state, index)方法,commit方法的第二个参数就是setCurrentIndex()中要传入的参数index:setCurrentIndex(state, 0)
- 第一个参数中的state就是状态state对象,但是切记,所有的state状态的修改,必须要通过mutation中的方法来执行。
- 第二个参数{ list, index } 就是传入要修改值。这里如果传入多个值,最好使用一个对象传入。如果仅仅一个值那么就无需使用对象。如:randomPlay({ commit }, list)
- 如果要在actions中不通过{ commit, state }参数,直接访问state的话,那么就使用this.state.xxx属性去访问【在vuex的index,js中,this指代的是store,因此如果要在actions和mutations中使用state中的数据,需要写成this.state.xxx,而不是this.xxx】
index.js
所有文件引用,导出,对vuex的配置。
// createLogger 开发环境下可以查看状态
import { createStore, createLogger } from 'vuex'
import state from './state'
import mutations from './mutations'
// 引入文件中所有的方法
import * as getters from './getters'
import * as actions from './actions'
const debug = process.env.NODE_ENV !== 'production'
export default createStore({
state: state,
mutations: mutations,
// 可以同下方这样写 也可以与上面一样写
getters,
actions,
// vuex提供了严格模式,
// 帮忙检测state修改是否是在提交mutation的时候,如果不是,就会报错,
// 开启时候会深度watch,所以线上模式就关闭这个模式
strict: debug,
// 插件,如果线上模式就不用插件 线下模式使用插件
plugins: debug ? [createLogger()] : []
})
main.js
下面就需要在main.js中注册了。
import App from './App.vue'
import router from './router'
// 引入vuex的配置和设定
import store from './store'
// 引入全局样式文件,注意不能加双引号
import '@/assets/scss/index.scss'
createApp(App)
.use(store)
.use(router)
.mount('#app')
在组件中使用
使用actions.js中的方法
在demo.vue中:
<div @click="selectItme({song, index})" v-for="(song, index) in songs">...</div>
<script type="text/ecmascript-6">
// 使用vuex中的actions方法,将其在methods中注册
import { mapActions } from 'vuex'
export default {
name: 'demo',
data() {
return {
songs: [...]
}
},
methods: {
// 对传入的对象进行解构赋值
selectItme({ song, index }) {
// 传入播放列表和点击的索引
this.selectPlay({
list: this.songs,
index
})
},
// 参照上面的actions中的方法查看对比
random() {
this.randomPlay(this.songs)
},
// 注册后,就可以在methods中直接调用这个方法了 this.xxx()
...mapActions([
'selectPlay',
'randomPlay'
])
}
}
</script>
使用getters中的方法
<h1 class="title">{{currentSong.name}}</h1>
import { mapGetters } from 'vuex';
computed: {
...mapGetters([
'currentSong'
])
},
从vuex中获取mapGetters方法,然后在computed中进行注册,引入需要的方法,就可以直接在模板中进行渲染了。
调用mutation中的方法
你可以在组件中使用this.$store.commit('xxx')
提交 mutation,或者使用mapMutations
辅助函数将组件中的 methods 映射为store.commit
调用(需要在根节点注入store
)。
import { mapMutations } from 'vuex'
methods: {
...mapMutations([
// 将 `this.increment()` 映射为 `this.$store.commit('increment')`
'increment',
// 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
'incrementBy'
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})
}
调用state中的数据
在计算属性中, 可以通过mapState直接获取state中的数据:
import { mapState } from 'vuex'
computed: {
xxx() {
return this.playlist.length
},
...mapState([
'playlist'
])
},
在普通的JS文件中使用
直接引入使用即可,如下: 更详细的使用《vue3项目中在普通js文件里获取vuex里的值和方法》
import store from './store.js';
console.log(store.state.data);
store.commit('xxx');
// 其中 store.commit 中的xxx是 mutation.js中的方法 比如 与action.js中相同
store.commit(setCurrentIndex', index);
获取getters中的值
store.getters["getters文件中export的属性"]
或者使用
store.getters.xxx
store.getters.currentSong 或者 store.getters['currentSong']
获取state中的值
store.state.name:store.state.TestData
使用mutations(定义在mutations.js中的方法):
store.commit('set_TestData', data)
使用actions(定义在action.js中的方法):
store.dispatch(‘method', data)
store.dispatch('TestDatafun', data)
当在js文件中使用vuex,取出的对象是proxy时
在使用vuex的时候发现获取state的stringnumber类型的变量时,是可以直接通过store.state.value 取到值的,但是在取对象的时候,控制台打印出来的是Proxy 对象,Proxy对象里边的[[Target]]才是真实的对象。
//第一种获取target值的方式,通过vue中的响应式对象可使用toRaw()方法获取原始对象
import { toRaw } from '@vue/reactivity'
var list = toRaw(store.state.sequenceList)
//第二种获取target值的方式,通过json序列化之后可获取值
JSON.parse(JSON.stringify(store.getters.sequenceList))
更新:最近遇到一个在使用toRaw时遇到的小问题,使用这个方法会导致数据更新但视图不更新,从vue里面取出来的虽然是proxy对象,但是也可以直接进行数组的操作,如splice,数组拓展等
在组合API中使用vuex
在组合API中使用vuex,就需要在setup钩子函数中进行手动调用。
比如如下模板结构(setup中使用组合API方法,点击查看):
<template>
<div class="player">
<div
class="normal-player"
v-show="fullScreen"
>
<!-- 使用v-if避免currentSong没有值的时候被渲染 -->
<!-- 使用template避免产生多余的dom层 -->
<template v-if="currentSong">
<div class="background">
<img :src="currentSong.pic">
</div>
<div class="top">
<div
class="back"
@click="goBack"
>
<i class="icon-back"></i>
</div>
<h1 class="title">{{currentSong.name}}</h1>
</div>
</template>
</div>
<audio
ref="audioRef"
></audio>
</div>
</template>
下面是JS部分
// vuex提供useStore专门从vuex中拿数据的方法
import { useStore } from 'vuex'
// 如果需要在组合api中使用计算监听 那么就要单独引入
import { computed, watch, ref } from 'vue'
export default {
// 对于复杂的组件 将其中的各个逻辑进行拆分成不同文件
setup() {
// 获取的时候要先定义一个ref对象
const audioRef = ref(null)
// 这个store可以理解为createStore中返回的实例,其中含有state、getters等属性
const store = useStore()
// setup中的响应式需要自己去处理,所以要使用computed()这个API
// 让state.fullScreen变成一个响应式的
// 记得必须要返回转化的计算属性
const fullScreen = computed(() => store.state.fullScreen)
const currentSong = computed(() => store.getters.currentSong)
// 监听currentSong的属性变化,如果变化了就重新给audio赋值
watch(currentSong, (newSong) => {
if (!newSong.id || !newSong.url) {
return
}
// 通过audioRef.value获取到audio的DOM元素
const audioEl = audioRef.value
audioEl.src = newSong.url
audioEl.play()
})
// 通过store.commit方法,提交一个修改操作
function goBack() {
store.commit('setFullScreen', false)
}
// 只有返回出去 才能被模板获取到
return {
audioRef,
fullScreen,
currentSong,
// 返回方法,模板中可以使用goback()
goBack
}
}
}
上面的结构有点乱,我们来简单梳理下:
获取vuex中的实例(所有一切的前提)
// vuex提供useStore专门从vuex中拿数据的方法
import { useStore } from 'vuex'
setup(){
// 这个store可以理解为createStore中返回的实例,其中含有state、getters等属性
const store = useStore()
}
获取vuex中的state和getters
// setup中的响应式需要自己去处理,所以要使用computed()这个API
// 让state.fullScreen变成一个响应式的
// 记得必须要返回转化的计算属性
import { computed } from 'vue'
// 直接返回 state中字段的属性
const fullScreen = computed(() => store.state.fullScreen)
// 执行vuex中的getters方法
const currentSong = computed(() => store.getters.currentSong)
// 只有返回出去 才能被模板获取到
return {
fullScreen,
currentSong
}
执行mutations
setup() {
// 通过store.commit方法,提交一个修改操作
function goBack() {
store.commit('setFullScreen', false)
}
// 只有返回出去 才能被模板获取到
return {
// 返回方法,模板中可以使用goback()
goBack
}
}
通过store.commit执行mutations中定义的方法,并对其传参。
使用watch监听并获取元素DOM
import { watch, ref } from 'vue'
setup() {
// 获取的时候要先定义一个ref对象
// 注意 也要在模板元素上加 <audio ref="audioRef">
const audioRef = ref(null)
// 监听currentSong的值变化,如果变化了就重新给audio元素赋值
watch(currentSong, (newSong) => {
if (!newSong.id || !newSong.url) {
return
}
// 通过audioRef.value获取到audio的DOM元素 固定套路
const audioEl = audioRef.value
audioEl.src = newSong.url
audioEl.play()
})
}
在setup中获取计算属性的值
<i :class="playIcon"></i>
import { computed } from 'vue'
import { useStore } from 'vuex'
setup() {
const store = useStore()
// 从vuex中取出state.playing字段
const playing = computed(() => store.state.playing)
// 因为playing是一个计算属性,所以在js中要通过playing.value获取它的值
const playIcon = computed(() => {
return playing.value ? 'icon-pause' : 'icon-play'
})
return {
playIcon
}
}
这里有个疑问:为什么需要用playing.value进行判断呢?之前判断player是否显示的fullscreen都是直接用的。
答:因为 playing 是一个计算属性,在 JS 中需要通过 .value 获取它的值。
在setup中使用actions
这里从不同文件角度进行分析:
在player.vue中:
// player.vue
<i @click="changeMode"></i>
import useMode from './use-mode'
setup() {
const { changeMode } = useMode()
return {
// 将方法返回给模板
changeMode
}
}
在use-mode.js文件中:
// use-mode.js文件
import { useStore } from 'vuex'
import { computed } from 'vue'
export default function useMode() {
const store = useStore()
const playMode = computed(() => store.state.playMode)
function changeMode () {
// 这样取值结果值就在0,1,2之间
// 在js中获取计算属性的值要使用.value
const mode = (playMode.value + 1) % 3
// 执行一个actions-changeMode,传递参数mode
store.dispatch('changeMode', mode)
}
// 注意 这里返回的必须是一个对象!这样接收的时候使用解构赋值接收
return {
changeMode
}
}
这里执行vuex中的actions中封装的方法,要使用:
const store = useStore()
store.dispatch('actions方法名', 参数);
再来看看actions中封装的changeMoe方法(actions就是封装了多个Mutations修改方法的函数):
// getters也可以被解构 commit是可以提交mutation的方法, state是定义的数据字段状态
export function changeMode({ commit, state, getters }, mode) {
// 先从getters中拿到当前正在播放歌曲(对象)的Id
const currentId = getters.currentSong.id
// 设置提交Mutation,通过mutation方法修改state中的属性
commit('setPlaylist', state.sequenceList)
// 在修改后的playlist中找到这个ID对应的这首歌,找到它的索引之后再提交commit
const index = state.playlist.findIndex((song) => {
return song.id === currentId
})
// 修改当前正在播放的歌曲的索引
commit('setCurrentIndex', index)
// 修改播放模式
commit('setPlayMode', mode)
}
如果watch监听vuex中属性的变化
使用mapState
我们可以在组件中通过组件的 watch方法来做, 因为组件可以将state数据映射到 组件的计算属性上,
// vuex中的state数据
state: {
count: 0
},
// A组件中映射 state数据到计算属性
computed: {
// this.$store.state.count
// mapState 把全局 count 变成 可以直接使用的 数据
...mapState(['count'])
}
// A组件监听 count计算属性的变化
watch: {
// watch 可以监听 data 数据 也可以监听 全局 vuex数据
count () {
// 用本身的数据进行一下计数
this.changeCount++
}
}
store的watch函数
vuex中store对象本身提供了watch函数 ,可以利用该函数进行监听
响应式地侦听 fn 的返回值,当值改变时调用回调函数。fn 接收 store 的 state 作为第一个参数,其 getter 作为第二个参数。最后接收一个可选的对象参数表示 Vue 的 vm.$watch 方法的参数。
watch(fn: Function, callback: Function, options?: Object): Functionvuex的实例方法,接收两个参数:第一个参数为fn,响应式地侦听 fn 的返回值,当值改变时调用回调函数。fn 接收 store 的 state 作为第一个参数,其 getter 作为第二个参数;第二个参数为一个可选的对象参数表示 ,类似于vue的watch的回调函数,表示新旧值。当我们不想监听时,可以通过定义变量接收该方法的返回值函数,调用该函数即可停止监听。
created () {
this.$store.watch((state, getters) => {
return state.count
}, () => {
this.changeCount++
})
}
x如果是在js文件中,那么就如下使用
/* eslint-disable */
import store from "../store/index";
const watchFun = store.watch(
state => state.pathName,
(newValue, oldValue) => {
console.log("search string is changing");
console.log("rd: newValue", newValue);
console.log("rd: oldValue", oldValue);
}
);
setTimeout(() => {
watchFun();
}, 10000);
调用store的watch实例方法,第一个函数参数返回一个需要监听的state中的值(比如我想监听vuex中的pathName的变化情况,就返回该值)
第二个参数同vue的watch,接收2个参数代表新旧值
通过变量watchFun接收watch的返回值,调用该方法会停止监听
监听store中数据变化的两种方式
方式一
watch: {
"$store.state.userInfo.Name":{
handler:function(newVal,oldVal){
console.log(newVal,oldVal);
}
}
}
方式二
computed: {
isEdit () {
return this.$store.state.userInfo.Name; //需要监听的数据
}
},
watch: {
isEdit(newVal, oldVal) {
console.log(newVal,oldVal);
}
},
- 第二种方式是先通过计算属性时刻监测store的数据变化,从而触发isEdit的监听函数,明显需要多一步
- 如果监听store的数据是一个对象,第一种方式只需要加上深度监听,也可以实现数据的变化监听,而第二种方式却无法监听到store的对象数据变化
watch: {
//此时我监听的是对象,当$store.state.userInfo.Name发生修改时,此时需要深度监听才能监听到数据变化
"$store.state.userInfo":{
deep:true,//深度监听设置为 true
immediate: true, // 刚开始就监听
handler:function(newVal,oldVal){
console.log("数据发生变化啦"); //修改数据时,能看到输出结果
console.log(newVal,oldVal);
}
}
}
computed: {
isEdit () {
return this.$store.state.userInfo; //需要监听的数据
},
},
watch: {
isEdit(newVal, oldVal) {
console.log("数据发生变化啦"); //修改数据时,看不到该输出结果,因为无法监听到数据的变化
console.log(newVal,oldVal);
}
},