使用装饰器和class关键字编写Vue2.x组件

您好,我是沧沧凉凉,是一名前端开发者,目前在掘金知乎以及个人博客上同步发表一些学习前端时遇到的趣事和知识,欢迎关注。


相信目前在前端行业中,大部分项目依然是使用的Vue2.x来进行编写,除了很多人不愿意跳出自己的舒适圈去学习新的东西之外,还有一个原因就是Vue2.x比Vue3.x的第三方库丰富太多,因为Vue3 Composition API的关系,导致很多支持Vue2.x的第三方库与Vue3.x都不兼容。

当然这并不是本篇文章要讲的重点,本篇文章的重点其实是vue-property-decorator这个装饰器库提供的装饰器。

之所以要使用装饰器的原因是因为装饰器可以极大程度简化Vue组件中各种状态的声明,并且Mixins的引用会变得更加明确(但是通常不推荐在一个项目中大量使用Mixins,因为会大大降低代码的可读性。)


vue-property-decorator一共有提供以下几种装饰器:

  1. @Prop
  2. @PropSync
  3. @Model
  4. @ModelSync
  5. @Watch
  6. @Provide
  7. @Inject
  8. @ProvideReactive
  9. @InjectReactive
  10. @Emit
  11. @Ref
  12. @VModel
  13. @Component

需要值得注意的是,装饰器并不仅仅可以用在ts上面,在js上面一样可以使用!下面就来看一下这些装饰器的魅力吧:

1. @Component/Mixins

使用class关键字来创建组件的基本方法:

import { Component, Vue, VModel, Mixins } from "vue-property-decorator";

@Component
export default class C extends Vue {}

// Mixins 括号中引入混入的文件
@Component
export default class C extends Mixins() {}

这样就可以创建一个Vue组件,同时如果你使用了TypeScript,那么你会得到更好的类型推断。

2. @Prop/@Emit

在本库中最常用的两个装饰器,分别对应Propthis.$emit

2.1 @Prop

用法:

@Component
export default class YourComponent extends Vue {
  // 下面3个是官方给的用法
  @Prop(Number) readonly propA: number | undefined
  @Prop({ default: 'default value' }) readonly propB!: string
  @Prop([String, Boolean]) readonly propC: string | boolean | undefined
}

相当于:

export default {
  props: {
    propA: {
      type: Number,
    },
    propB: {
      default: 'default value',
    },
    propC: {
      type: [String, Boolean],
    },
  },
}

我个人比较喜欢使用下面这种写法:

@Component
export default class YourComponent extends Vue {
  @Prop({ type: Number, required: true, default: 0 }) readonly propA:
    | number
    | undefined;
}

@Prop()括号里面接各种参数。

2.2 @Emit

一般来讲,要更改Prop的值则需要通过this.$emit("xxx" , value)这种写法,例如:

change(): void {
  this.$emit("change", 5);
}

而使用了装饰器后可以这样写:

@Emit()
change(): number {
  return 5;
}

其中return的值就是$emit所要传递的值,如果@Emit()不带参数,则默认将函数名作为$emit所要触发的名称,如果带了参数:例如@Emit("a"),则相当于this.$emit("a", 5);

同时@Emit装饰器会自动将camelCase(驼峰命名)命名转换为kebab-case。

3. @PropSync

因为在Vue2.x里只能拥有一个v-model,所以可以使用.sync关键字来创建多个类似于v-model的双向绑定。

// 父组件通过:name.sync关键字
<B :name.sync="name" />

// 子组件
@PropSync("name") syncName!: string;

@Emit("update:name")
input(): string {
  return "张三";
}

对于.sync关键字,可以看官方的文章

4. @VModel

快速创建一个v-model

import { Component, Vue, VModel } from "vue-property-decorator";

@Component
export default class C extends Vue {
  @VModel({ type: String }) name!: string;
}

等同于:

export default {
  props: {
    value: {
      type: String,
    },
  },
  computed: {
    name: {
      get() {
        return this.value
      },
      set(value) {
        this.$emit('input', value)
      },
    },
  },
}

就是实现一个v-model双向绑定,只要在子组件中调用this.$emit('input', value)就可以修改传入的值,也可以配合@Emit装饰器使用:

@Component({
  components: { C },
})
export default class B extends Vue {
  @VModel({ type: String }) name!: string;

  @Emit()
  input(): string {
    return "张三";
  }
}

5. @Model/@ModelSync

5.1 @Model

从上面的v-model双向绑定我们可以得知,v-model实际上是绑定了一个Prop value属性,和$emit的input属性,如果想将v-model所绑定的这两个属性更改一下名字时,我们就可以使用@Model装饰器:

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

@Component
export default class YourComponent extends Vue {
  @Model('change', { type: Boolean }) readonly checked!: boolean
}

上面的代码等同于:

export default {
  model: {
    prop: 'checked',
    event: 'change',
  },
  props: {
    checked: {
      type: Boolean,
    },
  },
}

至于model属性,可以参考官方文档

5.2 @ModelSync

该装饰器是结合了@Model@VModel两个装饰器,将两个装饰器的功能合二为一。

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

@Component
export default class YourComponent extends Vue {
  @ModelSync('checked', 'change', { type: Boolean })
  readonly checkedValue!: boolean
}

上面的代码等同于:

