这篇在介绍实例选项的ts写法时,会给出js的写法以作对比,同时也会给出选项的风格指南,保持良好的代码规范。

基本用法

vue结合typescript,有两种写法。

Vue.extend

  1. <script>
  2. import Vue from 'vue'
  3. const Component = Vue.extend({
  4. data: () => ({...}),
  5. methods: {...},
  6. })
  7. </script>

Vue.extend学习成本最低,现有写法的基础上,学习成本几乎为0。但是对ts的支持不佳。这就意味着会出现丢失代码提示、类型检查、编译报错等问题。

vue-class-component(推荐)

  1. <script lang="ts">
  2. // vue-property-decorator 库强依赖于 vue-class-component 库
  3. import { Component, Vue } from 'vue-property-decorator'
  4. // @Component 修饰符注明了此类为一个 Vue 组件
  5. @Component({
  6. // 所有的组件选项都可以放在这里,与原写法类似
  7. components: {...},
  8. })
  9. export default class ComponentName extends Vue {
  10. // 初始数据可以直接声明为实例的 property
  11. public message: string = 'Hello!'
  12. // 组件方法也可以直接声明为实例的方法
  13. public onClick (): void {
  14. window.alert(this.message)
  15. }
  16. // ...
  17. }
  18. </script>

Vue与typescript结合的第二种方法,推荐使用该写法。
基于 vue-property-decorator 和 vuex-class 提供的装饰器,可以很好的与typescript结合。
注: vue-property-decorator 强依赖于 vue-class-component

vue-class-component 写法区别与风格指南

下面介绍 .vue 文件中 vue-class-component 风格的ts写法与普通js写法的区别。同时介绍应该注意的事项和风格指南。

script标签

示例 - js:

  1. <script>
  2. </script>

示例 - ts:

  1. <script lang="ts">
  2. </script>

name 组件名

示例 - js:

  1. // js
  2. export default {
  3. name: "TodoList",
  4. }

示例 - ts:

  1. // ts
  2. import { Component, Vue } from "vue-property-decorator";
  3. @Component
  4. export default class TodoList extends Vue {}

风格指南:

  • name属性应该采用大驼峰 PascalCase 的形式命名。
  • ts写法在导出class时一定要带上命名,不要写匿名组件,在编写指令或者其他组件时,可能会遇到不可预计的影响

    components 引入组件

    示例 - js:

    ```javascript // js import HelloWorld from “@/components/HelloWorld.vue”;

export default { components: { HelloWorld } }

  1. <a name="jbrfo"></a>
  2. ### 示例 - ts:
  3. ```typescript
  4. // ts
  5. import { Component, Vue } from "vue-property-decorator";
  6. // 注意不能省略.vue的后缀。因为在shims-vue.d.ts中声明扩展的模块是 "*.vue"
  7. import HelloWorld from "@/components/HelloWorld.vue";
  8. @Component({
  9. components: {
  10. HelloWorld
  11. }
  12. })
  13. export default class App extends Vue {}

导入的组件放在 @Component 的装饰器中。

data 组件数据

示例 - js:

  1. // js
  2. export default {
  3. data: () => ({
  4. user: {
  5. name: 'Li Lei',
  6. age: 18,
  7. },
  8. loading: false,
  9. list: []
  10. })
  11. }

示例 - ts:

  1. // ts
  2. import { Component, Vue } from "vue-property-decorator";
  3. @Component
  4. export default class App extends Vue {
  5. public user = {
  6. name: 'Li Lei',
  7. age: 18,
  8. }
  9. public loading = false
  10. public list: unknown[] = []
  11. }

风格指南:

在ts的类中,不写public默认为public。建议还是都写上,便于一眼区分类别。至于public、private、readonly等在何时使用,在后面会讲到。

computed 计算属性

示例 - js:

  1. // js
  2. export default {
  3. data: () => ({
  4. num1: 1,
  5. num2: 1,
  6. }),
  7. computed: {
  8. // 普通计算属性
  9. totalCount() {
  10. return this.num1 + this.num2
  11. },
  12. // get、set计算属性
  13. numCalc: {
  14. get() {
  15. return this.num2 + 10
  16. },
  17. set(val) {
  18. this.num2 = val - 10
  19. },
  20. },
  21. },
  22. }

示例 - ts:

  1. // ts
  2. import { Component, Vue } from "vue-property-decorator";
  3. @Component
  4. export default class App extends Vue {
  5. public num1 = 1
  6. public num2 = 1
  7. // 普通计算属性
  8. private get totalCount() {
  9. return this.num1 + this.num2
  10. }
  11. // get、set版计算属性
  12. private get numCalc() {
  13. return this.num2 + 10
  14. }
  15. private set numCalc(val) {
  16. this.num2 = val - 10
  17. }
  18. }

