前言
vuex
作为跨组件通信的利器,在开发复杂应用过程中必不可少.但vuex
在组件中渲染状态和调用action
和mutation
的语法过于繁琐,严重影响开发效率.
本文借助vue3
提出的hook
思想对vuex
进行封装,使其使用起来更加快捷高效.从而使前端同学能更加关注业务本身,而少写样板代码(源代码贴在了文尾).
vuex的基础使用
配置
vuex 4
相当于3
版本没有多大的区别,4
版本增加了ts
的支持和Composition Api
.
配置之初,在项目根目录下执行命令安装vuex
.
yarn add vuex@next --save
新建src/store/index.ts
开始配置vuex
(代码如下).
createStore
函数用于创建仓库,根模块下定义了一个状态content
,并且配置了一个子模块HomePage
.
import { createStore } from 'vuex';
import { m as HomePage } from './modules/HomePage';
import { RootState } from './store';
export default createStore<RootState>({
state:{
content:"hello guy"
},
modules:{
HomePage
}
});
为了能使用ts
的特性,创建仓库时还需要定义一份类型文件,src/store/store.d.ts
代码如下.
import { HomeType } from "./modules/HomePage";
export interface RootState { //根模块
content:string
}
export interface ModuleState { // 子模块
HomePage:HomeType
}
根模块配置完成,子模块src/store/modules/HomePage
配置代码如下.
子模块HomePage
定义了两个状态、一个action
和一个mutation
.通过Module
将HomeType
和RootState
类型引入后,在编写action
或者mutation
函数时会出现智能提示.
import { RootState } from "@/store/store";
import { Module } from "vuex";
export interface HomeType {
name:string,
age:number
}
export const m:Module<HomeType,RootState> = {
namespaced:true,
state: {
name:"hello world",
age:18
},
mutations:{
incrementAge (state) {
state.age = 3;
}
},
actions:{
increment({ state, commit, rootState }) {
commit('incrementAge')
}
}
}
以上两步就轻松将根模块和子模块配置完了,但当前配置在页面调用vuex
时并不能出现智能提示,还需要添加下面两步.
第一步在原来的类型文件src/store/store.d.ts
添加一个接口AllState
,继承根模块和子模块的状态.
import { HomeType } from "./modules/HomePage";
import { GlobalType } from "./modules/Global";
export interface RootState {
content:string
}
export interface ModuleState {
HomePage:HomeType,
Global:GlobalType
}
export interface AllState extends RootState,ModuleState {}
第二步在原来配置vuex
的主文件中额外添加一个变量key
和函数useStore
,并将类型RootState
和AllState
添加进去.
import { createStore, Store, useStore as baseStore } from 'vuex';
import { m as HomePage } from './modules/HomePage';
import { RootState,AllState } from './store';
import { InjectionKey } from 'vue';
export default createStore<RootState>({
state:{
content:"hello guy"
},
modules:{
HomePage
}
});
export const key:InjectionKey<Store<RootState>> = Symbol(); //定义一个key
export function useStore() {
return baseStore<AllState>(key);
}
安装
上述配置结束后,现在需要将vuex
引入到项目.
在项目入口文件main.ts
使用use
将vuex
安装到应用中,并且传入定义好的key
.
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store,{key} from './store'
const app = createApp(App)
app.use(store,key).use(router).mount('#app')
调用
vuex
成功引入到应用后,页面组件就能够获取vuex
定义的状态了.
首先在页面组件引入上面定义好的useStore
方法,通过执行useStore
获取store
对象(代码如下).
此时在编辑器中使用store
对象拿数据时能够弹出智能提示(如下图).
store
对象通过state
属性可以往下寻找,直至找到想要的状态再通过computed
包裹返回.此时包裹的状态就能够在页面上呈现被具备响应式特性.
另外store
对象调用dispatch
和commit
就可以执行action
和mutation
相关方法.但调用子模块定义的方法时,需要拼接模块名(如HomePage/increment
).
<template>
<p @click="hanlder">点击{{age}}</p>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { useStore } from "@/store";
export default defineComponent({
name: 'Home',
setup() {
const store = useStore();
const hanlder = ()=>{ // 触发事件
store.dispatch("HomePage/increment");
}
const age = computed(()=>{
return store.state.HomePage.age;
})
return {
hanlder,
age
}
}
})
</script>
上述调用方式可正常使用,但vuex
中定义的状态和方法一旦数量剧增,页面调用时会变得十分繁琐,下一小节将介绍如果简化vuex
的调用.
vuex的封装
state封装
vue3
提出的hook
思想能将逻辑代码抽离出来封装复用,我们可以利用这一特性先对state
进行改造.
新建文件src/hook/useState.ts
(代码如下).
useState
第一个参数module_name
为模块名称,第二个参数wrapper
是需要获取的状态.
调用形式如useState('HomePage', ['name', 'age'])
,执行后返回HomePage
模块下的name
和age
的值.
如果module_name
不传值,useState
将直接获取根模块下定义的状态.
useState
整体实现逻辑很简单(代码如下),利用vuex
提供的mapState
和createNamespacedHelpers
能够获取根模块和子模块下的状态,再使用computed
包裹一层组合成对象返回.
import { mapState, createNamespacedHelpers } from "vuex";
import { computed, ComputedRef } from "vue";
import { ModuleState } from "@/store/store";
import { useStore } from "@/store";
/**
* 对store导出数据做封装
*/
export const useState = (module_name:keyof ModuleState | string[],wrapper:string[] = [])=>{
const store = useStore();
let mapFn = mapState;
if(typeof module_name === "string"){ // 访问子模块的状态
mapFn = createNamespacedHelpers(module_name).mapState;
}else{ // 访问RootState
wrapper = module_name;
}
const storeStateFns = mapFn(wrapper);
// 对数据进行转换
const storeState:{ [key:string]: ComputedRef<any>} = {};
// 使用computed将状态包裹一层再返回
Object.keys(storeStateFns).forEach(fnKey => {
const fn = storeStateFns[fnKey].bind({$store: store}) // 不绑定store,vuex执行时会报错
storeState[fnKey] = computed(fn)
})
return storeState;
}
hook
函数编写完成后,页面组件就可以调用获取状态了(代码如下).
从下面代码可以看出,利用useState
获取vuex
中的数据变的非常简单,只需要传入模块名称和状态名称就可以快速拿到数据(vuex中定义的子模块必须要加上namespaced:true
,否则状态获取会失效).
useState
函数如果不传入模块名,直接传入数组形如const rootState = useState(['content'])
,返回的rootState
对象装载着根模块下定义的content
值.
<template>
<p>{{name}}</p>
<div>{{age}}</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useState } from '@/hook/useState'
export default defineComponent({
setup() {
const state = useState('HomePage', ['name', 'age']);
return {
...state
}
},
})
</script>
Getter封装
假如vuex
中定义了getters
如下,getName
用于获取name
值的反转.
export const m:Module<HomeType,RootState> = {
namespaced:true,
state: {
name:"hello world",
age:18
},
getters:{
getName(state){
return state.name.split("").reverse().join("");
}
}
}
Getter
封装的原理和state
非常相似(可以将两者抽离公共代码复用),利用vuex
提供mapGetters
和createNamespacedHelpers
获取根模块和子模块下定义的Getter
函数,再使用computed
包裹组装成对象返回.
// src/hook/useGetter.ts
import { mapGetters, createNamespacedHelpers } from "vuex";
import { computed, ComputedRef } from "vue";
import { ModuleState } from "@/store/store";
import { useStore } from "@/store";
/**
* 对getters做封装
*/
export const useGetter = (module_name:keyof ModuleState | string[],wrapper:string[] = [])=>{
const store = useStore();
let mapFn = mapGetters;
if(typeof module_name === "string"){ // 访问子模块的getter
mapFn = createNamespacedHelpers(module_name).mapGetters;
}else{ // 访问根模块的getter
wrapper = module_name;
}
const storeGettersFns = mapFn(wrapper);
// 对数据进行转换
const storeGetter:{ [key:string]: ComputedRef<any>} = {};
// 使用computed将状态包裹一层再返回
Object.keys(storeGettersFns).forEach(fnKey => {
const fn = storeGettersFns[fnKey].bind({$store: store})
storeGetter[fnKey] = computed(fn)
})
return storeGetter;
}
页面调用方式如下,useGetter
函数传入模块名称和属性返回getters
.
<template>
<p>{{getName}}</p>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useGetter } from "@/hook/useGetter";
export default defineComponent({
setup() {
const getters = useGetter('HomePage',["getName"]);
return {
...getters
}
},
})
</script>
action和mutation封装
为了让action
和mutation
调用起来更加方便,创建hook
函数useMethod
将两者封装到一起.
页面调用形式形如const [actions, mutations] = useMethod('HomePage', ['increment','incrementAge'])
.
useMethod
第一个参数填写模块名称,第二个参数填写方法名称.最终返回的actions
和mutations
会去HomePage
模块下寻找有没有定义increment
和incrementAge
方法.寻找成功就将方法封装成对象返回.
useMethod
函数代码如下.实现原理是利用vuex
提供的mapActions
和mapMutations
获取根模块或者子模块下定义的action
和mutation
.然后通过store._modules.root._rawModule
找到开发者编写的vuex
配置代码.
通过配置中定义的action
和mutation
过滤掉那些不存在的方法名,最后将方法都组合成action
和mutation
对象,以数组的形式返回.(由于官方没有提供api通过store对象获取vuex的配置代码,这里是通过私有属性store._modules.root._rawModule
拿到的,目前vuex 4.0.0
版本支持此做法,如果获取不到请确认vuex
版本号)
import { ModuleState } from "@/store/store";
import { mapActions,mapMutations } from "vuex";
import { useStore } from "@/store";
export const useMethod = (module_name:keyof ModuleState | string[],wrapper:string[] = [])=>{
const store = useStore();
// @ts-ignore
let options = store._modules.root._rawModule; // 获取根模块的配置
if(typeof module_name === "string"){
options = options.modules[module_name]; // 获取子模块的配置
}else{
wrapper = module_name;
}
const { mutations = {},actions = {} } = options;
const mutation_keys = Object.keys(mutations);
const action_keys = Object.keys(actions);
const action_wrapper:string[] = [];
const mutation_wrapper:string[] = [];
wrapper.forEach((item)=>{ // 过滤掉原始配置中不包含的方法
if(mutation_keys.includes(item)){
mutation_wrapper.push(item);
}
if(action_keys.includes(item)){
action_wrapper.push(item);
}
})
const aactions = typeof module_name === "string"?mapActions(module_name, action_wrapper):mapActions(action_wrapper);
const mmutations = typeof module_name === "string"?mapMutations(module_name,mutation_wrapper):mapMutations(mutation_wrapper);
bindStore([aactions,mmutations]); // 不绑定store,vuex执行时会报错
return [aactions,mmutations];
}
function bindStore(list:any[]){
const store = useStore() as any;
list.forEach((item)=>{
for(let key in item){
item[key] = item[key].bind({$store:store});
}
})
}
hook
函数编写完成后,页面调用形式如下.
useMethod
执行后以数组的形式返回actions
和mutations
对象.而页面组件需要调用的action
和mutation
都位于这两个对象下,直接执行对象下的函数即可调用相应的action
或mutation
(遗憾的是目前没有找到hook
函数中对ts
的支持写法,因此返回的结果无法使用智能提示).
<template>
<el-button @click="actions.increment">{{name}}</el-button>
<div @click="mutations.incrementAge">age:{{age}}</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useMethod } from '@/hook/useMethod'
export default defineComponent({
name: 'Home',
setup() {
const state = useState('HomePage', ['name', 'age']);
const [actions, mutations] = useMethod('HomePage', [
'increment',
'incrementAge',
])
return {
actions,
mutations,
...state
}
},
})
</script>