本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!
Vue3搭建生产项目踩坑系列已经进入第四篇,目前我们的项目已经完成了ESLint、husky、lint-staged和大杯Element的引入。没有看到的同学们可以点击传送门回看。
- Vue3+Vite+TS+Eslint(Airbnb规则)搭建生产项目,踩坑详记(一)
- Vue3+Vite+TS+Eslint(Airbnb规则)搭建生产项目,踩坑详记(二):配置husky和lint-staged
- Vue3+Vite+TS+Eslint(Airbnb规则)搭建生产项目,踩坑详记(三):引入Element-plus,解决字体文件404问题
今天我们原计划一次性引入vuex和vue-router,但是写完vuex的引入以后发现篇幅已经很长了,所以只能单开一篇。
不过也好,就又可以水一篇文章了,哈哈哈哈
vuex是干啥的大家肯定都烂熟于心了。所以就不做过多解释,直接开搞
安装
这里我们需要安装的是 vuex@next,它是和Vue3相匹配的版本
npm install vuex@next --save
初始化
单文件形式
我们创建 src/store/index.ts 文件,在文件内初始化vuex。
// src/store/index.ts
import { createStore } from 'vuex';
const store = createStore({
// vuex相关内容
state() {
return {
count: 0,
};
},
mutations: {
increment(state) {
state.count += 1;
},
},
});
export default store;
然后在 main.ts 中引入并绑定该文件到Vue实例
//main.ts
...
import store from './store/index'; // ++
createApp(App).use(ElementPlus, { locale }).use(store).mount('#app'); // edit
引入store文件后,ESLint会报错
这是我们第一次引入ts文件,因为airbnb-base只提供了原生js的校验,而且目前我没有找到airbnb官方的规则库。所以还是像上次一样,我们对airbnb-base的校验规则进行修改。
// .eslintrc.js
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
}
rules: [
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
mjs: 'never',
jsx: 'never',
ts: 'never',
},
],
]
修改后重启ESLint服务,报错就已经不存在了。
完成初始化以后,我们会发现mutations里的方法报了ESLint错误,不要直接修改参数值。但是在vuex里,我们势必要直接修改state。
和上次处理vite的引入一样,查看airbnb-base关于这条规则的定义,然后将state加入例外就可以了。 airbnb-base规则定义
我们在ESLint中添加如下规则:
...
ruls: {
'no-param-reassign': [
'error',
{
props: true,
ignorePropertyModificationsFor: [
'acc', // for reduce accumulators
'accumulator', // for reduce accumulators
'e', // for e.returnvalue
'ctx', // for Koa routing
'context', // for Koa routing
'req', // for Express requests
'request', // for Express requests
'res', // for Express responses
'response', // for Express responses
'$scope', // for Angular 1 scopes
'staticContext', // for ReactRouter context
'state', // for vuex state ++
],
},
],
}
添加完规则后,再次查看发现依然有报错,但是报错信息已经变了。
这是因为我们没有声明state的类型,解决这个问题,我们首先创建关于state类型声明的接口。
// src/store/index.ts
...
interface State {
count: number;
}
...
定义了类型接口后,我们要在什么地方使用呢?
通过查看vuex关于类型的定义可以知道,createStore方法可以接收一个参数S表示类型和一个StoreOptions<S>类型的参数options。
再看StoreOptions类型接口,它把S当做了接口的参数,并且定义为state属性的类型, 同时还把S传给了其它属性当做参数,这样就可以在mutations的方法中用state当参数时拿到state的类型定义。
因此,我们将自定义的State接口当做泛型参数传给createStore即可,如下:
import { createStore } from 'vuex';
interface State {
count: number;
}
const store = createStore<State>({
// vuex相关内容
...
});
export default store;
修改完成后,可以看到increment方法接收的state参数已经可以拿到类型了。
到这里我们的vuex已经可以正常使用了,但是在项目中,我们很可能有很多数据需要在vuex中维护,如果把所有的数据全部放到一个文件中,会让代码可读性变差并且不易维护。因此当数据较多时,推荐将数据按功能分割成Module使用。
接下来我们就将上面的例子拆成模块。
模块组合
文件迁移
首先我们新建 src/store/module.ts 文件,并将原来定义到index.ts中的state和mutations转移到新创建的module.ts文件中。
// src/store/module.ts
// 类型声明
export interface ModuleState {
count: number;
}
// module信息
export default {
state() {
return {
count: 0,
};
},
mutations: {
increment(state: ModuleState) {
state.count += 1;
},
},
};
// src/store/index.ts
import { createStore } from 'vuex';
import module from './module';
const store = createStore({
modules: {
module,
}
});
export default store;
模块类型声明
创建module.ts模块后,mutations中的方法依然会报state类型。
我们理所当然的想到的就是上面例子中的声明方式,创建模块级别的类型接口,然后声明mutation方法的state参数。这种声明方式当然是可以的。但是如果模块内有10个mutation,甚至还有10个getters,那意味着我们每一次使用state都要去声明类型。而且这种声明方式并不符合vuex源码中关于Module和State的类型声明逻辑。
源码中的类型声明
在单文件形式一节中我们知道,createStore接收了一个StoreOptions<S>类型的参数,截图我们可以看到这个接口中将 modules 的类型声明为 ModuleTree<S>,并且将S当做参数传给了接口ModuleTree,也就是createStore传入的state类型参数。
看一下ModuleTree的声明:
ModuleTree接收的S作为R使用,并将R传给Module接口。
Module接口接收S和R,并将state声明为S类型,将S、R传给内部的getters等。
通过Getter等的类型定义,我们知道了R其实就是rootState的类型,因为它是从createStore中接收的类型参数。
改进我们的类型声明
知道了源码中关于类型的定义,我们就可以根据它的思路优化我们store中的类型声明方式了。
首先,在模块中我们可以将抛出的Object声明为vuex中的Module类型,并给它传入S和R参数。S参数就是我们声明的模块state接口;R是rootState类型,目前还没有传入,先用R代替。
// src/store/module.ts
import { Module } from 'vuex';
export interface ModuleState {
count: number;
}
export default {
state() {
return {
count: 0,
};
},
mutations: {
increment(state) {
state.count += 1;
},
},
} as Module<ModuleState, R>;
修改以后,mutations中的state已经有类型信息了,但是R此时还没有类型传入。不急,接下来我们修改index.ts,并解决R的类型问题。
在index.ts中,需要创建一个State接口来声明rootState的类型,代码如下
import { createStore } from 'vuex';
import module from './module';
export interface State{
rootCount: number; // 如果没有根state,可以为空,此处为了稍后验证,添加类型信息
}
const store = createStore<State>({
state() {
return {
rootCount: 111,
}
},
modules: {
module,
}
});
export default store;
这时根类型声明有了,我们就可以在模块中引入State接口,并传给Module接口需要的R参数,同时添加一个getters进行验证。
// src/store/module.ts
import { State } from './index'
export default {
...
getters: {
getCount(state, getters, rootState) {
return `${state.count}---=${rootState.rootCount}`
}
}
} as Module<ModuleState, State>;
这时rootState已经可以拿到类型信息了。
至此,我们也就按照官方类型声明的逻辑完成了我们自己的类型优化。
使用
关于使用方法,官方文档中有详细的使用介绍,篇幅起见,我们不做详细介绍,参考文档即可。 (next.vuex.vuejs.org/guide/compo…)
Vue3更推荐使用Composition API,因为我们的项目源码中仅保留此种使用方式。
当前的配置使用起来没有什么问题,但是我们发现在组件中拿到的state变量没有返回类型信息,这在某些情况下就可能会出现问题,比如对我们定义的count属性使用.length方法。
在官方文档中对此也有详细的说明,我们自行查看即可。
返回类型化的store
我们先按照官方文档的格式修改vuex相关配置
// src/store/index.ts
import { InjectionKey } from 'vue';
import { createStore, Store, useStore as baseUseStore } from 'vuex';
...
export interface State{
rootCount: number;
}
export const key: InjectionKey<Store<State>> = Symbol() // ++
// 定义自己的 `useStore` 组合式函数 ++
export function useStore () {
return baseUseStore(key)
}
// main.ts
...
import store, { key } from './store/index'; // edit
createApp(App).use(ElementPlus, { locale }).use(store, key).mount('#app'); // edit
使用时我们引入自定义的 useStore 方法,通过它获取到的state就已经是有状态的值了。
模块类型处理
但是只完成上述改造,我们发现访问rootState的属性没有问题,如果访问module内部的state,会提示我们module在State类型上不存在:
这是因为我们声明 InjectionKey 时,只定义了State类型,它只包含rootState的类型定义。但是State是要传给createStore的,所以我们不能直接修改它,需要创建一个同时包含module和rootState信息的接口,然后将新接口传给InjectionKey的定义。
// src/store/index.ts
export interface InjectionState extends State{
module: ModuleState,
module2: Module2State,
}
export const key: InjectionKey<Store<InjectionState>> = Symbol()
如此一来,在组件中就可以正常访问module信息了。
遗留问题
按照文章进行到现在的代码结构,我们可以正常使用vuex,但是如果在module中使用rootState大家会发现,引入rootState.rootCount没有问题;但是如果使用rootState.module2.count,会出现 [返回类型化的store]小节中同样的问题,module2在State类型中不存在。
这是因为我们的State类型其实只定义了根state包含的属性类型。但是我们的State是要传给createStore当做rootState类型声明,并且它还当做了根state的类型(它们之间的关系可以看下图),因此我们注定没有办法把它像声明InjectionKey一样加上module信息。
目前这个问题我还没有找到完美的解决方案。不过我们可以在不定义根state变量的情况下,使用InjectionKey接口传给createStore当做类型参数,并且在定义module时也引入这个接口当做Module<S, R>中的参数R,这样可以当做模块内互相访问state的一种妥协方案,具体代码需要的同学可以自己查看git项目。
代码已更新在Git仓库:github.com/YuanDaoDao0…
这个遗留问题希望有较好解决办法的大佬不吝赐教