Vue 2 落地 TypeScript 指南

8,564 阅读3分钟

引言

随着系统的用户量日益增长,对系统稳定性的要求越发严格。 为了增强系统的稳定性,我们在项目中引入 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;

交流