今天无意中看到 Vue 作者尤大在知乎问题 TypeScript 不适合在 vue 业务开发中使用吗? 的回答,说 Vue2 现在确实不是很适合用 TypeScript,现有的 API 和类型系统的结合存在缺陷,存在着一些历史遗留的问题,这个将会在在 Vue3 中得到改善。
早在去年的时候,我就自学过一些 TypeScript 方面的知识,并且在 Vue 项目中简单试验过,感觉很好,不过因为当时的项目比较简单,很多 TypeScript 的功能和特性都没有用到。前段时间,又用 TypeScript 重构了一个比较复杂的项目,大部分的 TypeScript 功能基本上都用到了,这里总结一下。
类的装饰库#
vue-property-decorator装饰 Vue 的大部分属性,比如Prop、Watch、Component、Mixins等。vuex-class装饰 vuex。
关于 Vuex#
Vuex 是 Vue 的状态管理器,在一些复杂应用中可以方便状态的管理。因为现在使用 TypeScript 的类的模式还不支持mapGetter等语法糖,所以使用了一个封装好的装饰器库vuex-class。
- 在 store 中的使用,需要定义好接口。
// state.js 定义state的接口
export interface State {
/* code */
}
// 在Getter、Mutation、 action引用这个接口,这样就可以不用定义state类型了
import { State } from "./state";
import { GetterTree, MutationTree, ActionTree } from "vuex";
const getters: GetterTree<State, State> = {
/* code */
};
const mutations: MutationTree<State> = {
/* code */
};
const actions: ActionTree<State, State> = {
/* code */
};- 在 Vue 组件中,注意如果使用常量替代 Mutation 或者 Action 的事件类型,可以转换成驼峰的变量形式。
import * as types from "@/store/mutation-types";
import { Getter, Mutation, Action } from "vuex-class";
export default class Component extends Vue {
@Getter public count!: number;
@Mutation(types.SET_COUNT) public setCount!: (val: number) => void; // 转变成驼峰形式的方法
@Action public saveCount!: (val: number) => void;
}Api 请求接口#
请求的后端数据最好分类定义好接口,如果 vue 组件中需要为定义类型的话,还可以提取出来一些更独立的接口,这样方便类型管理。
export interface SongInterface {
songid: string;
songmid: string;
singer: SingerObject[];
songname: string;
albumname: string;
interval: number;
albummid: string;
url: string;
pay: {
payalbumprice: number;
};
musicData?: object;
data?: object;
}
export interface SongListResponse {
accessed_favbase: number;
accessed_plaza_cache: number;
cdnum: number;
code: number;
login: string;
realcdnum: number;
subcode: number;
cdlist: [
{
dissid: number;
songlist: [SongInterface];
}
];
}第三方库声明文件#
一些第三方库本身已经自带声明文件了,安装之后就可以直接使用了。
npm install axios还有一些库本身没有声明文件,但是社区已经帮它写好了。据说 npm 排名的 90%以上的包都已经写好了声明文件,放在DefinitelyTyped这个库里面,不需要你再重新编写。
使用这些声明文件也很简单,首先查询一下,在DefinitelyTyped里面是否存在这个包的声明文件,如果存在,直接安装它就行了。比如查询better-scroll库,发现已经存在声明文件了,然后安装它即可。
npm install @types/better-scroll --save-dev但是也有一些库使用的人比较少,或者因为一些其他原因,作者或者社区都没有为它提供声明文件,这时候就需要你自己编写声明文件了。
// lyric-parser库的声明
declare module "lyric-parser" {
export default class Lyric {
constructor(lrc: string, handler: (params: any) => void);
lrc: string;
tags: object;
lines: string[];
handler: () => void;
state: number;
curLine: number;
play(startTime: number, skipLast?: boolean): void;
togglePlay(): void;
stop(): void;
seek(offset: number): void;
}
}
// good-storage库的声明
declare module "good-storage" {
function get(key: string, def: any): any;
function set(key: string, val: any): void;
function remove(key: string): void;
}上面的lyric-parser库是直接暴露出一个类,所以可以直接默认声明一个类。另外,我还用到了good-storage库的几个方法,所以声明其中的方法。你可以在 src 文件夹下面创建一个vendors.d.ts的声明文件,专门用来写第三方库的声明代码。
初始值的定义#
Vue 的数据有时候是对象或者数组,因为 Vue 响应式系统的关系,需要提前定义数据,一开始定义的是可能需要是空的对象{}或者空的数组[],然后在通过请求后端获取数据再赋值填充数据,或者是后面使用时再赋值。这时候定义类型就比较麻烦。因为一开始的空值不满足相应定义的类型,会报错。
一开始解决办法是赋值为any类型,不过觉得这样约束太宽松了,后面采用了类型断言 的方式。
比如有个touch的对象,初始值是空值,后面再赋值。
interface Touch {
initiated: boolean;
moved: boolean;
startX: number;
startY: number;
percent: number;
}
export default class Component extends Vue {
public touch = {} as Touch; // 采用 as 的类型断言
public onTouchStart() {
this.touch.initiated = false;
// ...
}
}另外一些没有初始值的属性,一般是使用强制类型定义,忽略其中的undefined和null类型,在冒号:前面加!,主要是一些vuex的值和方法,还有 Vue 实例自带的一些属性或者特殊的初始值和对象等等。
@Getter public count!: number
@Action public saveCount!: (val: number) => void
public $refs!: {
listView: Scroll // 组件
listGroup: HTMLLIElement[]
fixed: HTMLElement
}
public fixedTop!: number
public timer!: number // 定时器
public scroll!: BScroll // scroll 为 BScroll 的实例对象$refs#
在 Vue 中使用$refs引用 dom 元素或者组件来操作样式或者组件的属性的方法是非常灵活好用的。但是关于$refs的定义会比较麻烦。
public $refs!: {
listView: Scroll // 组件
listGroup: HTMLLIElement[]
fixed: HTMLElement
}
// 在需要索引签名的时候,需要加上"Vue['$refs']"
public $refs!: Vue['$refs'] & {
listContent: Scroll
list: Scroll
confirm: Confirm
addSong: AddSong
}
private syncWrapperTransform(wrapper: string, inner: string) {
if (!this.$refs[wrapper]) {
return
}
// ...
}另外,因为组件需要使用$el来操作它的样式,Vue.$el默认是Element类型的,不存在style属性,这时候可以使用类型断言来解决。
const el = this.$refs.list.$el as HTMLElement;
el.style.top = "10px";mixins#
mixins在 vue 中用于对组件的复用。在vue-property-decorator库中也提供了对mixins的支持,如果同时继承多个实例,则把这些mixins都输入进去就行了。
import { Vue, Component, Mixins } from "vue-property-decorator";
export default class Component extends Mixins(Mixin) {
/* code */
}
export default class Component extends Mixins(Mixin1, Mixin2) {
/* code */
}注意,mixins定义的类型可能会和组件里面的定义的类型冲突,可以使用any来定义mixins的类型,避免冲突。
其他#
- 注意类的组件中不要漏写了
@Component装饰。 - 尽量少使用
any类型和类型断言。 - 不要滥用类型,能类型推断就不需要再定义了,不要定义无意义的类型。
展望#
TypeScript非常有利于代码的维护和重构。现在生态也越来越好,相信在今年也会大放光彩,之后在React和Nodejs方面也要尝试着使用了。