vue3简化vuex的调用

前言

vuex作为跨组件通信的利器,在开发复杂应用过程中必不可少.但vuex在组件中渲染状态和调用actionmutation的语法过于繁琐,严重影响开发效率.

本文借助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.通过ModuleHomeTypeRootState类型引入后,在编写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,并将类型RootStateAllState添加进去.

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使用usevuex安装到应用中,并且传入定义好的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对象调用dispatchcommit就可以执行actionmutation相关方法.但调用子模块定义的方法时,需要拼接模块名(如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模块下的nameage的值.

如果module_name不传值,useState将直接获取根模块下定义的状态.

useState整体实现逻辑很简单(代码如下),利用vuex提供的mapStatecreateNamespacedHelpers能够获取根模块和子模块下的状态,再使用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提供mapGetterscreateNamespacedHelpers获取根模块和子模块下定义的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封装

为了让actionmutation调用起来更加方便,创建hook函数useMethod将两者封装到一起.

页面调用形式形如const [actions, mutations] = useMethod('HomePage', ['increment','incrementAge']).

useMethod第一个参数填写模块名称,第二个参数填写方法名称.最终返回的actionsmutations会去HomePage模块下寻找有没有定义incrementincrementAge方法.寻找成功就将方法封装成对象返回.

useMethod函数代码如下.实现原理是利用vuex提供的mapActionsmapMutations获取根模块或者子模块下定义的actionmutation.然后通过store._modules.root._rawModule找到开发者编写的vuex配置代码.

通过配置中定义的actionmutation过滤掉那些不存在的方法名,最后将方法都组合成actionmutation对象,以数组的形式返回.(由于官方没有提供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执行后以数组的形式返回actionsmutations对象.而页面组件需要调用的actionmutation都位于这两个对象下,直接执行对象下的函数即可调用相应的actionmutation(遗憾的是目前没有找到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>

源代码

仓库地址

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值