Vuex+TypeScript实现hook,以及类型增强Store

最终实现效果

使用

ts 复制代码
import { useStates, useGetters, useMutations, useActions } from '@/hooks';

// 获取模块中的state
useStates('commonModule', ['adminUserInfo']);

// 获取root中的state
useStates(['adminToken']);

// root和模块中的都能获取,自动补充模块前缀
useGetters(['commonModule/test', 'test']);

// root和模块中的都能获取,自动补充模块前缀
useMutations(['SET_ADMIN_TOKEN', 'commonModule/SET_ADMIN_USERINFO']);

// root和模块中的都能获取,自动补充模块前缀
useActions(['fn', 'commonModule/GETADMINUSERINFO_ACTION']);

Q: 看到这里就有同学会问了,这么长的字符串,每次都要手动输入不是很麻烦吗 A: 在我们实现类型增强的时候,IDE就有类型提示了,我们只需要选择即可,如下图所示,非常安全!!!

目录组织

store目录组织 module里存放各各模块,如common root这里也认为是一个模块放在这里 types存放一些类型和工具类型 indexstore出口

hooks组织 实现4个hooks,useStates useMutations useGetters useActions

实现

root模块搭建

const.ts

ts 复制代码
export class Mutations_Const {
  static SET_ADMIN_TOKEN = 'SET_ADMIN_TOKEN' as const;
}

types.d.ts

ts 复制代码
import { ActionContext } from 'vuex';
import { Mutations_Const } from '@/store/modules/root/const';

type RootState = {
  version: string;
  adminToken: string;
};

type GettersInRoot = Record<string, (...args: any[]) => any>; // 测试数据

type MutationsInRoot = {
  [Mutations_Const.SET_ADMIN_TOKEN](state: RootState, token: string): void; // 测试数据
};

type ActionsInRoot = {
  fn(context: ActionContext<RootState, RootState>): void; // 测试数据
};

export { RootState, GettersInRoot, MutationsInRoot, ActionsInRoot };

index.ts

ts 复制代码
import type { InjectionKey } from 'vue';
import type { RootState } from '@/store/modules/root/types';
import { createStore, Store as VuexStore, useStore as useBaseStore } from 'vuex';
import { rootState, gettersInRoot, mutationsInRoot, actionsInRoot } from '@/store/modules/root';

// 创建一个具备类型安全的唯一注入 Key,用于在 Vue 组件中注入 Store 实例。一定需要
const key: InjectionKey<VuexStore<RootState>> = Symbol();

const store = createStore({

  // root模块
  state: () => rootState,

  mutations: mutationsInRoot,

  actions: actionsInRoot,

  getters: gettersInRoot,

});

function useStore() {
  return useBaseStore(key);
}

export { store, key, useStore };

在main.ts中使用即可

ts 复制代码
import { store, key } from '@/store';

import App from './App.vue';

const app = createApp(App);

app.use(store, key);

这样我们就创建好了一个不带类型提示的vuex仓库啦

common模块搭建

action-types.ts

两个都是用于保存常量的文件,可根据自己需要使用

ts 复制代码
export const GETPROFILE_ACTION = 'GETPROFILE_ACTION';

export const GETADMINUSERINFO_ACTION = 'GETADMINUSERINFO_ACTION';

mutation-types.ts

ts 复制代码
export const SET_PROFILE = 'SET_PROFILE';

export const SET_ADMIN_USERINFO = 'SET_ADMIN_USERINFO';

state.ts

state模块,定义一个类型和一个state对象,导出对象和类型, 具体类型朋友们可以忽略,直接自己定义几个字符串即可~

ts 复制代码
import type { Profile, AdminUserType } from '@/api/types';

interface State {
  profile: Profile.ProfileDetailData;
  adminUserInfo: AdminUserType.AdminUserInfo;
}

const state: State = {
  profile: {
    author: '',
    avatar: '',
    csdnHomepage: '',
    giteeHomepage: '',
    githubHomepage: '',
    introduction: '',
    logo: '',
    name: '',
    zhihuHomepage: '',
  },
  adminUserInfo: {
    username: '',
  },
};

export { state };
export type { State };

getters.ts

ts 复制代码
import type { GetterTree } from 'vuex';
// 刚刚写好的State类型,拿过来使用
import type { State } from './state';
// root模块的State
import type { RootState } from '@/store/modules/root/types';

// Getters 类型
type Getters = {
  test(): Record<string, Array<number>>; // 测试类型,随便定义
};

