Skip to content

前言

在vue中使用TypeScript时的一个好用的装饰器。

安装

bash
npm i -S vue-property-decorator

使用

@Component 创建组件

由vue-class-component提供的装饰器组件,一般写法如下

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

@Component
export default class ComponentName extends Vue {

}

可以接收一个对象作为参数,可以在对象中声明components,filters,directives等未提供装饰器的选项,也可以声明computed,watch等。

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

@Component({
  filters: {
    toFixed: (num: number, fix: number = 2) => {
      return num.toFixed(fix)
    }
  }
})

@Prop 接收参数

在vue里面,prop是不能赋初始值的。这个规则和typescript会发生矛盾,因此定义类型需要加undefined,避免typescript转义告警

js
// 父组件:
<template>
  <div class="Props">
    <Child :name="name" :age="age" :sex="sex"></Child>
  </div>
</template>

<script lang="ts">
import {Component, Vue,} from 'vue-property-decorator';
import Child from './Child.vue';

@Component({
  components: {Child}, // 上边有说 @Component 可接受的参数
})
export default class PropsPage extends Vue {
  private name = 'Hs';
  private age = 18;
  private sex = 1;
}
</script>

// 子组件:
<template>
  <div>
    name: {{name}} | age: {{age}} | sex: {{sex}}
  </div>
</template>

<script lang="ts">
import {Component, Vue, Prop} from 'vue-property-decorator';

@Component
export default class Child extends Vue {
   @Prop(String) readonly name!: string | undefined;
   @Prop({ default: 20, type: Number }) private age!: number;
   @Prop([String, Number]) private sex!: string | number;
}
</script>

@PropSync 不一样的@Prop

场景:巧用PropSync封装弹窗

区别:父组件传参的时候需要配合.sync, PropSync会生成新的计算属性,可逆向修改父组件传递过来的属性,父组件会同步修改

js
// 父组件:
<template>
  <div class="Props">
    <Child :name.sync="name"></Child>
  </div>
</template>

<script lang="ts">
import {Component, Vue,} from 'vue-property-decorator';
import Child from './Child.vue';

@Component({
  components: { Child }, // 上边有说 @Component 可接受的参数
})
export default class PropsPage extends Vue {
  private name = 'Hs';
}
</script>

// 子组件:
<template>
  <div>
    name: {{name_copy}}
    <button @click="setProp">修改prop</button>
  </div>
</template>

<script lang="ts">
import {Component, Vue, PropSync} from 'vue-property-decorator';

@Component
export default class Child extends Vue {
   @PropSync("name",String) name_copy!: string | undefined;
   setProp(){
       this.name_copy = "abcd" // 父组件会同步修改
   }
}
</script>

@Model

  • @Model(event?: string, options: (PropOptions | Constructor[] | Constructor) = {})

  • @Model装饰器允许我们在一个组件上自定义v-model,接受两个参数:

  • event: string类型,表示事件名;

  • options: PropOptions | Constructor[] | Constructor与@Prop的第一个参数一致;

js
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,
    },
  },
}
js
// 父组件
<template>
  <div class="Model">
    <ModelComponent v-model="val" value="some value"></ModelComponent>
    <div>父组件 value: {{val}}</div>
  </div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import ModelComponent from './ModelComponent.vue';

@Component({ components: {ModelComponent} })
export default class ModelPage extends Vue {
  private val = 'hello';
}
</script>

// 子组件
<template>
  <div class="child">
    子组件:<input type="text" :value="inputValue" @input="inputHandle($event)"/>
  </div>
</template>

<script lang="ts">
import {Component, Vue, Model} from 'vue-property-decorator';

@Component
export default class ModelComponent extends Vue {
   @Model('change', { type: String }) readonly inputValue!: string

   public inputHandle(event): void {
     this.$emit('change', event.target.value);
   }
}

@ModelSync

-   @ModelSync(propName: string, event?: string, options: (PropOptions | Constructor[] | Constructor) = {})
-   @ModelSync装饰器可接受三个参数:

-   propName: string类型,表示类型名称;
-   event: string类型,表示事件名;
-   options: PropOptions | Constructor[] | Constructor与@Prop的第一个参数一致;
js
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)
      },
    },
  },
}

@Vmodel

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

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

is equivalent to

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

@Watch 监听

@Watch接收两个参数:

  • 被监听的属性名
  • 可选属性: immediate: true, deep: true
js
<template>
  <div class="about">
    <h3> {{age}}</h3>
  </div>
</template>

<script lang="ts">
import { Component, Vue, Watch } from "vue-property-decorator";

@Component
export default class About extends Vue {
  private age = 18;

  @Watch("age")
  // 可选参数 @Watch('age', {immediate: true, deep: true})
  onChangeAge(v: number, o: number): void {}

}
</script>

@Emit 广播事件

@Emit 装饰器接收一个可选参数,广播事件名,如果没有定义这个参数,则是以回调方法名为广播事件名. 回调函数的返回值默认为第二个参数,如果返回是 promise ,则会默认为 resolve 之后触发回调

js
// 父组件
<template>
  <div class="about">
    <ChildComp @childEmit="chileEmit" />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import ChildComp from "./Child.vue";

@Component
export default class About extends Vue {
  chileEmit(n: string): void {
    console.log("子组件 emit 触发,参数:", n);
  }
}
</script>

//子组件
<template>
  <div>
    <h3>我是子组件</h3>
    <button @click="customClickName"> @emit age+1 </button>
  </div>
</template>

<script lang="ts">
import { Component, Emit, Vue } from "vue-property-decorator";

@Component
export default class Child extends Vue {
  @Emit("childEmit")
  customClickName(): string {
    return "hs"
  }
}
</script>

