08Vue+TS实战

作者: LM林慕 | 来源:发表于2020-07-18 19:36 被阅读0次

    08Vue+TS实战

    准备工作

    新建一个基于 TS 的 Vue 项目:

    image.png

    在已存在项目中安装 TS

    vue add @vue/typescript
    

    请暂时忽略引发的几处 Error,它们不影响项目运行,我们将在后面处理它们。

    TS 特点

    TypeScript 是 JavaScript 的超集,它可编译为纯 JavaScript,是一种给 JavaScript 添加特性的语言扩展。TS 有如下特点:

    • 类型注解和编译时类型检查

    • 基于类的面向对象编程

    • 泛型

    • 接口

    • 装饰器

    • 类型声明

    image.png

    类型注解和编译时类型检查

    使用类型注解约束变量类型,编译器可以做静态类型检查,使程序更加健壮。

    基础类型

    // ts-test.ts
    let var1: string  // 类型注解
    var1 = "林慕" // 正确
    var1 = 4  // 错误
    

    编译器类型推断可省略这个语法

    let var2 = true
    

    常见的原始类型:

    • string

    • number

    • boolean

    • undefined

    • null

    • symbol

    类型数组

    let arr: string[]
    arr = ['林慕']  // 或 Array<string>
    

    任意类型 any

    let varAny: any
    varAny = 'xx'
    varAny = 3
    

    任意类型也可用于数组

    let arrAny: any[]
    arrAny = [1,true,'free']
    arrAny [1] = 100
    

    函数中的类型约束

    function greet(person: string): string{
      return 'hello,'+person
    }
    

    void 类型,常用于没有返回值的函数

    function warn(): void{}
    

    范例:HelloWorld.vue

    <template>
     <div>
      <ul>
       <li v-for="feature in features" :key="feature">{{feature}}</li>
      </ul>
     </div>
    </template>
    <script lang='ts'>
    import { Component, Prop, Vue } from "vue-property-decorator";
    @Component
    export default class Hello extends Vue {
     features: string[] = ["类型注解", "编译型语言"];
    }
    </script>
    

    类型别名

    使用类型别名自定义类型。

    可以用下面这种方式定义对象类型:

    const objType: {foo:string,bar:string}
    

    使用 type 定义类型别名,使用更便捷,还能复用:

    type Foobar = { foo: string, bar: string }
    const aliasType: Foobar
    

    范例:使用类型别名定义 Feature,types/index.ts

    export type Feature = {
      id: number,
      name: string
    }
    

    使用自定义类型,HelloWorld.vue

    <template>
     <div>
      <!--修改模板-->
      <li v-for="feature in features" :key="feature.id">{{feature.name}}</li>
     </div>
    </template>
    <script lang='ts'>
    // 导入接口
    import { Feature } from '@/types'
    
    @Component
    export default class Hello extends Vue {
      // 修改数据类型
      features: Feature[] = [{ id: 1, name: '类型注解' }]
    }
    </script>
    

    联合类型

    希望某个变量或参数的类型是多种类型其中之一。

    let union: string | number
    union = '1'
    union = 1
    // 以上都是可以的
    

    交叉类型

    想要定义某种由多种类型合并而成的类型使用交叉类型。

    type First = {first: number}
    type Second = {second: number}
    // fas将同时拥有属性first和second
    type fas = First & Second
    

    范例:利用交叉类型给 Feature 添加一个 selected 属性。

    // types/index.ts
    type Select = {
      selected: boolean
    }
    export type FeatureSelect = Feature & Select
    

    使用这个 FeatureSelect,HelloWorld.vue

    features: FeatureSelect[] = [
      { id: 1, name: '类型注解', selected: false },
      { id: 2, name: '编译型语言', selected: true }
    ]
    
    <li :class="{selected: feature.selected}">{{feature.name}}</li>
    
    .selected {
      background-color: rgb(168, 212, 247)
    }
    

    函数

    必填参:参数一旦声明,就要求传递,且类型需符合。

    // 02-function.ts
    function greeting(person: string): string {
      return 'hello,' + person
    }
    greeting('tom')
    

    可选参数:参数名后面加上问号,变成可选参数。

    function greeting(person: string, msg?: string):string {
      return 'hello,' + person
    }
    

    默认值:

    function greeting(person: string, msg = ''): string{
      return 'hello,' + person
    }
    

    函数重载:以参数数量或类型区分多个同名函数。

    // 重载1
    function watch(cb: () => void): void
    // 重载2
    function watch(cb1: () =>void,cb2:(v1:any, v2:any) => void): void
    // 实现
    function watch(cb1: () => void, cb2?: (v1: any, v2: any) => void){
      if(cb1 && cb2){
        console.log('执行watch重载2')
      } else {
        console.log('执行watch重载1')
      }
    }
    

    范例:新增特性,Hello.vue

    <div>
      <input type="text" placeholder="输入新特性" @keyup.enter="addFeature">
    </div>
    
    addFeature(e: KeyboardEvent) {
      // e.target 是 EventTarget 类型,需要断言为 HTMLInputElement
      const inp = e.target as HTMLInputElement
      const feature: FeatureSelect = {
        id: this.features.length + 1,
        name: inp.value,
        selected: false
      }
      this.features.push(feature)
      inp.value = ''
    }
    

    范例:生命周期钩子,Hello.vue

    created() {
      this.features = [{ id:1, name: '类型注解' }]
    }
    

    class 的特性

    TS 中的类和 ES6 中大体相同,这里重点关注 TS 带来的访问控制等特性。

    // 03-class.ts
    class Parent {
      private _foo = 'foo'  // 私有属性,不能在类的外部访问
      protected bar = 'bar'  // 保护属性,可以在子类中使用
    
      // 参数属性:构造函数参数加修饰符,能够定义为成员属性
      constructor(public tua = 'tua') {}
    
      // 方法也有修饰符
      private someMethod() {}
    
      // 存取器:属性方式访问,可添加额外逻辑,控制读写性
      get foo() {
        return this._foo
      }
      set foo(val) {
        this._foo = val
      }
    }
    

    范例:利用 getter 设置计算属性,Hello.vue

    <template>
      <li>特性数量:{{count}}</li>
    </template>
    <script lang='ts'>
      export default class HelloWorld extends Vue {
        // 定义getter作为计算属性
        get count() {
          return this.features.length
        }
      }
    </script>
    

    接口

    接口仅约束结构,不要求实现,使用更简单

    // 04-interface
    // Person接口定义了结构
    interface Person {
      firstName: string;
      lastName: string;
    }
    // greeting函数通过Person接口约束参数解构
    function greeting(person: Person) {
      return 'Hello,' + person.firstName + ' ' + person.lastName
    }
    greeting({firstName: 'Jane', lastName: 'User'}) // 正确
    greeting({firstName: 'Jane'})  // 错误
    

    范例:Feature 也可用接口形式约束,./types/index.ts

    接口中只需定义结构,不需要初始化

    export interface Feature {
      id: number;
      name: string;
    }
    

    Interface vs type aliases

    泛型

    泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。以此增加代码通用性

    不使用泛型

    interface Result {
      ok: 0 | 1;
      data: Feature[];
    }
    

    使用泛型

    interface Result<T> {
      ok: 0 | 1;
      data: T;
    }
    

    泛型方法

    function getResult<T>(data: T): Result<T> {
      return {ok:1, data}
    }
    
    // 用尖括号方式指定T为string
    getResult<string>('hello')
    // 用类型推断指定T为number
    getResult(1)
    

    泛型优点

    • 函数和类可以支持多种类型,更加通用;

    • 不必编写多条重载,冗长联合类型,可读性好;

    • 灵活控制类型约束;

    不仅通用且能灵活控制,泛型被广泛用于通用库的编写。

    范例:用 axios 获取数据

    配置模拟一个接口,vue.config.js

    module.exports = {
     devServer: {
       before(app) {
         app.get('/api/list', (req, res) => {
           res.json([
             { id: 1, name: "类型注解", version: "2.0" },
             { id: 2, name: "编译型语言", version: "1.0" }
            ])
         })
       }
     }
    }
    

    使用接口,HelloWorld.vue

      async mounted () {
        const resp = await axios.get<FeatureSelect[]>('/api/list')
        this.features = resp.data
      }
    

    声明文件

    使用 TS 开发时如果要使用第三方 JS 库的同时还想利用 TS 诸如类型检查等特性就需要声明文件,类似 xx.d.ts。

    同时,Vue 项目中还可以在 shims-vue.d.ts 中对已存在模块进行补充。

    npm i @types/xxx
    

    范例:利用模块补充 $axios 属性到 Vue 实例,从而在组件里面直接用。

    // main.ts
    import axios from 'axios'
    Vue.prototype.$axios = axios
    
    // shims-vue.d.ts
    import Vue from 'vue'
    import { AxiosInstance } from 'axios'
    
    declare module 'vue/types/vue' {
      interface Vue {
        $axios: AxiosInstance;
      }
    }
    

    范例:给 router/index.js 编写声明文件,index.d.ts

    import VueRouter from 'vue-router'
    declare const router: VueRouter
    export default router
    

    装饰器

    装饰器用于扩展类或者它的属性和方法,@xxx 就是装饰器的写法。

    属性声明:@Prop

    除了在 @Component 中声明,还可以采用 @Prop 的方式声明组件属性。

    export default class HelloWorld extends Vue {
      // Props() 参数是为 Vue 提供属性选项,加括号说明 prop 是一个装饰器工厂,返回的才是装饰器,参数一般是配置对象
      // !称为明确赋值断言,它是提供给 TS 的
      @Prop({type: String, required: true})
      private msg!: string  // 这行约束是写给 TS 编译器的
    }
    

    事件处理:@Emit

    新增特性时派发事件通知,Hello.vue

    // 通知父类新增事件,若未指定事件名则函数名作为事件名(羊肉串形式)
    @Emit()
    private addFeature(event: any){  // 若没有返回值形参将作为事件参数
      const feature = { name: event.target.value, id: this.features.length + 1}
      this.features.push(feature)
      event.target.value = ''
      return feature  // 若有返回值则返回值作为事件参数
    }
    

    变更监测:@Watch

    @Watch('msg')
    onMsgChange(val:string, oldVal:any){
      console.log(val,oldVal)
    }
    

    状态管理推荐使用:vuex-module-decorators

    vuex-module-decorators 通过装饰器提供模块化声明 Vuex 模块的方法,可以有效利用 TS 的类型系统。

    安装

    npm i vuex-modulw-decorators -D
    

    根模块清空,修改 store/index.ts

    export default new Vuex.Store({})
    

    定义 counter 模块,创建 store/counter

    import { Module, VuexModule, Mutation, Action, getModule } from 'vuex-module-
    decorators'
    import store from './index'
    // 动态注册模块
    @Module({ dynamic: true, store: store, name: 'counter', namespaced: true })
    class CounterModule extends VuexModule {
      count = 1
      @Mutation
      add () {
        // 通过this直接访问count
        this.count++
      }
      // 定义getters
      get doubleCount () {
        return this.count * 2;
      }
      @Action
      asyncAdd () {
        setTimeout(() => {
          // 通过this直接访问add
          this.add()
        }, 1000);
      }
    }
    // 导出模块应该是getModule的结果
    export default getModule(CounterModule)
    

    使用,App.vue

    <p @click="add">{{$store.state.counter.count}}</p>
    <p @click="asyncAdd">{{count}}</p>
    
    import CounterModule from '@/store/counter'
    @Component
    export default class App extends Vue { 
     get count() {
      return CounterModule.count
    }
     add() {
      CounterModule.add()
    }
     asyncAdd() {
      CounterModule.asyncAdd()
    }
    }
    

    装饰器原理

    装饰器是加工厂函数,他能访问和修改装饰目标。

    类装饰器:类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。

    function log(target: Function){
      // target 是构造函数
      console.log(target === Foo) // true
      target.prototype.log = function() {
        console.log(this.bar)
      }
    }
    
    @log
    class Foo{
      bar = 'bar'
    }
    
    const foo = new Foo()
    // @ts-ignore
    foo.log()
    

    方法装饰器

    function rec (target: any, name: String, descriptor: any) {
      // 这里通过修改descriptor.value扩展了bar方法
      const baz = descriptor.value
      descriptor.value=function(val: string){
        console.log('run method',name)
        baz.call(this,val)
      }
    }
    
    class Foo{
      @rec
      setBar(val:string){
        this.bar = val
      }
    }
    
    foo.setBar('lalala')
    

    属性装饰器

    function mua(target,name){
      target[name]='mua~'
    }
    
    class Foo{
      @mua ns!:string
    }
    
    console.log(foo.ns)
    

    稍微改造一下使其可以接收参数:

    function mua(params:string){
      return function (target, name){
        target[name] = param
      }
    }
    

    实战一下:

    
    <template>
      <div>{{ msg }}</div>
    </template>
    <script lang='ts'>
    import { Vue } from "vue-property-decorator";
    function Component(options: any) {
      return function(target: any) {
        return Vue.extend(options);
      };
    }
    @Component({
      props: {
        msg: {
          type: String,
          default: ""
        }
      }
    })
    export default class Decor extends Vue {}
    </script>
    

    显然 options 中的选项都可以从 Decor 定义中找到。

    参考资料

    1. TypeScript:https://www.typescriptlang.org/

    2. TypeScript 支持 Vue:https://cn.vuejs.org/v2/guide/typescript.html

    相关文章

      网友评论

        本文标题:08Vue+TS实战

        本文链接:https://www.haomeiwen.com/subject/aafxkktx.html