// 类型实现
const getters: GetterTree<State, RootState> & Getters = {
  test() {
    const a = { a: [1] };
    return a;
  },
};

// 依旧导出
export { getters };
export type { Getters };

mutations.ts

ts 复制代码
import type { MutationTree } from 'vuex';
// 刚刚写好的State类型
import type { State } from './state';
import { SET_PROFILE, SET_ADMIN_USERINFO } from './mutation-types';
import type { Profile, AdminUserType } from '@/api/types';

// 这里使用上述定义的常量进行赋值,不容易错
type Mutations = {
  [SET_PROFILE](state: State, payload: Profile.ProfileDetailData): void;
  [SET_ADMIN_USERINFO](state: State, payload: AdminUserType.AdminUserInfo): void;
};

const mutations: MutationTree<State> & Mutations = {
  [SET_PROFILE](state, payload) {
    state.profile = payload;
  },
  [SET_ADMIN_USERINFO](state, payload) {
    state.adminUserInfo = payload;
  },
};

// 依旧导出
export { mutations };
export type { Mutations };

actions.ts

ts 复制代码
import type { State } from './state';
import type { ActionContext as VuexActionContext, ActionTree } from 'vuex';
import type { RootState } from '@/store/modules/root/types';
import { GETPROFILE_ACTION, GETADMINUSERINFO_ACTION } from './actions-types';
import API from '@/api';

// VuexActionContext泛型,传入定义的State和RootState 类型,好让context有类型推导
type CommonActionContext = VuexActionContext<State, RootState>;

type Actions = {
  [GETPROFILE_ACTION](context: CommonActionContext): void;
  [GETADMINUSERINFO_ACTION](context: CommonActionContext): void;
};

const actions: ActionTree<State, RootState> & Actions = {
  async [GETPROFILE_ACTION](context) {
    const profile = await API.profileDetail.getProfileDetail();
    context.commit('SET_PROFILE', profile!.data);
  },

  async [GETADMINUSERINFO_ACTION](context) {
    const adminUserInfo = await API.AdminUser.getAdminUserInfo();
    context.commit('SET_ADMIN_USERINFO', adminUserInfo!.data);
  },
};

export { actions };
export type { Actions };

index.ts

ts 复制代码
import type { Module } from 'vuex';
import type { RootState } from '@/store/modules/root/types';
import type { State } from './state';

// 导入上述定义的文件
import { state } from './state';
import { getters } from './getters';
import { mutations } from './mutations';
import { actions } from './actions';

const commonModule: Module<State, RootState> = {
  namespaced: true, // 开启命名空间,独立

  state: () => state, // 单独定义state是为了让ts验证State类型

  getters,

  mutations,

  actions,
};

//导出模块对象
export default commonModule;

挂载到vuex中

ts 复制代码
const store = createStore({
  state: () => rootState,

  mutations: mutationsInRoot,

  actions: actionsInRoot,

  getters: gettersInRoot,

  // 这里注册模块
  modules: {
    commonModule,
  },
})

到这里,我们就搭建好了基本的的vuex了,接下来来增强其类型推导

types文件夹

root.d.ts

我们要增强其类型推导,就要实现一个增强后的仓库类型赋值给Store,如EnhancedStore 补充小知识,TS中,Omit类型工具可以对一个类型从该类型中去除指定的属性,extends可以继承某个类型 那么我们大概需要一个如下的伪代码

ts 复制代码
interface EnhancedStore extends Omit<Store,  'getters', 'commit', 'dispatch'> {
    getters:  xxx;
    commit: xxx;
    dispatch: xxx;
}

// 然后导出该类型

export { EnhancedStore }

types.d.ts

这个文件就用来写各种工具类型,来帮我们转换成想要的类型

ts 复制代码
// 首先处理state
// 因为state没有额外的处理,我们直接定义即可

// 访问需要store.state[namespace][key]
import { State as CommonState } from '@/store/modules/common/state';
type ModuleStates = {
  commonModule: CommonState;
  // otherModule: OtherState;
};


// 处理getters
// 开启了namespaced的module要访问,需要store.getters['modulename/key'],故我们要构造这样的结构
// 将模块化的Getters 映射为带模块名字的字面量:commonModule/name

// 泛型G是包含模块中getters的类型
type NamespacedGetters<ModuleName extends string, G> = {
  [K in keyof G as `${ModuleName}/${string & K}`]: ReturnType<G[K]>;
};