export default {
  model: {
    prop: 'checked',
    event: 'change',
  },
  props: {
    checked: {
      type: Boolean,
    },
  },
  computed: {
    checkedValue: {
      get() {
        return this.checked
      },
      set(value) {
        this.$emit('change', value)
      },
    },
  },
}

6. @Provide/@Inject

其实我之前是不知道有这两个装饰器的,直到我最近写Vue组件中的值传递比较费劲,突然想起Vue中有Provide/Inject,所以去翻阅了一下Vue中的几种值传递方式,然后又想到我目前正在写的项目虽然是使用js构建,但是使用了vue-property-decorator装饰器库,就去翻阅一下官方文档看是否有这两个装饰器,一翻还真有。

这里说一下vuex存在的副作用,以及我为啥想要使用Provide/Inject,其实我之前一直是一个vuex党,对于一个复杂状态的传递我搞不清楚就统统使用vuex,但是有时候vuex会将一些简单的逻辑复杂化,因为vuexstate状态的修改理论上都是需要通过Mutation(虽然可以使用对象的方式绕过Mutation直接修改state的值,就跟Prop传递到子组件的对象,子组件也可以修改该对象下的属性一样)。

同时vuex还带来一个问题,因为vuex中存储的状态是不会因为组件的销毁而自动销毁的,除非刷新浏览器,所以有时候会因为留下了很多状态而引起一些BUG,如果要手动清除这些状态势必又会带来额外的工作量,所以为了解决子孙之间的值传递,我就想到了Provide/Inject

但是使用Provide/Inject又会引发另一个问题,就是Provide传递的值因为可以在任何子组件甚至孙组件中通过Inject来进行调用,所以该值的源头找起来就会比较麻烦,所以一定要做好注释!不然过段时间再看代码你根本找不到该值从哪个父层级中传递而来。

我曾经接手过一个项目,它是使用$refs的方式进行传值,又没有注释,半找半猜终于找到了该值的源头,过程十分艰辛。

6.1 @Provide

该装饰器可以直接声明在类中的变量上,将该属性通过provide传递给子组件。

例子:

@Component({
  components: { B },
})
export default class A extends Vue {
  @Provide()
  message = {
    content: "",
  };
}

相当于:

export default {
  components: { B },
  data() {
    return {
      message: { content: "" },
    };
  },
  provide() {
    return {
      message: this.message,
    };
  },
};

可以看到简化了非常多的代码,非常方便,同时@Provide()中还可以跟一个String类型的参数,该参数表示了provide()所要传递出去的名字,一旦设置在后面的Inject中就需要使用设置的别名来调用:

// 父组件
@Component({
  components: { B },
})
export default class A extends Vue {
  // Provide a
  @Provide("a")
  message = {
    content: "",
  };
}

// 子组件
export default {
  name: "B",
  components: { C },
  inject: {
    // 这里的值一定要与父组件@Provide()括号中的值对应
    message: "a",
  },
};

如果不设置别名,则会默认将所装饰的变量名当做Provide的名称。

6.2 @Inject

跟上面一样,使用起来非常方便:

@Component
export default class C extends Vue {
  @Inject() readonly message!: { content: string };
}

如果@Inject()括号中不带参数,则默认将后面的变量名作为注入名称。

也可以像@Inject("a")这样指定对应的名称,同时后面还可以跟一个对象设置其默认值:@Inject({ from: 'optional', default: 默认值 })

7. @ProvideReactive/@InjectReactive

可以看到上面演示@Provide/@Inject我是使用了一个message对象,然后修改它的content属性,这是因为Provide/Inject传递的值不是响应式,如果你将message对象换成一个字符串,如下所示:

export default class A extends Vue {
  @Provide()
  message = "";
}

则当message被改变时,子组件中调用该数据的界面显示不会有任何变化,换句话说就是子组件其界面不会刷新,那么这个时候@ProvideReactive/@InjectReactive就可以解决这个问题。

// 父组件
export default class A extends Vue {
  @ProvideReactive()
  message = "";
}

// 子组件
export default class B extends Vue {
  @InjectReactive() message!: string;
}

你再去页面中尝试,你会发现即使更改了message的值,在子组件调用到的界面也会进行刷新。

8. @Watch

这个很简单,就不过多讲解了,一看就会:

// 需要监听的变量名
@Watch("data")
change(): void {
  // 所要执行的语句
}

同时变量名后还可以接选项:@Watch('person', { immediate: true, deep: true })

9. @Ref

跟上面一样,不带参数默认将变量名作为名称,带了参数后则将参数作为名称,变量名作为别名:

@Ref() aaa!: HTMLElement;

mounted(): void {
  console.log(this.aaa);
}

10. 最后

通过对装饰器的学习,对于一些我平时没有了解过或者平时没有注意过的属性有了一定的认知,比如.sync关键字,model属性等。

使用装饰器会大大降低我们平时写代码时声明Vue组件状态的繁琐性,其实vue-property-decorator装饰器库的使用并不复杂,官方文档也写的比较清楚。但是鉴于很多人不喜欢看官方文档(因为是英文的)而我之前找了几篇好像又没有列出所有的装饰器,所以我才写下了这篇文章,希望大家对于Vue中的装饰器有更进一步的了解。

参考链接:vue-property-decorator 文档


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!