如何使用TypeScript编写Vue应用

7,091 阅读3分钟

前言

都2020了,本小姐姐还没开始在项目中用上ts,趁着在家闲的慌,初尝ts。下面我将展示如何使用Vue CLI 构建一个Vue + TypeScript应用

初始化项目

Vue create ts-vue

选择 Manually select features 我这里选择了Babel, TypeScript, Router, Vuex,CSS Pre-processors,Linter / Formatter WechatIMG84.png 然后根据提示安装,最终配置如下 2.png 安装完后,根据提示操作

cd ts-vue
npm run serve

在浏览器打开地址 "http://localhost:8080/",可以看到项目已经跑起来啦

项目结构

├── public
│   ├── favicon.ico
│   └── index.html
├── src
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   └── HelloWorld.vue
│   └── views
│       ├── About.vue
│       └── Home.vue
│   ├── App.vue
│   ├── main.ts
│   ├── router.ts
│   ├── shims-tsx.d.ts
│   ├── shims-vue.d.ts
│   ├── store.ts
├── eslintrc.js
├── babel.config.js
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json

shims-tsx.d.ts:允许在vue项目中使用.tsx文件

shims-vue.d.ts:允许导入和使用.vue单个文件组件,因为typescript默认并不支持导入vue文件

本文主要从以下五个方面介绍如何使用TypeScript编写

  1. Class-based components
  2. Data, props, computed properties, methods, watchers, and emit
  3. Lifecycle hooks
  4. Mixins
  5. Vuex

1.Class-based components

// ts
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
@Component
export default class HelloWorld extends Vue {
}
</script>

与ts等效的js代码是:

<script>
export default {
  name: 'HelloWorld'
}
</script>

为了使用TypeScript,需要在script标签上添加 lang = ts

vue-property-decorator是一个第三方包,它使用官方的vue-class-component软件包,并在其之上添加更多装饰器。我们还可以使用name属性来命名组件,但是将其用作类名就足够了

vue-property-decorator

@component({
  name: 'HelloWorld'
})

2.导入组件 在一个组件里面引入其他组件的写法如下:

<template>
  <div class="main">
    <User />
  </div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import User from '@/components/User.vue'
@Component({
  components: {
    User
  }
})
export default class HelloWorld extends Vue {
}
</script>

与ts等效的js代码是:

<template>
  <div class="main">
    <User />
  </div>
</template>
<script>
import User from '@/components/User.vue'
export default {
  name: 'HelloWorld',
  components: {
    User
  }
})
</script>

2. Data, props, computed properties, methods, watchers

使用data
@Component
export default class HelloWorld extends Vue {
  private msg: string = "welcome to my app"
  private list: Array<object> = [
    {
        name: 'Melody',
        age: '20'
    },
    {
        name: 'James',
        age: '20'
    }
  ]
}

与ts等效的js代码

export default {
  data() {
    return {
      msg: "welcome to my app",
      list: [
        {
          name: 'Melody',
          age: '20'
        },
        {
          name: 'James',
          age: '20'
        }
      ]
    }
}
使用props

可以添加 required, default, type 为props指定验证要求,同样也可以使用 readonly 禁止操作props

import { Component, Prop, Vue } from 'vue-property-decorator'
@Component
export default class HelloWorld extends Vue {
  @Prop() readonly msg!: string
  @Prop({default: 'Joy Melody'}) readonly name: string
  @Prop({required: true}) readonly age: number,
   @Prop(String) readonly address: string
  @Prop({required: false, type: String, default: 'Developer'}) readonly job: string
}
</script>

与ts等效的js代码如下

export default {
  props: {
    msg,
    name: {
      default: 'Joy Melody'
    },
    age: {
      required: true,
    },
    address: {
      type: String
    },
    job: {
      required: false,
      type: string,
      default: 'Developer'
    }
  }
}

Computed 属性

计算属性一般用于编写简单的模版逻辑

export default class HelloWorld extends Vue {
  get fullName(): string {
    return this.first+ ' '+ this.last
  }
}

与ts等效的js代码如下

export default {
  fullName() {
    return this.first + ' ' + this.last
  }
}

当需要写一个稍微复杂点的涉及到setter和getter的 computed属性时,在ts中写法如下

export default class HelloWorld extends Vue {
  get fullName(): string {
    return this.first+ ' '+ this.last
  }
  set fullName(newValue: string) {
    let names = newValue.split(' ')
    this.first = names[0]
    this.last = names[names.length - 1]
  }
}

与ts等效的js写法如下

fullName: {
  get: function () {
    return this.first + ' ' + this.last
  },
  set: function (newValue) {
    let names = newValue.split(' ')
    this.first = names[0]
    this.last = names[names.length - 1]
  }
}
Watch

@Watch(path: string, options: WatchOptions = {})

  • @Watch 装饰器接收两个参数:path: string 被侦听的属性名
  • options?: WatchOptions={} options可以包含两个属性
    • immediate?:boolean 侦听开始之后是否立即调用该回调函数
    • deep?:boolean 被侦听的对象的属性被改变时,是否调用该回调函数
@Watch('child')
onChildChanged (val: string, oldVal: string) {
    if (val !== oldVal) {
      window.console.log(val)
    }
}

与ts等效的js代码如下

watch: {
    'child': {
        handler: 'onChildChanged',
        immediate: false,
        deep: false 
    }
},
method: {
    onChildChanged(val, oldVal) {
        if (val !== oldVal) {
          window.console.log(val)
        }
    }
}

也可以写成: @Watch('child', { immediate: true, deep: true }), 等价于:

watch: {
    'child': {
        handler: 'onChildChanged',
        immediate: true,
        deep: true 
    }
},
method: {
    onChildChanged(val, oldVal) {
        if (val !== oldVal) {
          window.console.log(val)
        }
    }
}

Methods
export default class HelloWorld extends Vue {
  public clickMe(): void {
    console.log('clicked')
    console.log(this.addNum(4, 2))
  }
  public addNum(num1: number, num2: number): number {
    return num1 + num2
  }
}

与ts等效的js代码如下

export default {
  methods: {
    clickMe() {
      console.log('clicked')
      console.log(this.addNum(4, 2))
    }
    addNum(num1, num2) {
      return num1 + num2
    }
  }
}
Emit

子组件触发父组件的自定义事件并传递数据,在TypeScript中使用@Emit 装饰器

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

@Component
export default class YourComponent extends Vue {
  count = 0

  @Emit()
  addToCount(n: number) {
    this.count += n
  }

  @Emit('reset')
  resetCount() {
    this.count = 0
  }

  @Emit()
  returnValue() {
    return 10
  }

  @Emit()
  onInputChange(e) {
    return e.target.value
  }

  @Emit()
  promise() {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(20)
      }, 0)
    })
  }
}