// common模块中的Getters类型
import { Getters as CommonGetters } from '@/store/modules/common/getters';
// Root模块中的Getters类型
import { GettersInRoot } from '@/store/modules/root/types';
//使用工具类型对common模块转换得到新类型,root模块直接store.getters[key], 故不用改造,最后合并类型
type RootGetters = NamespacedGetters<'commonModule', CommonGetters> & GettersInRoot;


// 处理mutations
// 开启了namespaced的module要访问,需要store.commit['modulename/key'],故我们要构造这样的结构
// actions同理,故类型工具可以合并在一起
type StoreTypes = 'Actions' | 'Mutations';

// 将模块化的Actions/Mutations映射为带模块名的字面量
type NameSpacedActionsOrMutations<
  ModuleName extends string,
  AllTypes extends (...args: any[]) => any,
> = {
  [K in keyof AllTypes as `${ModuleName}/${K}`]: AllTypes[K];
};

// 处理commit和dispatch的options
type GetTypesOptions<T extends StoreTypes> =
  T extends Extract<StoreTypes, 'Mutations'> ? CommitOptions : DispatchOptions;

// 处理commit和action中,payload携带参数的类型与提示
// 意思是对应的commit/action函数,如果第二个参数payload为undefined,payload为可选,补充options
// 如果是不为undefined,则把推断的P当作payload的参数类型
type GetParametersTypes<Type extends StoreTypes, Fn> =
  Parameters<Fn> extends [any, infer P]
    ? undefined extends P
      ? [payload?: undefined, options?: GetTypesOptions<Type>]
      : [payload: P, options?: GetTypesOptions<Type>]
    : [payload?: undefined, options?: GetTypesOptions<Type>];

// 使用起来!!!增强Store的commit和dispatch类型推导

// 泛型T为mutations或者actions对象,返回一个函数,第一个参数type为对应泛型T中的key,剩余参数由上述GetParametersTypes决定
type EnhanceStoreTypes<
  Type extends StoreTypes,
  T extends Record<string, (...args: any[]) => any>,
> = <K extends keyof T>(type: K, ...args: GetParametersTypes<Type, T[K]>) => ReturnType<T[K]>;



// 现在已经编写完类型工具了,可以直接来实现mutations和actions
import { Mutations as CommonMutations } from '@/store/modules/common/mutations';
import { MutationsInRoot } from '@/store/modules/root/types';

type RootMutations = NameSpacedActionsOrMutations<'commonModule', CommonMutations> &
  MutationsInRoot;
type RootCommit = EnhanceStoreTypes<'Mutations', RootMutations>;

import { Actions as CommonActions } from '@/store/modules/common/actions';
import { ActionsInRoot } from '@/store/modules/root/types';

type RootActions = NameSpacedActionsOrMutations<'commonModule', CommonActions> & ActionsInRoot;
type RootDispatch = EnhanceStoreTypes<'Actions', RootActions>;

// 导出工具
export {
  ModuleStates,
  RootGetters,
  RootCommit,
  RootMutations,
  RootDispatch,
  RootActions,
  StoreTypes,
  GetTypesOptions,
};

到这里,已经完成编写类型了,可以回到root.d.ts完成增强后的store类型了

root.d.ts--update

ts 复制代码
import { Store as VuexStore } from 'vuex';
import { RootGetters, RootCommit, RootDispatch } from '@/store/types/types-helper';

interface EnhancedStore extends Omit<VuexStore<RootState>, 'getters' | 'commit' | 'dispatch'> {
  getters: RootGetters;
  commit: RootCommit;
  dispatch: RootDispatch;
}

export { EnhancedStore };

index.ts--update

ts 复制代码
import type { InjectionKey } from 'vue';
import type { EnhancedStore } from './types/root';
import type { RootState } from '@/store/modules/root/types';
import { createStore, Store as VuexStore, useStore as useBaseStore } from 'vuex';
import commonModule from './modules/common';
import { rootState, gettersInRoot, mutationsInRoot, actionsInRoot } from '@/store/modules/root';

const key: InjectionKey<VuexStore<RootState>> = Symbol(); // 创建一个具备类型安全的唯一注入 Key,用于在 Vue 组件中注入 Store 实例。

const store = createStore({
  state: () => rootState,

  mutations: mutationsInRoot, 

  actions: actionsInRoot, 

  getters: gettersInRoot,

  modules: {
    commonModule,
  },
}) as EnhancedStore;

function useStore(): EnhancedStore {
  return useBaseStore(key);
}

export { store, key, useStore };

现在去使用store,就会具有对应的类型推导和提示了!!!!!!

hook实现

types.d.ts

ts 复制代码
import { ComputedRef } from 'vue';
import { StoreTypes, GetTypesOptions } from '@/store/types/types-helper';

