typescript|【Vue】vue2.6使用TS之vue-class-component与vue-property-decorator使用详解

vue2.6中对typescript的支持
Vue CLI 3 可以使用 TypeScript 生成新工程。

# 1. 如果没有安装 Vue CLI 就先安装 cnpm install --global @vue/cli# 2. 创建一个新工程,并选择 "Manually select features (手动选择特性)" 选项 vue create my-project-name

1. vue-class-component vue-class-component 是 vue 的官方库,作用是用类的方式编写组件。这种编写方式可以让 .vue 文件的 js 域结构更扁平,并使 vue 组件可以使用继承、混入等高级特性。
vue2.x 对 TS 的支持并不友好,所以 vue2.x 跟 TS 的整合,通常需要基于vue-class-component(官方推荐的)来用基于 class(类) 的组件书写方式。
vue-class-component(以下简称Component)带来了很多便利:
  • methods 钩子都可以直接写作 class 的方法
  • computed 属性可以直接通过 get 来获得
  • 初始化 data 可以声明为 class 的属性
  • 其他的都可以放到 Component 装饰器里
官方文档:https://class-component.vuejs.org/
基本示例如下:
import Vue from 'vue' import Component from 'vue-class-component'// @Component 修饰符注明了此类为一个 Vue 组件 @Component({ // 所有的组件选项都可以放在这里 template: '' }) export default class MyComponent extends Vue { // 初始数据可以直接声明为实例的 property message: string = 'Hello!'// 组件方法也可以直接声明为实例的方法 onClick (): void { window.alert(this.message) } }

2. vue-property-decorator vue-property-decorator 是一个非官方库,是 vue-class-component 的很好的补充。
它可以让vue的某些属性和方法,通过修饰器的写法让它也写到vue组件实例的类里面。
比如 @Prop @Watch @Emit
2.0 安装下载
# 安装vue的官方插件 npm install vue-class-component vue-property-decorator --save-dev# ts-loader typescript 必须安装,其他的相信你以后也会装上的 npm i ts-loader typescript tslint tslint-loader tslint-config-standard --save-dev

这些库大体的作用,可以按需引入:
  • vue-class-component:强化 Vue 组件,使用 TypeScript/装饰器 增强 Vue 组件
  • vue-property-decorator:在 vue-class-component 上增强更多的结合 Vue 特性的装饰器
  • ts-loader:TypeScript 为 Webpack 提供了 ts-loader,其实就是为了让webpack识别 .ts .tsx文件
  • tslint-loader跟tslint:我想你也会在.ts .tsx文件 约束代码格式(作用等同于eslint)
  • tslint-config-standard:tslint 配置 standard风格的约束
注意: 可以参考此repo vue-typescript-starter脚手架 (完全按照文章一步一步操作)。这个项目起始于在vue加入强类型的念头,目的是提供一个手脚架快速上手和开始, 主要是基于vue + typescript使用 .vue 单文件开发脚手架,支持jsx
2.1 @Component (继承于vue-class-component) @Component装饰器可以接收一个对象作为参数,可以在对象中声明 components ,filters,directives,beforeRouteLeave,beforeRouteEnter等未提供装饰器的选项,也可以声明computed,watch等。
@Component({ components: { HellowWordComponent, }, beforeRouteLeave(to: any, from: any, next: any) { console.log('beforeRouteLeave'); next(); }, beforeRouteEnter(to: any, from: any, next: any) { console.log('beforeRouteLeave'); next(); }, })

2.2 Mixins (继承于vue-class-component) 在使用Vue进行开发时我们经常要用到混合,结合TypeScript之后我们有两种mixins的方法.
一种是vue-class-component提供的:
//定义要混合的类 mixins.ts import Vue from 'vue'; importComponentfrom 'vue-class-component'; @Component// 一定要用Component修饰 export default class myMixins extends Vue { value: string = "Hello" }

// 引入 importComponent{mixins}from 'vue-class-component'; import myMixins from 'mixins.ts'; @Component export class myComponent extends mixins(myMixins) { // 直接extends myMinxins 也可以正常运行 created(){ console.log(this.value) // => Hello } }

第二种方式是在@Component中混入:
我们改造一下mixins.ts,定义vue/type/vue模块,实现Vue接口
// mixins.ts import { Vue, Component } from 'vue-property-decorator'; declare module 'vue/types/vue' { interface Vue { value: string; } }@Component export default class myMixins extends Vue { value: string = 'Hello' }