风格指南:

计算属性主要是一个名词,因此不要用动词作为函数的命名。

推荐如:totalCount, fullName

methods 方法

示例 - js:

  1. // js
  2. export default {
  3. methods: {
  4. consoleNumber() {
  5. console.log(123)
  6. },
  7. },
  8. }

示例 - ts:

  1. // ts
  2. @Component
  3. export default class App extends Vue {
  4. public consoleNumber() {
  5. console.log(123)
  6. }
  7. }

filters 过滤器

示例 - js:

  1. // js
  2. export default {
  3. filters: {
  4. formatValue(val) {
  5. return val + 10
  6. },
  7. },
  8. }

示例 - ts:

  1. // ts
  2. @Component({
  3. filters: {
  4. formatValue(val) {
  5. return val + 10
  6. },
  7. },
  8. })
  9. export default class App extends Vue {}

风格指南:

filters的函数命名推荐为 format + 要过滤的val。

如: formatCreateTime 等。

watch 监听

示例 - js:

  1. export default {
  2. data: () => ({
  3. num: 10,
  4. user: {
  5. age: 10,
  6. },
  7. }),
  8. watch: {
  9. num(newValue, oldValue) {
  10. console.log('newValue', newValue)
  11. console.log('oldValue', oldValue)
  12. },
  13. 'user.age': function(newValue, oldValue) {
  14. console.log('newValue', newValue)
  15. console.log('oldValue', oldValue)
  16. },
  17. user: {
  18. immediate: true,
  19. deep: true,
  20. handler(newValue, oldValue) {
  21. console.log('newValue', newValue)
  22. console.log('oldValue', oldValue)
  23. },
  24. },
  25. },
  26. }

示例 - ts:

  1. // ts
  2. import { Component, Vue, Watch } from 'vue-property-decorator'
  3. @Component
  4. export default class App extends Vue {
  5. public num = 10
  6. public user = {
  7. age: 10,
  8. }
  9. @Watch('num')
  10. onNumChange(newVal: number, oldVal: number) {
  11. console.log('newVal', newVal)
  12. console.log('oldVal', oldVal)
  13. }
  14. @Watch('user.age')
  15. onUserAgeChange(newVal: number, oldVal: number) {
  16. console.log('newVal', newVal)
  17. console.log('oldVal', oldVal)
  18. }
  19. @Watch('user', { immediate: true, deep: true })
  20. onUserChange(newVal: { age: number }, oldVal: { age: number }) {
  21. console.log('oldVal', oldVal)
  22. console.log('newVal', newVal)
  23. }
  24. }

风格指南:

watch需要依赖于装饰器。@Watch()第一个参数为要监听的属性。第二个参数是一个object,用于设置deep等参数。

下面紧跟一个函数,函数的命名推荐使用小驼峰,以on开头,Change结尾:onXXXChange

Prop

示例 - js:

  1. // js
  2. export default {
  3. props: {
  4. propA: { type: Number, default: 1 },
  5. propB: { type: [String, Number], default: '' },
  6. propC: { type: [Object, Array], default: () => ({}) },
  7. },
  8. }

示例 - ts:

  1. // ts
  2. import { Component, Vue, Prop } from 'vue-property-decorator'
  3. @Component
  4. export default class App extends Vue {
  5. @Prop({ type: Number, default: 1 })
  6. readonly propA!: number
  7. @Prop({ type: [String, Number], default: '' })
  8. readonly propB!: string | number
  9. @Prop({ type: [Object, Array], default: () => ({}) })
  10. readonly propC!: object | unknown[]
  11. }