// 处理mutations和actions函数的参数和函数的返回值类型
type GetFnParametersAndReturnType<Type extends StoreTypes, Fn> =
  Parameters<Fn> extends [any, infer P]
    ? undefined extends P
      ? (payload?: undefined, options?: GetTypesOptions<Type>) => ReturnType<Fn>
      : (payload: P, options?: GetTypesOptions<Type>) => ReturnType<Fn>
    : (payload?: undefined, options?: GetTypesOptions<Type>) => ReturnType<Fn>;

// 根据mutations和actions对象,映射函数
type FnMapper<Type extends StoreTypes, T extends Record<string, (...args: any[]) => any>> = {
  [K in keyof T]: GetFnParametersAndReturnType<Type, T[K]>;
};

// 因为state和getters需要包装成响应式的computedRef
type StateOrGettersMapper<T> = {
  [K in keyof T]: ComputedRef<T[K]>;
};

// 导出
export { GetFnParametersAndReturnType, FnMapper, StateOrGettersMapper };

useStates.ts

ts 复制代码
import type { ComputedRef } from 'vue';
import type { RootState } from '@/store/modules/root/types';
import type { ModuleStates } from '@/store/types/types-helper';
import type { StateOrGettersMapper } from '@/hooks/store-hooks/types-helper';
import { computed } from 'vue';
import { useStore } from '@/store';

//  重载定义

// 根模块数组
function useStates<K extends keyof RootState>(keys: K[]): Pick<StateOrGettersMapper<RootState>, K>;

// 根模块别名
function useStates<M extends Record<string, keyof RootState>>(
  keys: M
): { [K in keyof M]: ComputedRef<RootState[M[K]]> };

// 模块命名空间数组
function useStates<N extends keyof ModuleStates, K extends keyof ModuleStates[N]>(
  namespace: N,
  keys: K[]
): Pick<StateOrGettersMapper<ModuleStates[N]>, K>;

// 模块命名空间别名
function useStates<N extends keyof ModuleStates, M extends Record<string, keyof ModuleStates[N]>>(
  namespace: N,
  keys: M
): { [K in keyof M]: ComputedRef<ModuleStates[N][M[K]]> };

//  实现体
function useStates(
  namespaceOrKeys:
    | keyof RootState
    | (keyof RootState)[]
    | Record<string, keyof RootState>
    | keyof ModuleStates,
  keysOrMaybeNothing?: any
) {
  const store = useStore();
  const result: Record<string, ComputedRef<any>> = {};

  if (
    Array.isArray(namespaceOrKeys) ||
    (typeof namespaceOrKeys === 'object' && namespaceOrKeys !== null)
  ) {
    // 根模块
    const keys = namespaceOrKeys;
    if (Array.isArray(keys)) {
      keys.forEach((key) => {
        result[key as string] = computed(() => store.state[key]);
      });
    } else {
      Object.entries(keys).forEach(([alias, key]) => {
        result[alias] = computed(() => store.state[key]);
      });
    }
  } else if (typeof namespaceOrKeys === 'string') {
    const namespace = namespaceOrKeys as keyof ModuleStates;
    const keys = keysOrMaybeNothing;

    if (Array.isArray(keys)) {
      keys.forEach((key) => {
        result[key as string] = computed(
          () => store.state[namespace][key as keyof ModuleStates[typeof namespace]]
        );
      });
    } else {
      Object.entries(keys).forEach(([alias, key]) => {
        result[alias] = computed(
          () => store.state[namespace][key as keyof ModuleStates[typeof namespace]]
        );
      });
    }
  }

  return result;
}

export default useStates;

useGetters.ts

ts 复制代码
import type { ComputedRef } from 'vue';
import type { RootGetters } from '@/store/types/types-helper';
import type { StateOrGettersMapper } from '@/hooks/store-hooks/types-helper';
import { computed } from 'vue';
import { useStore } from '@/store';

//  重载定义

// 方式 1:传数组(返回 Pick 工具函数)
function useGetters<K extends keyof RootGetters>(
  keys: K[]
): Pick<StateOrGettersMapper<RootGetters>, K>;

// 方式 2:传别名映射(返回别名类型)
function useGetters<M extends Record<string, keyof RootGetters>>(
  keys: M
): { [K in keyof M]: ComputedRef<RootGetters[M[K]]> };

//  实现