@Ref 句柄

@Ref 装饰器接收一个可选参数,用来指向元素或子组件的引用信息,即 ref="这个值"

js
<template>
  <div class="about">
    <button @click="getRef"
            ref="child_btn">age++</button>
    <hr>
    <ChildComp :name="name"
               :age="age"
               ref="child_c"
               @childEmit="chileEmit" />
  </div>
</template>

<script lang="ts">
import { Component, Provide, Ref, Vue, Watch } from "vue-property-decorator";
import ChildComp from "./Child.vue";

@Component({
  components: { ChildComp }
})
export default class About extends Vue {
  @Ref("child_c") readonly child_comp!: ChildComp;
  @Ref("child_btn") readonly child_btn_dom!: HTMLButtonElement;
  getRef() {
    console.log(this.child_comp, this.child_btn_dom);
  }
}
</script>

@Provide/@Inject 和@ProvideReactive和@InhectReactive

提供/注入装饰器,key可以为string或者symbol类型,使用方式都一样

  • 相同: Provide/ProvideReactive提供的数据,在子组件内部使用Inject/InjectReactive都可取到
  • 不同: ProvideReactive 的值被父组件修改,子组件可以使用 InjectReactive 捕获
js
// 顶层组件
<template>
  <div class="about">
    <ChildComp />
  </div>
</template>

<script lang="ts">
import { Component, Provide,Vue } from "vue-property-decorator";
import ChildComp from "./Child.vue";

@Component({
  components: { ChildComp },
})
export default class About extends Vue {
  @Provide("provide_value") private p = "from provide";
}
</script>

// 子组件
<template>
  <div>
    <h3>我是子组件</h3>
    <h3>provide/inject 跨级传参 {{provide_value}}</h3>

  </div>
</template>

<script lang="ts">
import { Component, Inject, Vue } from "vue-property-decorator";

@Component
export default class Child extends Vue {
  @Inject() readonly provide_value!: string;
}
</script>

tsx

其实 tsx 用起来会让我们有 react 的感觉,写过 react 的都知道是使用 jsx javascript + xml, 那么 tsx 基本上跟 jsx 差不多,等同于 typescript + xml,用一个实例来体现一下 现有的工程不变,我们搭建环境的时候已经支持了 tsx

创建一个 demo.tsx ,键入如下简单内容,大致看下来,基本上跟我们上边分享的类组件一样,唯一有一点不一样的就是模板变成 render 函数,这样可以让我们更加灵活。

js
import { Component, Emit, Prop, PropSync, Vue, Watch } from "vue-property-decorator";

@Component
export default class Demo extends Vue {

  public name = "Hs"
  public str = "hello tsx"
  public data = [1, 2, 3, 4]

  // Prop
  @Prop() demo_name!: string
  @Prop(Number) demo_age!: number

  // Propsync
  @PropSync("propsync", Number) propsync_copy!: number | undefined

  // Computed
  get _age(): number {
    this.str = this.str + "-x"
    return this.demo_age * 10
  }

  //watch
  @Watch("str")
  onhangeStr(v: string, o: string) {
    console.log(v, o)
  }

  //emit
  @Emit("tsx_emit")
  clickEvent() { return "params 123" }

  // 渲染函数
  render() {
    return (
      <div>
        <h2>data属性: {this.name}-{this.str}</h2>
        <h2>prop: {this.demo_name}</h2>
        <h2>计算属性: {this._age}</h2>
        <h2>prop-sync: {this.propsync_copy}</h2>
        <h2>遍历</h2>
        {
          this.data.map(c => {
            return <span>{c} - </span>
          })
        }
        <button onClick={() => this.clickEvent()}>emit</button>
      </div>
    )
  }
}

完整示例

ts
<template>
  <div class="test-container">
    {{message}}
    <input type="button" value="点击触发父级方法" @click="bindSend"/>
    <input type="button" value="点击触发父级方法" @click="handleSend"/>
    <input type="button" value="点击触发父级方法" @click="bindSend2"/>
    <!-- <Hello></Hello> -->
  </div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch, Emit } from "vue-property-decorator";
import Hello from "./HelloWorld.vue";
// 注明此类为一个vue组件
@Component({
  components: {
    Hello
  }
})
export default class Test extends Vue {
  // 原有data中的数据在这里展开编写
 public message: string = "asd";
  //原有props中的数据展开编写
  @Prop({
    type: Number,
    default: 1,
    required: false
  })
  propA?: number
  @Prop()
  propB:string
  //原有computed
  public get computedMsg(){
      return '这里是计算属性' + this.message;
  }
  public set computedMsg(message:string){
  }
  //原有的watch属性
  @Watch('propA',{
      deep:true
  })
  public test(newValue:string,oldValue:string){
      console.log('propA值改变了' + newValue);
  }
  // 以前需要给父级传值的时候直接方法中使用emit就行了,当前需要通过emit来处理
  @Emit()
  private bindSend():string{
      return this.message
  }
  @Emit()
  private bindSend1(msg:string,love:string){
      // 如果不处理可以不写下面的,会自动将参数回传
    //   msg += 'love';
    //   return msg;
  }
  //原有放在methods中的方法平铺出来
  public handleSend():void {
      this.bindSend1(this.message,'love');
  }
  // 这里的emit中的参数是表明父级通过什么接受,类似以前的$emit('父级定义的方法')
  @Emit('test')
  private bindSend2(){
      return '这个可以用test接受';
  }
}
</script>

参考

https://github.com/kaorun343/vue-property-decorator

https://juejin.cn/post/6844903741456384014

https://segmentfault.com/a/1190000019906321