风格指南:

  • @Props装饰器里的参数与原Vue普通的写法一致
  • 声明prop字段前要加上readonly,以保持Vue中prop是单向数据流,不允许直接赋值与修改
  • prop装饰器内必须添加default默认值,声明时无需再赋值,后面要加上感叹号 ! 以告知ts编译器当前属性是非空的。

    Emit 自定义事件

    示例1 - js:

    1. export default {
    2. data() {
    3. return {
    4. count: 0,
    5. }
    6. },
    7. methods: {
    8. addToCount(n) {
    9. this.count += n
    10. // 将addToCount转成add-to-count
    11. this.$emit('add-to-count', n)
    12. },
    13. resetCount() {
    14. this.count = 0
    15. this.$emit('reset')
    16. },
    17. returnValue() {
    18. this.$emit('return-value', 10)
    19. },
    20. onInputChange(e) {
    21. this.$emit('on-input-change', e.target.value)
    22. },
    23. handlePromise() {
    24. const promise = new Promise((resolve) => {
    25. setTimeout(() => {
    26. resolve(20)
    27. }, 0)
    28. })
    29. promise.then((value) => {
    30. this.$emit('promise', value)
    31. })
    32. },
    33. },
    34. }

    示例1 - ts:

    ```typescript 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() handlePromise() { return new Promise((resolve) => { setTimeout(() => { resolve(20) }, 0) }) } }

  1. - @Emit装饰器接收一个可选参数,作为事件名;如果没有提供这个参数,$emit会将回调函数的camelCase(驼峰式)转为kebab-case(短横线命名),并将其作为事件名
  2. - @Emit会将回调函数的返回值作为第二个参数,如果返回值是一个Promise对象,$emit会将Promise对象状态为resolved之后触发
  3. - @Emit的回调函数的参数,会放在其返回值之后,一起被$emit当作参数使用
  4. - @Emit装饰器可以选择不传事件名,不返回处理的值,在里面添加业务处理。
  5. 但是为了能够让emit事件更加的清晰,以及与组件内业务代码的解耦,推荐如下写法:
  6. <a name="sDv3c"></a>
  7. ### 更好地写法:
  8. ```typescript
  9. import { Component, Vue, Emit } from 'vue-property-decorator'
  10. @Component
  11. export default class App extends Vue {
  12. public count = 1
  13. @Emit('add-to-count')
  14. public emitAddToCount(val: number) {
  15. return val
  16. }
  17. @Emit('reset')
  18. public emitReset() {
  19. return
  20. }
  21. @Emit('return-value')
  22. public emitReturnValue(val: number) {
  23. return val
  24. }
  25. @Emit('on-input-change')
  26. public emitOnInputChange(e: Event) {
  27. return (e.target as HTMLInputElement).value
  28. }
  29. @Emit('promise')
  30. public emitPromise(val: unknown) {
  31. return val
  32. }
  33. public addToCount(n: number) {
  34. this.count += n
  35. this.emitAddToCount(n)
  36. }
  37. public resetCount() {
  38. this.count = 0
  39. this.emitReset()
  40. }
  41. public handlePromise() {
  42. const promise = new Promise(resolve => {
  43. setTimeout(() => {
  44. resolve(20)
  45. }, 0)
  46. })
  47. promise.then(val => {
  48. this.emitPromise(val)
  49. })
  50. }
  51. }

风格指南:

  • 明确每个emit接收的事件名
  • emit处理函数都以 emit 开头
  • emit处理函数声明仅接收参数和直接返回参数,避免与业务逻辑耦合
  • 若想传递多个参数,还是建议编写业务代码时,将多个参数作为一整个对象传递

    生命周期 Hook

    示例 - ts:

    ```typescript import { Component, Vue } from ‘vue-property-decorator’

@Component export default class App extends Vue { private beforeCreated() { console.log(‘beforeCreated’) } private created() { console.log(‘created’) } private beforeMount() { console.log(‘beforeMount’) } private mounted() { console.log(‘mounted’) } private beforeUpdate() { console.log(‘beforeUpdate’) } private updated() { console.log(‘updated’) } private beforeDestroy() { console.log(‘beforeDestroy’) } private destroyed() { console.log(‘destroyed’) } }

  1. <a name="vaDfU"></a>
  2. ## mixins 混入
  3. <a name="L5EBD"></a>
  4. ### 示例 - ts:
  5. <a name="XQXP9"></a>
  6. #### 创建 - FormMixins.ts
  7. ```typescript
  8. // mixins/FormMixins.ts
  9. import { Component, Mixins } from 'vue-property-decorator'
  10. @Component
  11. export default class FormMixins extends Mixins() {
  12. public form = {
  13. type: 1,
  14. search: '搜索信息',
  15. }
  16. public logFormSearch() {
  17. console.log(this.form.search)
  18. }
  19. }
  20. // mixins/UserMixins.ts
  21. import { Component, Mixins } from 'vue-property-decorator'
  22. @Component
  23. export default class UserMixins extends Mixins() {
  24. public user = {
  25. name: '韩梅梅',
  26. age: 18,
  27. }
  28. public logUserName() {
  29. console.log(this.user.name)
  30. }
  31. }