function useGetters(keys: (keyof RootGetters)[] | Record<string, keyof RootGetters>) {
  const store = useStore();
  const result: Record<string, any> = {};

  if (Array.isArray(keys)) {
    keys.forEach((key) => {
      result[key as string] = computed(() => store.getters[key]);
    });
  } else {
    Object.entries(keys).forEach(([alias, key]) => {
      result[alias] = computed(() => store.getters[key]);
    });
  }

  return result;
}

export default useGetters;

useMutations.ts

ts 复制代码
import { useStore } from '@/store';
import type { CommitOptions } from 'vuex';
import type { RootMutations } from '@/store/types/types-helper';
import type { GetFnParametersAndReturnType, FnMapper } from '@/hooks/store-hooks/types-helper';

//  重载定义
// 方式 1:传数组(返回 Pick 工具函数)
function useMutations<K extends keyof RootMutations>(
  keys: K[]
): Pick<FnMapper<'Mutations', RootMutations>, K>;

// 方式 2:传别名映射(返回别名类型)
function useMutations<M extends Record<string, keyof RootMutations>>(
  keys: M
): {
  [K in keyof M]: GetFnParametersAndReturnType<'Mutations', RootMutations[M[K]]>;
};

// 实现

function useMutations(keys: (keyof RootMutations)[] | Record<string, keyof RootMutations>) {
  const store = useStore();
  const result: Record<string, any> = {};

  if (Array.isArray(keys)) {
    keys.forEach((key) => {
      result[key as string] = (payload?: any, options?: CommitOptions) =>
        store.commit(key as keyof RootMutations, payload, options);
    });
  } else {
    Object.entries(keys).forEach(([alias, key]) => {
      result[alias] = (payload?: any, options?: CommitOptions) =>
        store.commit(key, payload, options);
    });
  }

  return result;
}

/**
 * 含有payload的mutation使用时,不传会报错
 * 定义时未定义的则为可选,undefined
 *
 */
export default useMutations;

useActions.ts

ts 复制代码
import { useStore } from '@/store';
import type { DispatchOptions } from 'vuex';
import type { RootActions } from '@/store/types/types-helper';
import type { GetFnParametersAndReturnType, FnMapper } from '@/hooks/store-hooks/types-helper';

// 重载定义
// 1. 数组形式,返回 Pick
function useActions<K extends keyof RootActions>(
  keys: K[]
): Pick<FnMapper<'Actions', RootActions>, K>;

// 2. 别名映射形式,返回对应别名映射
function useActions<M extends Record<string, keyof RootActions>>(
  keys: M
): {
  [K in keyof M]: GetFnParametersAndReturnType<'Actions', RootActions[M[K]]>;
};

// 实现
function useActions(keys: (keyof RootActions)[] | Record<string, keyof RootActions>) {
  const store = useStore();
  const result: Record<string, any> = {};

  if (Array.isArray(keys)) {
    keys.forEach((key) => {
      result[key as string] = (payload?: any, options?: DispatchOptions) =>
        store.dispatch(key as keyof RootActions, payload, options);
    });
  } else {
    Object.entries(keys).forEach(([alias, key]) => {
      result[alias] = (payload?: any, options?: DispatchOptions) =>
        store.dispatch(key, payload, options);
    });
  }

  return result;
}
export default useActions;

index.ts

ts 复制代码
import useStates from '@/hooks/store-hooks/useStates';
import useGetters from '@/hooks/store-hooks/useGetters';
import useMutations from '@/hooks/store-hooks/useMutations';
import useActions from '@/hooks/store-hooks/useActions';

export { useStates, useGetters, useMutations, useActions };

这样子我们就封装好了,就可以和开头一样使用啦,有安全的类型提示啦!!!!

相关推荐
濮水大叔21 分钟前
Node.js 主流ORM框架动态分表方案大盘点
typescript·nodejs·orm·prisma
8931519602 小时前
TypeScript 泛型
typescript·鸿蒙开发·鸿蒙教程·鸿蒙泛型
烛阴20 小时前
解锁动态键:TypeScript 索引签名完全指南
前端·javascript·typescript
艾小码1 天前
TypeScript在前端的实践:类型系统助力大型应用开发
前端·typescript
烛阴2 天前
掌握 TypeScript 的边界:any, unknown, void, never 的正确用法与陷阱
前端·javascript·typescript
simple_lau2 天前
H5资源包热更新:从下载、解压到渲染的实现方案
typescript·harmonyos·arkts
王林不想说话2 天前
新的枚举使用方式enum-plus
前端·vue.js·typescript
烛阴3 天前
告别繁琐的类型注解:TypeScript 类型推断完全指南
前端·javascript·typescript