TypeScript 在 Vue 的实践

前言

在 vue-cli 3.0 的脚手架出来以后,官方我们提供了一套 Vue 的 TypeScript 模板,解决了许多模块以及类型问题,官方的东西真香,因此可以使用 TypeScript 搞一波事情。

基础配置

code-7.png

配置默认是全家桶,其中预处理器建议使用 less,如果使用 sass 可能会因为各种莫名其妙的原因安装不上 node-sassbabel 也是必选的,目的是将 TypeScript 编译后的代码转变成 ES5 的代码,提供低版本浏览器支持。

code-8.png

VScode 的插件配置,基本上安装 TypeScript Extension Pack 这个插件以后附带的几个插件够用了(我是一个强迫症,能少安装插件就尽量少安装插件)。然后需要额外安装一个 TSlint Vue 插件,因为 VScode 对 .vue 单文件的支持并不是很好,TSlint 不能有效纠错,需要这个插件配合。

code-10.png

这是生成的默认配置。其中 tsconfig.json 里会设置 src/xxx 的别名为 @/xxx,但是 VScode 是不能识别的,所以需要自行新建一个 jsconfig.json 文件。

// jsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "module": "commonjs",
    "paths": {
      "@/*":["./src/*"]
    }
  }
}

开始开发

基础使用

import { Vue, Component, Prop, Watch } from 'vue-property-decorator'
import { State, Getter, Mutation, Action } from 'vuex-class'
import { Bind, Debounce } from 'lodash-decorators'
import { UBT } from '@/decorator'
import HelloWorld from '@/components/HelloWorld.vue'
import BasicMixin from '@/mixin/PrintMixin'

@Component({
  components: {
    HelloWorld
  },
  mixins: [BasicMixin]
})
export default class EleComponent extends Vue {
  @Prop({ default: 'Hello' })
  private text!: string

  @State(state => state.user)
  private user!: string

  @Getter('email')
  private email!: string

  private msg: string = 'Hello Element'
  private name: string = 'Typescript'

  private get userInfo (): string {
    return this.text + this.name
  }
    private set userInfo (val: string) {
    this.text = val
  }

  @Mutation('setUserEmail')
  private setUserEmail!: (email: string) => void

  @Action('getUserInfo')
  private getUserInfo!: (params: {token: string}) => Promise<any>

  @Watch('name', { deep: true })
  private onNameChange () {
    console.log('name has been changed!')
  }
  
  @UBT('click', Date.now())
  @Bind()
  @Debounce(300)
  private handleClick () {
    console.log('click', this.text)
  }
}

templatestyle 部分和普通的 js 差不多,这里只贴出 script 部分的代码。基本上就是把传统的配置对象改为了基于 class 的组件,传递的 props、watch、computed 以及 Vuex 的相关属性都通过装饰器实现。

vue-property-decorator 提供 Vue 基本属性的实现。注意一定要使用 @compoenet 去修饰这个组件,否则其它的装饰器无法正常使用。@component({option}) 中接收的参数 option 就是传统的配置,mixin 和子组件的注册都要在这里声明。 vuex-class 提供的是与 Vuex 相关的装饰器,具体用法参考文档。美中不足的是,Store 的定义还是基于配置的,因此 TypeScript 无法正确推导出其方法的签名,并且通过装饰器在组件中声明的方法也是没有签名,所以在组件中需要自行补上方法的签名。

最后一部分实现了一个方法 handleClick 并且使用了三个装饰器进行修饰。主要的目的是实现点击事件的防抖,lodash-decorators 提供了相关的装饰器。传统的 vue 组件如果需要实现的一个防抖事件需要这样写

{
    methods: {
        debounceClick: _.debounce(this.handleClick, 300)
    }
}

这样做是为了 this 指向正确,Vue 会自动为 methods 中的方法绑定 this,但是这样的实现既不优雅也不通用,基于 class 的组件我们只需要 BindDebounce 两个装饰器就能完成,并且在 React 中也是通用的

使用 Mixin