使用 - .vue

  1. // App.vue
  2. import { Component, Mixins } from 'vue-property-decorator'
  3. import FormMixins from '@/mixins/FormMixins'
  4. import UserMixins from '@/mixins/UserMixins'
  5. @Component
  6. export default class App extends Mixins(FormMixins, UserMixins) {
  7. private mounted() {
  8. this.logFormSearch() // 搜索信息
  9. this.logUserName() // 韩梅梅
  10. }
  11. }

风格指南:

  • mixins命名时要使用大驼峰命名
  • mixins命名以 Mixins 结尾

    directive 自定义指令

    示例 - ts:

    创建 - colorDirective.ts

    ```typescript // colorDirective.ts // 获取类型提示 import { DirectiveOptions } from ‘vue’

const directive: DirectiveOptions = { inserted(el, node) { /**

  1. * Using value:
  2. * v-color={color: 'red', backgroundColor: 'blue'}
  3. */
  4. if (node.value) {
  5. el.style.backgroundColor = node.value.backgroundColor
  6. el.style.color = node.value.color
  7. }
  8. /**Using modifiers:
  9. * v-color.color
  10. * v-color.backgroundColor
  11. */
  12. if (node.modifiers.color) {
  13. el.style.color = node.value
  14. }
  15. if (node.modifiers.backgroundColor) {
  16. el.style.backgroundColor = node.value
  17. }

}, }

export default directive

  1. <a name="VgTON"></a>
  2. #### 使用 - .vue
  3. ```vue
  4. // App.vue
  5. <template>
  6. <div id="app">
  7. <h1 v-color="{color: 'red', backgroundColor: 'blue'}">
  8. about
  9. </h1>
  10. </div>
  11. </template>
  12. <script lang="ts">
  13. import { Component, Vue } from 'vue-property-decorator'
  14. import colorDirective from '@/directive/colorDirective'
  15. @Component({
  16. directives: {
  17. color: colorDirective,
  18. },
  19. })
  20. export default class App extends Vue {}
  21. </script>

风格指南:

自定义指令用法与普通Vue的指令用法相同,写在@Component装饰器中。
类型提示仅在封装directives时。

provide 和 inject

示例 - js:

  1. // js
  2. // 祖先组件 - Todo.vue
  3. import TodoListItem from '@/components/TodoListItem.vue'
  4. export default {
  5. components: { TodoListItem },
  6. provide: () => ({
  7. userId: 1,
  8. userName: '韩梅梅',
  9. }),
  10. }
  11. // 孙组件 - TodoListItem.vue
  12. export default {
  13. name: 'TodoListItem',
  14. inject: {
  15. todoUserName: {
  16. from: 'userName',
  17. default: '韩梅梅',
  18. },
  19. todoUserId: {
  20. from: 'userId',
  21. default: 1
  22. },
  23. },
  24. mounted() {
  25. console.log(this.todoUserName) // 韩梅梅
  26. console.log(this.todoUserId) // 1
  27. },
  28. }

示例 - ts:

  1. // ts
  2. // 祖先组件 - Todo.vue
  3. import { Component, Vue, Provide } from 'vue-property-decorator'
  4. import TodoListItem from '@/components/TodoListItem.vue'
  5. @Component({
  6. components: { TodoListItem },
  7. })
  8. export default class Todo extends Vue {
  9. @Provide('userName') public userName = '韩梅梅'
  10. @Provide('userId') public userId = 1
  11. }
  12. // 孙组件 - TodoListItem.vue
  13. import { Component, Vue, Inject } from 'vue-property-decorator'
  14. @Component({})
  15. export default class TodoListItem extends Vue {
  16. @Inject('userId') public todoUserId = 1
  17. @Inject('userName') public todoUserName = '韩梅梅'
  18. private mounted() {
  19. console.log(this.todoUserName) // 韩梅梅
  20. console.log(this.todoUserId) // 1
  21. }
  22. }

Ref

示例 - ts:

在Vue中,ref可以绑定DOM元素,也可以直接绑定Vue组件。

  1. <template>
  2. <div class="todo">
  3. <div ref="refTitle">
  4. this is title
  5. </div>
  6. <TodoItem ref="refTodoItem" />
  7. </div>
  8. </template>
  9. <script lang="ts">
  10. import { Component, Vue, Ref } from 'vue-property-decorator'
  11. import TodoItem from '@/components/TodoItem.vue'
  12. @Component({
  13. components: { TodoItem },
  14. })
  15. export default class Todo extends Vue {
  16. @Ref('refTitle') readonly refTitle!: HTMLDivElement // DOM
  17. @Ref('refTodoItem') readonly refTodoItem!: TodoItem // Vue Component
  18. private mounted() {
  19. console.log(this.refTitle) // 等同于 this.$refs.refTitle
  20. console.log(this.refTodoItem) // 等同于 this.$refs.refTodoItem
  21. }
  22. }
  23. </script>

