引言
随着系统的用户量日益增长,对系统稳定性的要求越发严格。 为了增强系统的稳定性,我们在项目中引入 TypeScript,利用其静态类型检查能力,大大减少非预期错误。 下文介绍如何在 Vue 2 项目中渐进式引入 TypeScript。
技术选型
由于 Vue 2 采用了 object-based 的风格进行组件开发,导致 Options 中方法的 this 不是预期的类型。 此外,它还支持通过 mixin、原型链、optionMergeStrategies 等方式向组件添加额外内容,带来极大的不确定性。
Vue class component
在 Vue 3 以前,官方推荐使用 vue-class-component 来提供类型推导支持。 该方案主要是通过 class 与 decorator 语法的结合,解决了 this 类型无法推导的问题。然而,深度依赖 decorator 语法又带来了新的问题。 decorator 语法尚未纳入 ECMA-262 标准,其历经多个版本至今仍停留在 stage 2 阶段,而且第三版是对前两版的推倒重来。 Vue 3 放弃了 class-based component 而转向 composition api,也有部分原因源于 decorator 前景不明。
Composition API
目前,Vue 3 主推 composition api,并且为 Vue 2 提供了相应的插件。 该方案不再基于 this 进行编程,也就不存在前面说的问题。 而且函数式 API 对类型推导支持良好,代码组织方式十分灵活,这既是优点也是缺点。(相信我们的同学有能力驾驭它的灵活性) 此外,原有 options api 风格的代码逻辑关注点分散,强依赖于 this,不利于代码的拆分与复用,composition api 正好能解决这个问题。
(注:网图,忘了在哪篇文章找到的了 🤣 侵删 )
综上可见,vue-class-component 方案存在较大风险,官方推荐的 composition api 是个更佳的选择,同时还便于向 Vue 3 演进。
主要改造点
启用 TypeScript
除了将文件后缀由 .js 改为 .ts 外,在 .vue 组件中需设置
<script lang="ts">
// ...
</script>
defineComponent
Vue 组件选项在导出时,需经由 defineComponent 处理,以便 setup 的 props 类型被正确推导。
import { defineComponent } from '@vue/composition-api';
export default defineComponent({
components: {
// ...
} as any,
props: {
// ...
},
setup(props) {
// ...
},
});
从实现上看,defineComponent 只返回传递给它的对象。但是就类型而言,返回的值有一个合成类型的构造函数,可用于手动渲染函数、TSX 和 IDE 工具支持。
Props 类型
使用 PropType 对复杂类型进行标注。
import { PropType } from 'vue';
export default defineComponent({
props: {
a: {
type: Number,
},
b: {
type: Array as PropType<string[]>,
},
c: {
type: Function as PropType<() => void>,
},
},
});
Vuex
改造 store
导出模块的 state、getters、mutations、actions 类型
// "store/user-module.ts"
import { ActionContext } from 'vuex';
const state = {
a: null as Record<string, any>,
b: [] as any[],
};
type State = typeof state;
const getters = {
c(state: State) {},
};
const mutations = {
d(state: State, payload: string) {},
};
const actions = {
e(context: ActionContext<State, RootState>, payload: boolean) {},
};
export interface User {
state: State;
getters: typeof getters;
mutations: typeof mutations;
actions: typeof actions;
}
创建 state、getters、mutations、actions 的根类型
// "store/index.ts"
import { User } from './user-module.ts';
export interface RootState {
stateA: number;
user: User['state'];
}
export interface RootGetters {
getterA: () => string;
user: User['getters'];
}
export interface RootMutations {
mutationA: () => string;
user: User['mutations'];
}
export interface RootActions {
actionA: () => string;
user: User['actions'];
}
创建并导出带有类型信息的辅助函数 useState、useGetters、useMutations、useActions
// "utils/vuex-helpers.ts"
import { createVuexHelpers } from 'vue2-helpers';
import { RootActions, RootGetters, RootMutations, RootState } from '/store/index.ts';
export const {
useState, useGetters, useMutations, useActions,
} = createVuexHelpers<
RootState, RootGetters, RootMutations, RootActions
>();
使用 store
this.$store 的新用法
import { useStore } from 'vue2-helpers/vuex';
export default defineComponent({
setup() {
const store = useStore();
const { stateB } = store.state;
},
});
替代辅助函数 mapState、mapGetters、mapMutations、mapActions
import {
useState, useGetters, useMutations, useActions,
} from 'path/to/vuex-helpers.ts';
export default defineComponent({
setup() {
const { stateB } = useState('moduleA', ['stateB']);
const { getterB } = useGetters('moduleA', ['getterB']);
const { mutationB } = useMutations('moduleA', ['mutationB']);
const { actionB } = useActions('moduleA', ['actionB']);
return { stateB, getterB, mutationB, actionB };
},
});
vue2-helpers/vuex 提供部分 vuex next 的 API。倘若有朝一日升级到 Vue3,只需将“vue2-helpers/vuex”替换为“vuex”即可。
Vue Router
替代 this.$route、this.$router
import { useRoute, useRouter } from 'vue2-helpers/vue-router';
export default defineComponent({
setup() {
const route = useRoute();
const p = encodeURIComponent(route.fullPath);
const router = useRouter();
return {
toLogin: () => router.push(`/login?r=${p}`),
};
},
});
替代 beforeRouteLeave、beforeRouteUpdate
import {
onBeforeRouteLeave,
onBeforeRouteUpdate,
} from 'vue2-helpers/vue-router';
export default defineComponent({
setup() {
onBeforeRouteLeave((to, from) => {
// ...
});
onBeforeRouteUpdate((to, from) => {
// ...
});
},
});
vue2-helpers/vue-router 提供部分 vue-router next 的 API。迁移到 Vue3,只需将“vue2-helpers/vue-router”替换为“vue-router“即可。
FAQ
setup 入参 props 被识别成 unknown 类型
由于多数组件未提供正确的类型声明,使得选项 components 不是预期的类型,影响 TS 类型推导。 建议将 components 断言为 any 类型。
export default defineComponent({
components: {
// ...
} as any,
});
类型断言 <Type> 被识别成 JSX 语法
改用 as 进行断言。
- const foo = <Foo> bar;
+ const foo = bar as Foo;
交流
- 邮箱:ambit_tsai@qq.com
- 微信:cai_fanwei
- QQ群: 663286147