TypeScript在Vue的一些实践

今天无意中看到 Vue 作者尤大在知乎问题 TypeScript 不适合在 vue 业务开发中使用吗? 的回答,说 Vue2 现在确实不是很适合用 TypeScript,现有的 API 和类型系统的结合存在缺陷,存在着一些历史遗留的问题,这

目录
  1. 类的装饰库
  2. 关于 Vuex
  3. Api 请求接口
  4. 第三方库声明文件
  5. 初始值的定义
  6. $refs
  7. mixins
  8. 其他
  9. 展望
  10. 参考资料

今天无意中看到 Vue 作者尤大在知乎问题 TypeScript 不适合在 vue 业务开发中使用吗?回答,说 Vue2 现在确实不是很适合用 TypeScript,现有的 API 和类型系统的结合存在缺陷,存在着一些历史遗留的问题,这个将会在在 Vue3 中得到改善。

早在去年的时候,我就自学过一些 TypeScript 方面的知识,并且在 Vue 项目中简单试验过,感觉很好,不过因为当时的项目比较简单,很多 TypeScript 的功能和特性都没有用到。前段时间,又用 TypeScript 重构了一个比较复杂的项目,大部分的 TypeScript 功能基本上都用到了,这里总结一下。

类的装饰库#

  • vue-property-decorator装饰 Vue 的大部分属性,比如PropWatchComponentMixins等。
  • 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;
    // ...
  }
}

另外一些没有初始值的属性,一般是使用强制类型定义,忽略其中的undefinednull类型,在冒号:前面加!,主要是一些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非常有利于代码的维护和重构。现在生态也越来越好,相信在今年也会大放光彩,之后在ReactNodejs方面也要尝试着使用了。

参考资料#