风格指南:

@Ref 装饰器函数第一个参数是模板中ref的值,代表你要绑定的是哪个ref。后面的变量代表可以用this.xxx访问到ref的变量名。
**

  • ref的命名,推荐以ref开头。DOM的ref进行语义化的命名,如 refTitle 。Vue组件的则以ref开头,后面跟上驼峰式的组件名,如 refTodoItem
  • ref的命名和要在class内通过this访问的变量名,命名保持一致。
  • 变量前加上 readonly 关键字,以保持ref对象不能随意赋值修改的属性。
  • 变量后无需赋值,同时加上非空断言。即 refTitle! 。来告诉ts编译器这个ref的类型就是冒号后面声明的类型,且无需手动赋值。
  • DOM类型的ref。官方已经定义好了几乎所有DOM类型的声明(https://developer.mozilla.org/zh-CN/docs/Web/API#%E6%8E%A5%E5%8F%A3)。实在无法确定的,可以先使用 HTMLElement 类型。
  • 组件类型的ref。import 获取组件后,直接将值作为ref的类型。(ts的import一般都会将值和类型同时引入)

    Model

    Vue官方文档: 一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的。model 选项可以用来避免这样的冲突:

示例 - js:

我们先来回顾一下vue js下model的用法:

父组件 Parent.vue

  1. <template>
  2. <div class="parent">
  3. <h1>parent</h1>
  4. <ChildItem v-model="parentAge" />
  5. </div>
  6. </template>
  7. <script>
  8. import ChildItem from '@/components/ChildItem.vue'
  9. export default {
  10. components: {
  11. ChildItem,
  12. },
  13. data: () => ({
  14. parentAge: 18,
  15. }),
  16. }
  17. </script>

子组件 ChildItem.vue

  1. <template>
  2. <div class="child">
  3. <button @click="getRandomName()">
  4. 获取随机年龄 - ({{ userAge }})
  5. </button>
  6. </div>
  7. </template>
  8. <script>
  9. export default {
  10. name: 'ChildItem',
  11. model: {
  12. prop: 'user-age',
  13. event: 'age-change',
  14. },
  15. props: {
  16. userAge: {
  17. type: Number,
  18. default: 0,
  19. },
  20. },
  21. methods: {
  22. getRandomAge() {
  23. // 这里触发model绑定的事件名
  24. this.$emit('age-change', parseInt(Math.random() * 15, 10))
  25. },
  26. },
  27. }
  28. </script>

示例 - ts:

父组件 TodoApp.vue

  1. <template>
  2. <div class="todo-app">
  3. <TodoItem v-model="parentAge" />
  4. </div>
  5. </template>
  6. <script lang="ts">
  7. import { Component, Vue } from 'vue-property-decorator'
  8. import TodoItem from '@/components/TodoItem.vue'
  9. @Component({
  10. components: { TodoItem },
  11. })
  12. export default class TodoApp extends Vue {
  13. public parentAge = 18
  14. }
  15. </script>

子组件 TodoAppItem.vue

  1. <template>
  2. <div class="todo">
  3. <button @click="getRandomAge()">
  4. 获取随机年龄 - ({{ userAge }})
  5. </button>
  6. </div>
  7. </template>
  8. <script lang="ts">
  9. import { Component, Vue, Model, Emit } from 'vue-property-decorator'
  10. @Component
  11. export default class TodoAppItem extends Vue {
  12. @Model('user-age')
  13. readonly userAge!: number
  14. @Emit('user-age')
  15. emitAgeChange(age: number) {
  16. return age
  17. }
  18. public getRandomAge() {
  19. this.emitAgeChange(parseInt(`${Math.random() * 15}`, 10))
  20. }
  21. }
  22. </script>

风格指南:

父组件通过 v-model 传值,子组件不再需要 prop 。可以直接通过 @Model 装饰器同时配置 v-model 以及获取 prop 的属性。@Emit 装饰器中传入跟 @Model 中一样的值即可。

  • 通过 @Model 装饰器获取的属性也可以直接通过this访问,但是不能直接修改,因此加上 readonly 修饰符
  • @Emit 装饰器的参数最好使用横线连接kabeb-case格式,由于要跟 @Model 参数保持一致,因此model的参数最好也是用kabeb-case格式命名