与ts等效的js代码如下

export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    addToCount(n) {
      this.count += n
      this.$emit('add-to-count', n)
    },
    resetCount() {
      this.count = 0
      this.$emit('reset')
    },
    returnValue() {
      this.$emit('return-value', 10)
    },
    onInputChange(e) {
      this.$emit('on-input-change', e.target.value, e)
    },
    promise() {
      const promise = new Promise(resolve => {
        setTimeout(() => {
          resolve(20)
        }, 0)
      })

      promise.then(value => {
        this.$emit('promise', value)
      })
    }
  }
}

3.Lifecycle hooks

由于生命周期挂钩是自动调用的,因此它们既不带参数也不返回任何数据,所以,不需要访问修饰符 输入参数或者返回类型

export default class HelloWorld extends Vue {
  mounted() {
    //do something
  }
  beforeUpdate() {
    // do something
  }
}

与ts等效的js代码

export default {
  mounted() {
    //do something
  }
  beforeUpdate() {
    // do something
  }
}

4.Mixins

假设当前已经有一个mixins/ProjectMixin文件 如何在其他组件里面使用方式如下

<template>
  <div class="project-detail">
    {{ projectDetail }}
  </div>
</template>
<script lang="ts">
import { Component, Vue, Mixins } from 'vue-property-decorator'
import ProjectMixin from '@/mixins/ProjectMixin'
@Component
export default class Project extends Mixins(ProjectMixin) {
  get projectDetail(): string {
    return this.projName + ' ' + 'HS'
  }
}
</script>

与ts等效的js代码如下

<template>
  <div class="project-detail">
    {{ projectDetail }}
  </div>
</template>
<script>
import ProjectMixin from '@/mixins/ProjectMixin'
export default {
  mixins: [ ProjectMixin ],
  computed: {
    projectDetail() {
      return this.projName + ' ' + 'HS'
    }
  }
}
</script>

5.vuex

阅读本节前,先了解什么是vuex命名空间

首先安装

npm install vuex-module-decorators -D
npm install vuex-class -D

如果想通过名字空间的形式来使用module, 需在@Module装饰器中添加额外的参数. 例如, 以下示例代码中添加一个namespaced为user的module

import { VuexModule, Module, Mutation, Action } from 'vuex-module-decorators'
@Module({ namespaced: true, name: 'user' })
class User extends VuexModule {
  public name: string = ''
  @Mutation
  public setName(newName: string): void {
    this.name = newName
  }
  @Action
  public updateName(newName: string): void {
    this.context.commit('setName', newName)
  }
}
export default User
注意

@Module装饰器的属性字段name值, 必须与new store({modules: {}})中注册module name名称保持一致. 与ts等效的js代码

export default {
  namespaced: true,
  state: {
    name: ''
  },
  mutations: {
    setName(state, newName) {
      state.name = newName
    }
  },
  actions: {
    updateName(context, newName) {
      context.commit('setName', newName)
    }
  }
}

在组件里面使用vuex

要使用Vuex,可以使用vuex-class库。该库提供了装饰器,可以在Vue组件中绑定State,Getter,Mutation和Action。由于已经使用了命名空间的Vuex模块,因此我们首先从vuex-class导入命名空间,然后传递模块名称以访问该模块

</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { namespace } from 'vuex-class'
const user = namespace('user')
@Component
export default class User extends Vue {
  @user.State
  public name!: string

  @user.Getter
  public nameUpperCase!: string

  @user.Action
  public updateName!: (newName: string) => void
}
</script>

与ts等效的js代码如下

<template>
  <div class="details">
    <div class="username">User: {{ nameUpperCase }}</div>
    <input :value="name" @keydown="updateName($event.target.value)" />
  </div>
</template>
<script>
import { mapState, mapGetters, mapActions} from 'vuex'
export default  {
  computed: {
    ...mapState('user', ['name']),
    ...mapGetters('user', ['nameUpperCase'])
  }  
  methods: {
    ...mapActions('user', ['updateName'])
  }
}
</script>

参考

Vue & TypeScript 初体验 - 使用Vuex

vuex-module-decorators

vue-class-component

vue-property-decorator

vuex-class