一个让 vuex 更好的支持 typescript 的解决方案
传统 vuex 编码让人觉得麻烦的一点就是 state、getters、mutation、dispatch 在调用时无法获得编辑器的智能提示,必须切换文件去查找。本以为用上 typescript 后这个问题可以得到解决,却发现vuex官方提供的types并没有那么强大...
在找寻了一会儿各种解决方案后,觉得都存在这样或那样的问题(类型需要重复定义、侵入严重,和原本写法完全不一样),所以便自己写了这么一个解决方案,在获得了typescript的智能提示支持下却不需要重复写各种type和interface,和vuex原本写法保持一致,对业务代码侵入极小。
demo 项目由 vue-cli 3 生成,IDE 为 VSCODE
效果展示
1. state
state 会显示所有的 module、里面的属性及属性的类型
/src/store/modules/auth.ts
const moduleState = {
token: '',
tokenExpire: 0,
}
复制代码
2. getters
getter 可以显示值类型
/src/store/modules/auth.ts
const moduleGetters = {
isLogin(state: State, getters: any, rootState: Store['state'], rootGetters: any) {
return !!state.token && (!!rootState.user.userId || !!rootState.user.accountId);
},
};
复制代码
3. commit
commit 会显示所有注册的 mutation 以及对应 payload 的类型, 如果 payload 类型不对还会报错
/src/store/modules/auth.ts
const mutations = {
setToken(state: State, payload: State['token']) {
state.token = payload || '';
},
setTokenExpire(state: State, payload: State['tokenExpire']) {
state.tokenExpire = payload || 0;
},
};
复制代码
/src/store/modules/user.ts
const mutations = {
setAccountId(state: State, payload: State['accountId']) {
state.accountId = payload || '';
},
setUserId(state: State, payload: State['userId']) {
state.userId = payload || '';
},
setUserInfo(state: State, payload: State['info']) {
state.info = payload || {};
},
};
复制代码
/src/store/modules/pageCache.ts
const mutations = {
setPageToCache(state: State, payload: any) {
state.pagesName.unshift(payload.pageName);
setTimeout(() => {
if (payload.callback) payload.callback();
}, 0);
},
resetPageCache(state: State) {
state.pagesName = [...cachePages];
},
};
复制代码
4. dispatch
和 commit 类似
/src/store/modules/auth.ts
const actions = {
updateAuthData({ commit }: ActionContext, payload: any) {
commit('setToken', payload.token);
commit('setTokenExpire', payload.expire);
},
cleanAuthData({ commit }: ActionContext) {
commit('setToken', '');
commit('setTokenExpire', 0);
},
};
复制代码
实现方式
上面可以看到,这个方案在保证了 vuex 原来的写法(稍微有一点点变化,从 this.$store 换为 this.$$store)上支持了 typescript 的类型检查,为了实现它,vuex 的 store 在初始化的时候我们需要做一些额外的工作,但是也仅限于这一点额外的工作了,后续的 module(业务)的增加,也完全像 vuex 本来的写法那样去定义各种 state、getters、mutation、action,一劳永逸的获得 typescript 对 vuex 各种调用的支持。
1. 改造 vuex 部分
先来看看最基本的 vuex 代码
/store/modules/auth.ts
// ts 校验会提示好几个函数具有隐式 any 入参,暂略过
const moduleState = {
token: '',
tokenExpire: 0,
};
const moduleGetters = {
isLogin(state, getters, rootState, rootGetters) {
return !!state.token && (!!rootState.user.userId || !!rootState.user.accountId);
},
};
const mutations = {
setToken(state, payload) {
state.token = payload || '';
},
setTokenExpire(state, payload) {
state.tokenExpire = payload || 0;
},
};
const actions = {
updateAuthData({ commit }, payload) {
commit('setToken', payload.token);
commit('setTokenExpire', payload.expire);
},
cleanAuthData({ commit }) {
commit('setToken', '');
commit('setTokenExpire', 0);
},
};
export default {
state: moduleState,
getters: moduleGetters,
mutations,
actions,
};
复制代码
/store/index.ts
import Vue from 'vue';
import Vuex from 'vuex';
import auth from './modules/auth';
import user from './modules/user';
import pageCache from './modules/pageCache';
Vue.use(Vuex);
const store = new Vuex.Store({
modules: {
auth,
user,
pageCache,
},
});
export default store;
复制代码
下面我们给它加一点 Magic
/store/index.ts
import Vue from 'vue';
import Vuex from 'vuex';
import auth from './modules/auth';
import user from './modules/user';
import pageCache from './modules/pageCache';
Vue.use(Vuex);
// 从 module 的 state 中提取 state 的类型并集合
interface State {
auth: typeof auth.state;
user: typeof user.state;
pageCache: typeof pageCache.state;
}
// 将 getter 函数转换成 {getterName: getterFuncsReturnType} 的对象类型
export type ReturnGetters(...args: any) => any }> = {
[P in keyof T]: ReturnType;
}
// 提取所有 module 的 getter 函数类型对象
type GettersFuncs = (
typeof auth.getters
& typeof user.getters
& typeof pageCache.getters
)
// 将 getter 转换成对象
type Getters = ReturnGetters
// 提取 mutation 函数类型
type CommitFuncs = (
typeof auth.mutations
& typeof user.mutations
& typeof pageCache.mutations
)
// 将 mutation 函数名及 payload 类型转换成 commit 函数的两个入参类型
interface Commit {
(type: T, payload?: Parameters[1]): void;
}
// dispatch 处理步骤同 commit
type DispatchFuncs = (
typeof auth.actions
& typeof user.actions
& typeof pageCache.actions
)
interface Dispatch {
(type: T, payload?: Parameters[1]): Promise;
}
const store = new Vuex.Store({
modules: {
auth,
user,
pageCache,
},
});
export default store;
// 其他 ts 文件解构导入时获得每个对象的改造后类型
export const { state } = store;
export const { getters }: { getters: Getters } = store; // 定义 getters 的类型
export const { commit }: { commit: Commit } = store; // 定义 commit 的类型
export const { dispatch }: { dispatch: Dispatch } = store; // 定义 commit 的类型
// 导出类型 Store 以便在 Vue 原型上定义类型
export interface Store {
state: State;
getters: Getters;
commit: Commit;
dispatch: Dispatch;
}
复制代码
为了避免循环引用,我们需要在 /src/types/ 下面建一个 .d.ts 文件 ,来让各个 module 可以引用到全局的 State (rootState、commit、dispatch)
/src/types/store.d.ts
import { Store as s } from '../store/index';
export { ReturnGetters } from '../store/index';
export interface Store extends s {}
export interface ActionContext {
dispatch: Store['dispatch']; // 全局的 dispatch, 有 ts 提示支持
commit: Store['commit']; // 全局的 commit, 有 ts 提示支持
state: S;
getters: G;
rootState: Store['state']; // 全局的 state, 有 ts 提示支持
rootGetters: any; // 暂时还无法实现将全局 getter 定义过来,会出现类型循环引用问题
}
复制代码
最后去修改我们的 module(仅举例auth)
/src/store/modules/auth.ts
import { ReturnGetters, Store, ActionContext } from '../../types/store';
const moduleState = {
token: '',
tokenExpire: 0,
};
type State = typeof moduleState; // 提取 state 类型
const moduleGetters = {
isLogin(state: State, getters: any, rootState: Store['state'], rootGetters: any) {
return !!state.token && (!!rootState.user.userId || !!rootState.user.accountId);
},
};
type Getters = ReturnGetters<typeof moduleGetters>; // 提取 getter 类型
const mutations = {
setToken(state: State, payload: State['token']) {
state.token = payload || '';
},
setTokenExpire(state: State, payload: State['tokenExpire']) {
state.tokenExpire = payload || 0;
},
};
const actions = {
updateAuthData({ commit }: ActionContext, payload: any) {
commit('setToken', payload.token);
commit('setTokenExpire', payload.expire);
},
cleanAuthData({ commit }: ActionContext) {
commit('setToken', '');
commit('setTokenExpire', 0);
},
};
export default {
state: moduleState,
getters: moduleGetters,
mutations,
actions,
};
复制代码
2.改造 vue 实例化 vuex 部分
/src/main.ts
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store, { Store } from './store/index';
// 其他 ts 文件在直接解构引入的时候,也可以获得智能提示
// import { state, getters, commit, dispatch } from './store/index';
Vue.config.productionTip = false;
const app = new Vue({
router,
store,
render: h => h(App),
}).$mount('#app');
// 将改造过后的 Store 类型声明到 vue 的原型上,这样就可以在.vue 文件中获得 IDE 的智能提示了
// 因为下面去声明一个新的 Store 类型的时候,无法覆盖 vue 原有的 $store 类型声明,所以采取一个新的名字 $$store 来应用新的类型,本质上都是 app.$store
Vue.prototype.$$store = app.$store;
declare module 'vue/types/vue' {
interface Vue {
$$store: Store;
}
}
复制代码
至此 vuex 的 typescript 支持改造就完成了(其中 /src/store/index.ts 完全可以用nodejs写个脚本读取所有module文件名然后用mustache根据模板来生成,每次新增模块时跑一行命令就更新了),之后就可以愉快的使用 vuex 原本的写法又不用来回切文件找各种命名了,并且写错的时候 ts 还会校验错误,妈妈再也不用担心我犯这种低级错误了~
ps: 这个方案可能还不够好(rootGetters 类型没有实现等...),欢迎各位交流学习
ppps: 下一步想将这些 ts 类型的代码抽离成一个工具库,但是初步想了一下有点难度,就先以 demo 的形式分享一下
demo
地址:github
代码拉下来以后
yarn install
复制代码
之后就可以在 .vue 文件和 .ts 文件中体验 vuex 获得智能提示的便利了~
如果对你有帮助的话,麻烦给个star呗~
- 作者:BarneyZhao
- 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。