如何用 Typescript 写一个完整的 Vue 应用程序

24,546 阅读7分钟

原文地址: How to write a Vue.js app completely in TypeScript

原文作者:Preetish HS

译者:Gopal

译者推荐:TypescriptVue 都是现在前端必备的知识,本文基本覆盖了目前 Vue 2.x 的一些基础用法的 Typescript 版本实现,感兴趣的了解一下,更好的迎接 Vue 3.0

译者根据文章中提到的知识点,自己写了一个 demo,已放到 Github 地址 ,建议大家可以动手实战一下,如果遇到什么问题,可以留言评论或者提 issue

Vue 是一个惊人的,轻量的渐进式前端框架。因为 Vue 是灵活的,所以用户不需要使用 Typescript。但是不像 Angular,老版本的 Vue 并没有很好的支持 Typescript。因为这点,大多数 Vue 应用程序都是直接使用 JavaScript 写的。

现在随着官方对 Typescript 的支持,使用 Vue CLI 可以从头开始创建 Typescript 项目。但是我们仍然需要一些带有自定义装饰器和功能的第三方包来创建一个真正的、完整的 Typescript 应用程序,而官方文档并不包含入门所需要的所有信息。

为了帮助大家全面地了解它,我们将演示如何使用 Vue CLI 构建一个新的 Vue + TypeScript 应用程序。

开始

用下面这句代码开始

vue create typescript-app

选择手动选择功能并进行配置,如下所示

项目搭建好之后,我们将项目跑起来

cd typescript-app
npm run serve

会自动打开 localhost:8080 网页(或者会在你启动项目后打印出这个链接),这代表我们项目启动成功

我们通过这个教程,我们会回顾以下功能,并展示如何使用 Typescript 去实现

1.基于类的组件 2.Data, props, computed 属性, methods, watchers, and emit 3.生命周期 4.Mixins 5.Vuex

components 目录中打开 HelloWorld.vue,你会看到如下结构

注意:对于每个实例,我将同时显示 TypeScriptJavascript 等价代码,这样您就可以轻松地比较两者。让我们开始吧

1.基于类的组件

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

Javascript 等价代码如下:

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

为了使用 Typescript,我们首先需要设置 <script>lang 属性为 ts

是一个第三方包,它使用官方的 vue-class 组件包,并在此基础上添加了更多装饰器。

vue-property-decorator 是一个第三方包,它使用了 Vue 类组件包,并在此基础上添加了更多的装饰器。指定了类名,会默认使用类名作为 name,当然我们也可以显式地使用 name 属性来命名组件。

@component({
  name: 'HelloWorld'
})

引入一个组件

在组件中注册其他组件的代码是在 @Component 装饰器中编写的,如下所示。

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

Javascript 等价代码如下:

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

2. Data, props, computed 属性, methods, watchers, and emit

使用 data

要使用 data 属性,我们可以简单地将它们声明为类变量。

@Component
export default class HelloWorld extends Vue {
  private msg: string = "welcome to my app"
  private list: Array<object> = [
    {
      name: 'Preetish',
      age: '26'
    },
    {
      name: 'John',
      age: '30'
    }
  ]
}

Javascript 等价代码如下:

export default {
  data() {
    return {
      msg: "welcome to my app",
      list: [
        {
          name: 'Preetish',
          age: '26'
        },
        {
          name: 'John',
          age: '30'
        }
      ]
    }
}

使用 props

我们可以使用 @Prop 装饰器在 Vue 组件中使用 props。在 Vue 中,我们可以给额外的配置给 props,比如 requireddefaulttype。首先我们可以像下面一样从 vue-property-decorator 引入 Prop 装饰器。我们还可以使用 readonly 去避免操作改变 props

import { Component, Prop, Vue } from 'vue-property-decorator'
@Component
export default class HelloWorld extends Vue {
  @Prop() readonly msg!: string
  @Prop({default: 'John doe'}) 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>

Javascript 等价代码如下:

  props: {
    msg,
    name: {
      default: 'John doe'
    },
    age: {
      required: true,
    },
    address: {
      type: String
    },
    job: {
      required: false,
      type: string,
      default: 'Developer'
    }
  }
}

Computed 属性

计算属性用于编写简单的模板逻辑,例如操作、添加或连接数据。在 TypeScript 中,一个普通的计算属性也以 get 关键字作为前缀。

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

Javascript 等价代码如下:

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

Typescript 中,我们可以写一些复杂的包括 gettersetter 的计算属性,如下所示

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]
  }
}

Javascript 等价代码如下:

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]
  }
}

Methods

与普通类方法一样,TypeScript 中的方法也有一个可选的访问修饰符。

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
  }
}

Javascript 等价代码如下:

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

Watchers

Watcher 的写法跟我们平时书写 JavaScript 的方式不同,我们最常用的 JavaScript 书写 watcher 的语法如下:

watch: {
  name: function(newval) {
    //do something
  }
}

我们不经常使用 handler 语法

watch: {
  name: {
    handler: 'nameChanged'
  }
}
methods: {
  nameChanged (newVal) {
    // do something
  }
}

但是,Typescript 跟第二种方式相似。在 TypeScript 中,我们使用 @Watch 装饰器并传递需要监视的变量的名称。