mixin 在 Vue 中使用到的场景很多,其目的是在组件中复用相同的功能代码,但是这种实现并不优雅,它仅仅是功能上实现复用,结构上并没有拓展功能,并且会破坏组件原有的结构,特别是基于 class 的组件。不过传统的 Vue 组件使用 JavaScript 这种类型推断本来就没有,所以显得不重要。希望 Vue 3.0也能像 React 一样实现通过 HOC 复用代码。在 TypeScript 中,不能再像原来一样写基于配置的 mixin 对象,而应该也写为一个 Vue 的子类:

import { Vue, Component } from 'vue-property-decorator'

export default class BasicMixin extends Vue {
  private msg!: string

  private printWords (): void {
    console.log(this.msg)
  }
}

在需要的注入的组件中通过 @component 注入,需要注意的是,如果注入的 class 需要使用被注入组件的属性,需要通过 priavte msg!: string 强制断言属性存在,才能正常使用;同理,如果组件需要使用注入类的方法,也要强制断言。如果只是 template 中使用方法,那么不需要强制断言

填坑指南

  1. VScode 插件配置 TSLint Vue
  2. mixin 的相关配置
  3. Vuex 方法的接口实现
  4. 复用接口的摆放位置 使用了 TypeScript 以后必然需要声明许多接口。个人觉得有必要定义的接口有:
    1. 后台返回的数据结构,这样能够避免每次都打开 network 看返回的数据结构格式;
    2. 组件内部复用的数据结构,一些数据结构是前端生成的并且在多个组件复用,这些需要提取出来写成接口; 在接口文件存储的位置上一般分为两类:
    3. 统一定义在 @/interface 通用的接口提取出来放到这个地方;
    4. API 请求文件中,我按照页面的粒度分离了请求 API 的方法,页面级的接口文件也定义在这里,这样在导入请求方法时也可以同时导入接口声明;
  5. get set 的使用 TypeScript 中不再使用 computed 定义计算属性,而是通过 class 本身的 get set 定义,使用的方式和原来相同
  6. 路由的组件导航守卫失效 路由的导航钩子不属于 Vue 本身,这会导致 class 组件转义到配置对象时导航钩子无效,因此如果要使用导航钩子需要在 router 的配置里声明
  7. axios 填坑 使用 axios 请求数据时,它会将数据再包裹一个 AxiosPromise 的接口,具体实现是
export interface AxiosPromise<T = any> extends Promise<AxiosResponse<T>> {
}

export interface AxiosResponse<T = any>  {
  data: T;
  status: number;
  statusText: string;
  headers: any;
  config: AxiosRequestConfig;
  request?: any;
}

通常我们会在 axios.interceptors.response.use 这个拦截方法中取出 res.data,但是这样会导致 axios 返回数据的类型推断失败(即使取出来了,axios 还是会认为返回的是 AxiosPromise ),因此需要这样定义:

export const GET_CITY = (form: IQuery, { target, page, rows }: IParams): Promise<IRes<IRankList>> => {
  const query = Object.assign({}, form, { target, page, rows })
  return ajax.post(URL.city, query).then(res => res.data)
}

const res: IRes<IRankList> = await GET_CITY()

每个返回的结构都需要手动 then(res => res.data) 这样返回的才是 Promise<T>, await 才能正确的取出结构

总结

目前看来 Vue 对 TyepeScript 的支持并不算完善,因为 2.x 版本的 Vue 即使写了 class-compoent,最终也会被编译成配置式的组件。许多 Vue 中方便的 API 以及 Vuex 的方法也只能通过装饰器实现,这导致了方法签名的丢失;通过 ref 属性获取到的子组件实例的类型也不正确,只是一个普通的 Vue 实例并不是定义的 class 类型(在组件内部通过 private public 定义的方法,父组件调用时是无法使用的,React 则实现了这个功能);子组件需要的参数声明也不具有强制性,参考 React 组件参数传递是具有强约束力并且能静态检测,目前 Vue 仍然是在运行时抛出

不过好消息是,Vue 3.0 将采用 TypeScript 重构,全新的 Vue 不仅带来性能上的提升,还会进一步提升对类型的支持。未来,class-compoent 也将成为主流,现在写 TypeScript 以后进行 3.0 的迁移会更加方便。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券