混入
import { Vue, Component, Prop } from 'vue-property-decorator'; import myMixins from '@static/js/mixins'; @Component({ mixins: [myMixins] }) export default class myComponent extends Vue{ created(){ console.log(this.value) // => Hello } }

总结:
两种方式不同的是在定义mixins时如果没有定义vue/type/vue模块, 那么在混入的时候就要继承该mixins;
如果定义vue/type/vue模块,在混入时可以在@Componentmixins直接引入混入.
2.3 @Prop @Prop装饰器接收一个参数,这个参数可以有三种写法:
  • Constructor,例如String,Number,Boolean等,指定 prop 的类型;
  • Constructor[],指定 prop 的可选类型;
  • PropOptions,可以使用以下选项:type,default,required,validator。
注意: 属性的ts类型后面需要加上undefined类型;或者在属性名后面加上!,表示非null非undefined的断言,否则编译器会给出错误提示。
// 父组件: lang="ts"> import {Component, Vue,} from 'vue-property-decorator'; import PropComponent from '@/components/PropComponent.vue'; @Component({ components: {PropComponent,}, }) export default class PropsPage extends Vue { private name = '张三'; private age = 1; private sex = 'nan'; } // 子组件: lang="ts"> import {Component, Vue, Prop} from 'vue-property-decorator'; @Component export default class PropComponent extends Vue { @Prop(String) readonly name!: string | undefined; @Prop({ default: 30, type: Number }) private age!: number; @Prop([String, Boolean]) private sex!: string | boolean; }

2.4 @PropSync 【typescript|【Vue】vue2.6使用TS之vue-class-component与vue-property-decorator使用详解】@PropSync装饰器与@prop用法类似,二者的区别在于:
  • @PropSync 装饰器接收两个参数:
    propName: string 表示父组件传递过来的属性名;
    options: Constructor | Constructor[] | PropOptions 与@Prop的第一个参数一致;
  • @PropSync 会生成一个新的计算属性。
    注意: 使用PropSync的时候是要在父组件配合.sync使用的
// 父组件 lang='ts'> import { Vue, Component } from 'vue-property-decorator'; import PropSyncComponent from '@/components/PropSyncComponent.vue'; @Component({components: { PropSyncComponent },}) export default class PropSyncPage extends Vue { private like = '父组件的like'; } // 子组件 lang="ts"> import { Component, Prop, Vue, PropSync,} from 'vue-property-decorator'; @Component export default class PropSyncComponent extends Vue { @PropSync('like', { type: String }) syncedlike!: string; // 用来实现组件的双向绑定,子组件可以更改父组件穿过来的值 editLike(): void { this.syncedlike = '子组件修改过后的syncedlike!'; // 双向绑定,更改syncedlike会更改父组件的like } }

2.5 @Model model允许一个自定义组件在使用 v-model 时定制 prop 和 event。
@Model装饰器允许我们在一个组件上自定义v-model,接收两个参数:
  • event: string 事件名。
  • options: Constructor | Constructor[] | PropOptions 与@Prop的第一个参数一致。
    下面是Vue官网的例子
Vue.component('my-checkbox', { model: { prop: 'checked', event: 'change' }, props: { // this allows using the `value` prop for a different purpose value: String, // use `checked` as the prop which take the place of `value` checked: { type: Number, default: 0 } }, // ... })


上述代码相当于:

foo双向绑定的是组件的checked, 触发双向绑定数值的事件是change
使用vue-property-decorator提供的@Model改造上面的例子:
import { Vue, Component, Model} from 'vue-property-decorator'; @Component export class myCheck extends Vue{ @Model ('change', {type: Boolean})checked!: boolean; }

总结, @Model()接收两个参数, 第一个是event值, 第二个是prop的类型说明, 与@Prop类似, 这里的类型要用JS的. 后面在接着是prop和在TS下的类型说明。
// 父组件 lang="ts"> import { Component, Vue } from 'vue-property-decorator'; import ModelComponent from '@/components/ModelComponent.vue'; @Component({ components: {ModelComponent} }) export default class ModelPage extends Vue { private fooTs = 'App Foo!'; } // 子组件 lang="ts"> import {Component, Vue, Model,} from 'vue-property-decorator'; @Component export default class ModelComponent extends Vue { @Model('change', { type: String }) readonly checked!: string public inputHandle(that: any): void { this.$emit('change', that.target.value); // 后面会讲到@Emit,此处就先使用this.$emit代替 } }

2.6 @ModelSync 2.7 @Watch @Watch 装饰器接收两个参数:
  • path: string 被侦听的属性名;
  • options?: WatchOptions={} options可以包含两个属性 :
    • immediate?:boolean 侦听开始之后是否立即调用该回调函数;
    • deep?:boolean 被侦听的对象的属性被改变时,是否调用该回调函数;
注意: 发生在beforeCreate勾子之后,created勾子之前
lang="ts"> import { Vue, Watch, Component } from 'vue-property-decorator'; @Component export default class WatchPage extends Vue { private child = ''; // 监听事件 immediate: true 初始化加载一次deep: true 对象深度监听 @Watch('child', , {immediate: true, deep: true}) onChildChanged(newValue: string, oldValue: string) { console.log(newValue); console.log(oldValue); } }

2.8 @Emit
  • @Emit 装饰器接收一个可选参数,该参数是$emit的第一个参数,充当事件名。如果没有提供这个参数,$emit会将回调函数名的camelCase转为kebab-case,并将其作为事件名;
  • @Emit会将回调函数的返回值作为第二个参数,如果返回值是一个Promise对象,$emit会在Promise对象被标记为resolved之后触发;
  • @Emit的回调函数的参数,会放在其返回值之后,一起被$emit当做参数使用。
// 父组件 lang="ts"> import { Vue, Component } from 'vue-property-decorator'; import EmitComponent from '@/components/EmitComponent.vue'; @Component({ components: { EmitComponent }, }) export default class EmitPage extends Vue { private emitData = https://www.it610.com/article/{ name:'我还没有名字' }; returnPersons(data: any) { this.emitData = https://www.it610.com/article/data; } delemit(event: MouseEvent) { console.log(this.emitData); console.log(event); } } // 子组件 lang="ts"> import { Component, Vue, Prop, Emit, } from 'vue-property-decorator'; type Person = {name: string; age: number; sex: string }; @Component export default class PropComponent extends Vue { private name: string | undefined; private age: number | undefined; private person: Person = { name: '我是子组件的张三', age: 1, sex: '男' }; @Prop(String) readonly sex: string | undefined; @Emit('delemit') private delEmitClick(event: MouseEvent) {} @Emit() // 如果此处不设置别名字,则默认使用下面的函数命名 addToCount(p: Person) { // 此处命名如果有大写字母则需要用横线隔开@add-to-count return this.person; // 此处不return,则会默认使用括号里的参数p; } delToCount(event: MouseEvent) { this.delEmitClick(event); } }

2.9 @Provice @Provide(key?: string | symbol)
2.10 @Inject @Inject(options?: { from?: InjectKey, default?: any } | InjectKey)
2.11 @ProvideReactive @ProvideReactive(key?: string | symbol)
2.12 @InjectReactive @InjectReactive(options?: { from?: InjectKey, default?: any } | InjectKey)
提供/注入装饰器, key可以为string或者symbol类型。
  • 相同点: Provide/ProvideReactive提供的数据,在内部组件使用Inject/InjectReactive都可取到。
  • 不同点: 如果提供ProvideReactive的值被父组件修改,则子组件可以使用InjectReactive捕获此修改。
代码示例:
lang="ts"> import { Vue, Component, Provide, ProvideReactive, } from 'vue-property-decorator'; import provideGrandpa from '@/components/ProvideGParentComponent.vue'; @Component({ components: { provideGrandpa }, }) export default class ProvideInjectPage extends Vue { @Provide() foo = Symbol('fooaaa'); @ProvideReactive() fooReactive = 'fooReactive'; @ProvideReactive('1') fooReactiveKey1 = 'fooReactiveKey1'; @ProvideReactive('2') fooReactiveKey2 = 'fooReactiveKey2'; created() { this.foo = Symbol('fooaaa111'); this.fooReactive = 'fooReactive111'; this.fooReactiveKey1 = 'fooReactiveKey111'; this.fooReactiveKey2 = 'fooReactiveKey222'; } } lang="ts"> import { Component, Vue, Inject, InjectReactive, } from 'vue-property-decorator'; @Component export default class ProvideGSonComponent extends Vue { @Inject() readonly foo!: string; @InjectReactive() fooReactive!: string; @InjectReactive('1') fooReactiveKey1!: string; @InjectReactive() fooReactiveKey2!: string; }

2.13 @Ref @Ref装饰器接收一个可选参数,用来指向元素或子组件的引用信息。如果没有提供这个参数,会使用装饰器后面的属性名充当参数
lang="ts"> import { Vue, Component, Ref } from 'vue-property-decorator'; import RefComponent from '@/components/RefComponent.vue'; @Component({ components: { RefComponent }, }) export default class RefPage extends Vue { @Ref('RefComponent') readonly RefC!: RefComponent; @Ref('aButton') readonly ref!: HTMLButtonElement; getRef() { console.log(this.RefC); console.log(this.ref); } }

2.14 @VModel
import { Vue, Component, VModel } from 'vue-property-decorator'@Component export default class YourComponent 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) }, }, }, }

3. 区别与联系 (1)vue-class-component 是官方出品; vue-property-decorator 是社区出品;
(2)vue-class-component 提供了Vue、Component等;
(3)vue-property-decorator深度依赖了vue-class-component,拓展出了更多操作符:@Prop、@Emit、@Inject、@Model、@Provide、@Watch;
4. 示例代码 开发时正常引入vue-property-decorator就行,引入后写 vue 代码就是如此
lang="ts"> import { Component, Prop, Vue, Watch } from "vue-property-decorator"; import About from "@/views/About.vue"; // 导入组件 import { mapGetters, mapActions } from "vuex" // vuex模块// 跟多使用方法api请移步:https://www.npmjs.com/package/vue-property-decorator/* ts 特殊符号用法 1. 属性或参数中使用 ?:表示该属性或参数为可选项 2. 属性或参数中使用 !:表示强制解析(告诉typescript编译器,这里一定有值),常用于vue-decorator中的@Prop 3. 变量后使用 !:表示类型推断排除null、undefined */// 装饰器用法 @Component({ // 注册组件 components: { About }, beforeRouteLeave(to: any, from: any, next: any) { console.log('beforeRouteLeave'); next(); }, beforeRouteEnter(to: any, from: any, next: any) { console.log('beforeRouteLeave'); next(); }, // computed: mapGetters([ //'xx' // ]), // methods: mapActions([ //'xx' // ]) })// Prop等价于 // props: { //msg: { //type: String //} //count: { //type: Number, //}, //name: { //default: 'default value', //}, //addr: { //type: [String, Boolean], //}, // },export default class HelloWorld extends Vue { @Prop() private msg!: string; @Prop(Number) readonly count: number | undefined @Prop({ default: 'default value' }) readonly name!: string @Prop([String, Boolean]) readonly flag: string | boolean | undefined $refs!: { input: HTMLInputElement } dataMsg = "这就相当于vue + js的data里面的数据"; // 请注意,如果这里直接声明初始值为undefined,则class属性不会是反应性的为了避免这种情况,您可以使用null,value或使用datahook来代替(如下); personObj = { name: 'ts', age: 1 }created() { console.log("created-1") } // Declare mounted lifecycle hook 生命周期 mounted() { console.log("mounted-2") this.$refs.input.focus() }// data hook初始化值 data(){ return { testMsg: undefined } }// 监听事件 immediate: true 初始化加载一次deep: true 对象深度监听 @Watch('personObj.name', {immediate: true, deep: true}) onChangeValue(val:string){ console.log('watch里面的数据:' + val) // ts }// 计算属性---这就要用到 getter了. /* 对于Vue中的计算属性,我们只需要将该计算属性名定义为一个函数,并在函数前加上get关键字即可. 原本Vue中的computed里的每个计算属性都变成了在前缀添加get的函数. */ get newMsg(){ return '你好Ts' + this.dataMsg }// 弹出 Hello World! hello() { alert("Hello World!") } } scoped lang="scss">

现在 vue3.x 已出,对 TS 很友好的支持,所以使用 vue3.x 的话暂时就不需要这种写法了。
https://juejin.cn/post/6844903506676023310
https://vvbug.blog.csdn.net/article/details/106753517
https://blog.csdn.net/vv_bug/article/details/106758835
https://segmentfault.com/a/1190000011744210
https://segmentfault.com/a/1190000011878086?utm_source=sf-similar-article
https://www.cnblogs.com/lhl66/p/13577549.html
https://segmentfault.com/a/1190000018720570

    推荐阅读