@Watch('name')
nameChanged(newVal: string) {
  this.name = newVal
}

我们也可以使用 immediatedeep

@Watch('project', { 
  immediate: true, deep: true 
})
projectChanged(newVal: Person, oldVal: Person) {
  // do something
}

Javascript 等价代码如下:

watch: {
  person: {
      handler: 'projectChanged',
      immediate: true,
      deep: true
    }
}
methods: {
  projectChanged(newVal, oldVal) {
    // do something
  }
}

Emit

要从一个子组件 emit 一个方法到父组件,在 Typescript 中,我们将使用 @Emit 装饰器。

@Emit()
addToCount(n: number) {
  this.count += n
}
@Emit('resetData')
resetCount() {
  this.count = 0
}

在第一个示例中,函数名 addToCount 被转换为短横线分隔 (kebab-case),这与 Vue emit 的工作方式非常类似。

在第二个示例中,我们传递方法的显式名称 resetData,并使用该名称。因为 addData 是驼峰式的,所以它再次被转换为短横线分隔 (kebab-case)。

<some-component add-to-count="someMethod" />
<some-component reset-data="someMethod" />
//Javascript Equivalent
 methods: {
    addToCount(n) {
      this.count += n
      this.$emit('add-to-count', n)
    },
    resetCount() {
      this.count = 0
      this.$emit('resetData')
    }
}

3.生命周期钩子

一个 Vue 组件有八个生命周期,包括 createdmounted 等等,每个钩子都使用相同的 Typescript 语法。这些被声明为普通类方法。因为生命周期钩子是自动调用的,所以它们既不接受参数也不返回任何数据。因此,我们不需要访问修饰符、输入参数或返回类型。

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

Javascript 等价代码如下:

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

4. Mixins

为了在 Typescript 中创建 mixins,我们必须首先创建 mixin 文件,其中包含我们与其他组件共享的数据。

创建一个名为 ProjectMixin.ts 的文件。在 mixin 目录中添加下面的 mixin,它共享 projName 和更新 projName 的方法。

import { Component, Vue } from 'vue-property-decorator'
@Component
class ProjectMixin extends Vue {
  public projName: string = 'My project'
  public setProjectName(newVal: string): void {
    this.projName = newVal
  }
}
export default ProjectMixin

Javascript 等价代码如下:

export default {
 data() {
   return {
     projName: 'My project'
   }
 },
 methods: {
   setProjectName(newVal) {
     this.projName = newVal
   }
 }
}

在我们的 Vue 组件中使用上面的 mixin,我们需要从 vue-property-decorator 引入 Mixins 和引入 mixin 文件,如下所示

//Projects.vue
<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 + ' ' + 'Preetish HS'
  }
}
</script>

Javascript 等价代码如下:

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

5. Vuex

Vuex 是大多数 Vue.js 应用程序中使用的官方状态管理库。将 store 划分为命名空间模块是一个很好的实践。我们将演示如何在 TypeScript 中编写它。

首先,我们需要安装两个流行的第三方库:

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

在 store文件夹中,让我们创建一个 module 文件夹来放置我们的命名空间存储模块。

创建一个名为 user 的文件。ts 拥有 user 的状态。

// store/modules/user.ts
import { VuexModule, Module, Mutation, Action } from 'vuex-module-decorators'
@Module({ namespaced: true, name: 'test' })
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

vuex-module-decorators 库为 Module, MutationAction 提供了装饰器。状态变量是直接声明的,就像类变量一样。这是一个简单的模块,它存储用户名,并通过一个 mutation 和一个 action 去更新用户名的操作。

我们不需要将 state 作为 MutationsActions 中的第一个参数,这个库已经考虑到这一点。它已经被注入到那些方法中。

Javascript 等价代码如下:

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

store 文件夹中,我们需要创建一个 index.ts 去初始化 Vuex 并注册此模块

import Vue from 'vue'
import Vuex from 'vuex'
import User from '@/store/modules/user'
Vue.use(Vuex)
const store = new Vuex.Store({
  modules: {
    User
  }
})
export default store

在组件中使用 Vuex

要使用 Vuex,我们可以利用一个名为 Vuex -class 的库。这个库提供装饰器来绑定 Vue 组件中的 State, Getter, MutationAction

因为我们使用的是带有命名空间的 Vuex 模块,所以我们首先从 Vuex 类导入命名空间,然后传递模块的名称来访问该模块。

<template>
  <div class="details">
    <div class="username">User: {{ nameUpperCase }}</div>
    <input :value="name" @keydown="updateName($event.target.value)" />
  </div>
</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>

Javascript 等价代码如下:

<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>

总结

现在,你已经掌握了在 TypeScript 中完全创建 Vue.js 应用程序所需的所有基本信息,可以使用一些官方和第三方库来充分利用类型化和自定义装饰器特性。Vue 3.0 将对 TypeScript 提供更好的支持,并且整个 Vue.js 代码都在 TypeScript 中重写,以提高可维护性。

一开始,使用 TypeScript 似乎有点让人不知所措,但是当你习惯了之后,你的代码中就会有更少的 bug,并且在相同代码基础上的其他开发人员之间的代码协作也会更顺畅。