前言
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>

被折叠的 条评论
为什么被折叠?



