框架-Vue Class Component 官方支持(vue 2.*、Vue Class Component、vue-property-decorator 9.0.2、vuex-class 0.3.2)

疑问

  • 注释:装饰器传入的组件选项不具有类型定义无法在 class 中通过 this. 获得完成提醒 ?
  • 因为 Vue 的声明文件天生就具有循环性,TypeScript 可能在推断某个方法的类型的时候存在困难。因此,你可能需要在 render 或 computed 里的方法上标注返回值。
    疑问:Class Components 中并不是真正的继承于 Vue,而是通过装饰器调用了 vue.extends 来生成一个构造函数。在这个过程中定义在类上的方法实际为处理前的实例的方法,所以 methods 使用箭头函数的话 this 会指向处理前的实例。

总结

vue 及相关库提供的类型和对象

// 以下为构造函数和相关类型
import Vue, { VNodeData, VNode } from 'vue'
import VueRouter, { RouteConfig,Route, RawLocation } from "vue-router";
import { mapGetters, mapActions } from 'vuex'

// vuex 对应装饰器
import { State, Getter, Action, Mutation, namespace } from 'vuex-class'

// Component 组件装饰器
// createDecorator 用于创建装饰器
// PropSync 定义使用 async 实现双向绑定的 prop 
// Model 改变 v-model 绑定值的方法
// Provide 普通 inject/provide 的组件写法
// ProvideReactive 可响应 inject/provide 的组件写法
import Component, { createDecorator, mixins } from 'vue-class-component'
import { Component, Vue, Prop, PropSync, Model, Watch, Emit, Ref, Provide,ProvideReactive } from "vue-property-decorator";

@Component([options])

  • {Object} [options]
  • 使用 @Component 装饰器为类添加注释,从而以直观和标准的类语法定义组件数据和方法。
  • options 可以传递任何 Vue 组件选项
  • 当调用原始构造函数以收集初始组件数据时,建议不要 constructor 自己声明。由于Vue类组件的工作方式,constructor 会被调用两次
import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class Counter extends Vue {
  // data 类属性等价于组件 data
  count = 0
  // 如果初始值为undefined,则 class 属性代表的 data 将不是响应的,这意味着将不会检测到对属性的更改
  // 为了避免这种情况,您可以使用 null 或使用 data hook 来代替
  data() {
    return {
      // `hello` will be reactive as it is declared via `data` hook.
      hello: undefined
    }
  }

  // 类方法等价于组件 methods
  increment() {
    this.count++
  }

  // 可以将计算属性声明为类属性 getter / setter
  get name() {
    return this.firstName + ' ' + this.lastName
  }
  set name(value) {
    const splitted = value.split(' ')
    this.firstName = splitted[0]
    this.lastName = splitted[1] || ''
  }

  // data,render 所有 Vue生命周期可以直接声明为类方法,但是您不能通过实例调用它们。
  // 注释:.tsx 文件需要 import 'vue-class-component/hooks' 这个空文件来引入对应 ts 声明才能够实现 vue 选项的输入提醒
  mounted() {
    console.log('mounted')
  }
  render() {
    return <div>Hello World!</div>
  }
}
  • 除了上面的选项,对于所有其他选项,请将它们传递给装饰器函数
import Vue from 'vue'
import Component from 'vue-class-component'

@Component({
  template: '<button @click="onClick">Click!</button>',
})
export default class MyComponent extends Vue {}

@Prop(options: (PropOptions | Constructor[] | Constructor) = {}) decorator

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

@Component
export default class YourComponent extends Vue {
  @Prop(Number) readonly propA: number | undefined
  @Prop({ default: 'default value' }) readonly propB!: string
  @Prop([String, Boolean]) readonly propC: string | boolean | undefined
  // 引入 reflect-metadata 库,把 ts 类自动设为 prop 的 type
  @Prop() age!: number
}

@PropSync(propName: string, options: (PropOptions | Constructor[] | Constructor) = {}) decorator

  • 可以视为在传入 prop 时使用了 .sync
import { Vue, Component, PropSync } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
  @PropSync('name', { type: String }) syncedName!: string
}
// 等价于
export default {
  props: {
    name: {
      type: String
    }
  },
  computed: {
    syncedName: {
      get() {
        return this.name
      },
      set(value) {
        this.$emit('update:name', value)
      }
    }
  }
}

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

  • 注释:参数1可为 undefined 但是不会在父组件上绑定任何事件,要恢复默认值依然需要输入 'input'
  • 注释:参数2为可选,类型说明没有表现出来
  • @Model 定义的 prop 也能够通过 reflect-metadata 自动生成 type,即上面的例子可以不写 { type: Boolean }
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', // 默认为 value
    event: 'change' // 默认为 input
  },
  props: {
    checked: {
      type: Boolean
    }
  }
}

@Watch(path: string, options: WatchOptions = {}) decorator

  • 注释:把一个函数装饰为对当前实例下某个属性的 watch
import { Vue, Component, Watch } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
  @Watch('child')
  onChildChanged(val: string, oldVal: string) {}

  @Watch('person', { immediate: true, deep: true })
  onPersonChanged1(val: Person, oldVal: Person) {}

  @Watch('person')
  onPersonChanged2(val: Person, oldVal: Person) {}
}
// 等价于
export default {
  watch: {
    child: [
      {
        handler: 'onChildChanged',
        immediate: false,
        deep: false
      }
    ],
    person: [
      {
        handler: 'onPersonChanged1',
        immediate: true,
        deep: true
      },
      {
        handler: 'onPersonChanged2',
        immediate: false,
        deep: false
      }
    ]
  },
  methods: {
    onChildChanged(val, oldVal) {},
    onPersonChanged1(val, oldVal) {},
    onPersonChanged2(val, oldVal) {}
  }
}

@Emit(event?: string) decorator

  • 注释:用来装饰一个函数,在函数的最后 this.$emit 抛出返回值
import { Vue, Component, Emit } from 'vue-property-decorator'

@Component
export default class YourComponent extends Vue {
  count = 0
  // 使用函数名为事件名称,没有返回值,默认 emit 函数参数
  @Emit()
  addToCount(n: number) {
    this.count += n
  }

  // 自定义事件名称
  @Emit('reset')
  resetCount() {
    this.count = 0
  }

  // 指定 emit 返回值
  @Emit()
  returnValue() {
    return 10
  }

  // 函数接收原生事件对象时会作为 emit 的第 3 个参数,见下面的对比例子
  @Emit()
  onInputChange(e) {
    return e.target.value
  }

  // 返回是一个 promise 时
  @Emit()
  promise() {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(20)
      }, 0)
    })
  }
}
// 等价于
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    addToCount(n) {
      this.count += n
      this.$emit('add-to-count', n)
    },
    resetCount() {
      this.count = 0
      this.$emit('reset')
    },
    returnValue() {
      this.$emit('return-value', 10)
    },
    onInputChange(e) {
      this.$emit('on-input-change', e.target.value, e)
    },
    promise() {
      const promise = new Promise(resolve => {
        setTimeout(() => {
          resolve(20)
        }, 0)
      })

      promise.then(value => {
        this.$emit('promise', value)
      })
    }
  }
}

@Ref(refKey?: string) decorator

  • 注释:把当前组件中 ref 指向的实例映射到 computed 中,并设置该 computed 不缓存结果(ref 不是响应的)
import { Vue, Component, Ref } from 'vue-property-decorator'

import AnotherComponent from '@/path/to/another-component.vue'

@Component
export default class YourComponent extends Vue {
  @Ref() readonly anotherComponent!: AnotherComponent
  @Ref('aButton') readonly button!: HTMLButtonElement
}
// 等价于
export default {
  computed() {
    anotherComponent: {
      cache: false,
      get() {
        return this.$refs.anotherComponent as AnotherComponent
      }
    },
    button: {
      cache: false,
      get() {
        return this.$refs.aButton as HTMLButtonElement
      }
    }
  }
}

@Provide(key?: string | symbol) / @Inject(options?: { from?: InjectKey, default?: any } | InjectKey) decorator

import { Component, Inject, Provide, Vue } from 'vue-property-decorator'
const symbol = Symbol('baz')
@Component
export class MyComponent extends Vue {
  @Inject() readonly foo!: string
  @Inject('bar') readonly bar!: string
  @Inject({ from: 'optional', default: 'default' }) readonly optional!: string
  @Inject(symbol) readonly baz!: string

  @Provide() foo = 'foo'
  @Provide('bar') baz = 'bar'
}
// 等价于
const symbol = Symbol('baz')
export const MyComponent = Vue.extend({
  inject: { // 接收父、祖组件的 provide
    foo: 'foo',
    bar: 'bar',
    optional: { from: 'optional', default: 'default' },
    [symbol]: symbol
  },
  data() {
    return {
      foo: 'foo',
      baz: 'bar'
    }
  },
  provide() { // 当前组件传入子孙组件的值
    return {
      foo: this.foo,
      bar: this.baz
    }
  }
})

@ProvideReactive(key?: string | symbol) / @InjectReactive(options?: { from?: InjectKey, default?: any } | InjectKey) decorator

  • 装饰 Provide 和 Inject,并使它们具有响应
const key = Symbol()
@Component
class ParentComponent extends Vue {
  @ProvideReactive() one = 'value'
  @ProvideReactive(key) two = 'value'
}

@Component
class ChildComponent extends Vue {
  @InjectReactive() one!: string
  @InjectReactive(key) two!: string
}

mixins

  • 可以接受任意数量的参数
import Vue from 'vue'
import Component, { mixins } from 'vue-class-component'
@Component
class Hello extends Vue {
  hello = 'Hello'
}
@Component
class World extends Vue {
  world = 'World'
}

@Component
export class HelloWorld extends mixins(Hello, World) {
  created () {
    console.log(this.hello + ' ' + this.world + '!') // -> Hello World!
  }
}

vuex

import Vue from 'vue'
import Component from 'vue-class-component'
import {
  State,
  Getter,
  Action,
  Mutation,
  namespace
} from 'vuex-class'

const someModule = namespace('path/to/module')

@Component
export class MyComp extends Vue {
  @State('foo') stateFoo
  @State(state => state.bar) stateBar
  @Getter('foo') getterFoo
  @Action('foo') actionFoo
  @Mutation('foo') mutationFoo
  @someModule.Getter('foo') moduleGetterFoo

  // If the argument is omitted, use the property name 如果省略了参数,则使用属性名
  // for each state/getter/action/mutation type
  @State foo
  @Getter bar
  @Action baz
  @Mutation qux

  created () {
    this.stateFoo // -> store.state.foo
    this.stateBar // -> store.state.bar
    this.getterFoo // -> store.getters.foo
    this.actionFoo({ value: true }) // -> store.dispatch('foo', { value: true })
    this.mutationFoo({ value: true }) // -> store.commit('foo', { value: true })
    this.moduleGetterFoo // -> store.getters['path/to/module/foo']
  }
}

补充现有类型

// 确保在声明补充的类型之前导入 'vue'
import Vue from 'vue'
import { Route, RawLocation } from 'vue-router'

declare module 'vue/types/vue' {
  // 给 vue 实例补充属性声明,例如:vue-router 的 $router、$route 等属性
  // 注释:在 class 中添加该属性的类型提醒
  // 注释:可以配合 Component.registerHooks 添加路由守卫的挂钩和类型声明
  interface Vue {
    $myProperty: string
    beforeRouteEnter?(
      to: Route,
      from: Route,
      next: (to?: RawLocation | false | ((vm: Vue) => void)) => void
    ): void
  }
  // 给 vue 构造函数添加属性
  interface VueConstructor {
    $myGlobal: string
  }
}

// 额外的组件选项
// 注释:这个组件选项是指装饰器中组件选项,并不会出现在 class 上的提醒,而且只对 .tsx 文件有效,对 .vue 文件无效
declare module 'vue/types/options' {
  interface ComponentOptions<V extends Vue> {
    myOption?: string
  }
}
var vm = new Vue({
  myOption: 'Hello'
})

Component.registerHooks(hooks)

  • {Array} hooks
  • 注册方法名称,类组件将这些名称的方法作为挂钩处理。
  • 建议将此注册代码写在单独的文件中,因为您必须在任何组件定义之前注册它们。
  • 注释:webpack 打包时会依序加载并运行所有模块,然后才运行当前模块下的代码
import Component from 'vue-class-component'

Component.registerHooks([
  'beforeRouteEnter',
  'beforeRouteLeave',
  'beforeRouteUpdate'
])

@Component
export default class HelloWorld extends Vue {
  beforeRouteEnter(to, from, next) {...}
}
  • 以下是内置的钩子名称,类组件将它们视为特殊方法,不会被注册为组件的 methods
    • data
    • beforeCreate
    • created
    • beforeMount
    • mounted
    • beforeDestroy
    • destroyed
    • beforeUpdate
    • updated
    • activated
    • deactivated
    • render
    • errorCaptured
    • serverPrefetch
  • Only available in TypeScript. It enables built-in hooks methods auto-complete once your import it 一个空的引入文件,为了实现内置钩子的 TS 提醒
  • 注释:脚手架构建的项目如果安装 vue-tsx-support 库的话,会改变 vue-class-component 库的位置,可以引用 vue-property-decorator 下的库引入
import 'vue-class-component/hooks'

import "vue-property-decorator/node_modules/vue-class-component/hooks";

createDecorator(callback)

  • {Function} callback
  • 返回 {Function}
  • 创建一个装饰器。
  • createDecorator 期望将回调函数作为第一个参数,并且该回调函数将接收以下参数:
    • options:Vue 组件选项对象。对此对象所做的更改将影响所提供的组件。
    • key:这个装饰器需要处理的类上的某个属性的名称,这个装饰器用在那个属性上就传入哪个属性的名称
    • parameterIndex: The index of a decorated argument if the custom decorator is used for an argument. ?
  • 注释:装饰器内的 this 指向组件实例
import Vue from 'vue'
import Component,{ createDecorator } from 'vue-class-component'

const Log = createDecorator((options, key) => {
  const originalMethod = options.methods[key]
  options.methods[key] = function wrapperMethod(...args) {
    console.log(`Invoked: ${key}(`, ...args, ')')
    originalMethod.apply(this, args)
  }
})

@Component
class MyComp extends Vue {
  @Log
  hello(value) {
    // ...
  }
}

以下为原文——————————————————————————————————————————————————————————————————————————————

https://cn.vuejs.org/v2/guide/typescript.html

发布为 NPM 包的官方声明文件

推荐配置

  • 疑问:TS 的相关配置,还看不懂
  • 使用 --noImplicitAny 选项将会帮助你找到这些未标注的方法。
// tsconfig.json
{
  "compilerOptions": {
    // 与 Vue 的浏览器支持保持一致
    "target": "es5",
    // 这可以对 `this` 上的数据属性进行更严格的推断
    "strict": true,
    // 如果使用 webpack 2+ 或 rollup,可以利用 tree-shake:
    "module": "es2015",
    "moduleResolution": "node"
  }
}
  • 注意你需要引入 strict: true (或者至少 noImplicitThis: true,这是 strict 模式的一部分) 以利用组件方法中 this 的类型检查,否则它会始终被看作 any 类型。

开发工具链

工程创建

编辑器支持

基本用法

  • 要让 TypeScript 正确推断 Vue 组件选项中的类型,您需要使用 Vue.component 或 Vue.extend 定义组件:
  • 注释:vue 对 TS 的支持并不完美,默认只支持在组件内进行推断,并且无法识别 this.$refs. 的正确类型
import Vue from 'vue'
const Component = Vue.extend({
  // 类型推断已启用
})

const Component = {
  // 这里不会有类型推断,
  // 因为 TypeScript 不能确认这是 Vue 组件的选项
}

基于类的 Vue 组件

import Vue from 'vue'
import Component from 'vue-class-component'

// @Component 修饰符注明了此类为一个 Vue 组件
@Component({
  // 所有的组件选项都可以放在这里
  template: '<button @click="onClick">Click!</button>'
})
export default class MyComponent extends Vue {
  // 初始数据可以直接声明为实例的属性
  message: string = 'Hello!'

  // 组件方法也可以直接声明为实例的方法
  onClick (): void {
    window.alert(this.message)
  }
}

增强类型以配合插件使用

  • 插件可以增加 Vue 的全局/实例属性和组件选项。在这些情况下,在 TypeScript 中制作插件需要类型声明。庆幸的是,TypeScript 有一个特性来补充现有的类型,叫做模块补充 (module augmentation)。
  • 注释:当给 vue 写插件或着引入插件时,需要在补充 vue 的类型声明时,可以使用 TS 的模块补充特性
  • 注释:给 vue 实例的原型上添加属性
// 1. 确保在声明补充的类型之前导入 'vue'
import Vue from 'vue'

// 2. 定制一个文件,设置你想要补充的类型
//    在 types/vue.d.ts 里 Vue 有构造函数类型
declare module 'vue/types/vue' {
// 3. 声明为 Vue 补充的东西
  interface Vue {
    $myProperty: string
  }
}

var vm = new Vue()
console.log(vm.$myProperty) // 将会顺利编译通过
  • 在你的项目中包含了上述作为声明文件的代码之后 (像 my-property.d.ts) 也可以声明额外的属性和组件选项
  • 疑问: .d.ts 的运行方式?
  • 注释:给 vue 构造函数添加属性
import Vue from 'vue'

declare module 'vue/types/vue' {
  // 可以使用 `VueConstructor` 接口
  // 来声明全局属性,和上面
  interface VueConstructor {
    $myGlobal: string
  }
}
// 全局属性
console.log(Vue.$myGlobal)

// ComponentOptions 声明于 types/options.d.ts 之中
declare module 'vue/types/options' {
  interface ComponentOptions<V extends Vue> {
    myOption?: string
  }
}
// 额外的组件选项
var vm = new Vue({
  myOption: 'Hello'
})

标注返回值

  • 因为 Vue 的声明文件天生就具有循环性,TypeScript 可能在推断某个方法的类型的时候存在困难。因此,你可能需要在 render 或 computed 里的方法上标注返回值。
  • 疑问:因为 vue 嵌套的缘故吗?
import Vue, { VNode } from 'vue'

const Component = Vue.extend({
  data () {
    return {
      msg: 'Hello'
    }
  },
  methods: {
    // 需要标注有 `this` 参与运算的返回值类型
    greet (): string {
      return this.msg + ' world'
    }
  },
  computed: {
    // 需要标注
    greeting(): string {
      return this.greet() + '!'
    }
  },
  // `createElement` 是可推导的,但是 `render` 需要返回值类型
  render (createElement): VNode {
    return createElement('div', this.greeting)
  }
})
  • 如果你发现类型推导或成员补齐不工作了,标注某个方法也许可以帮助你解决这个问题。使用 --noImplicitAny 选项将会帮助你找到这些未标注的方法。

——————————————————————————————————————————————————————————————————————————————————

vue-class-component

总览

  • 使用@Component装饰器为类添加注释,从而以直观和标准的类语法定义组件数据和方法。
<template>
  <div>
    <button v-on:click="decrement">-</button>
    {{ count }}
    <button v-on:click="increment">+</button>
  </div>
</template>

<script>
import Vue from 'vue'
import Component from 'vue-class-component'

// Define the component in class-style
@Component
export default class Counter extends Vue {
  // Class properties will be component data 类属性等价于组件 data
  count = 0

  // Methods will be component methods 类方法等价于组件 methods
  increment() {
    this.count++
  }

  decrement() {
    this.count--
  }
}
</script>
  • 通过以类样式定义组件,不仅可以更改语法,还可以利用某些ECMAScript语言功能,例如类继承和装饰器。
  • 疑问:编译之后这个模块输出的是一个构造函数吗?
  • Vue类组件还提供了一个用于mixin继承的mixins助手,以及一个轻松创建自己的装饰器的createDecorator函数。
  • 疑问:mixin 和继承有什么运用上的区别,它们分别在什么时候被使用更合适
  • 疑问:Vue Property Decorator提供的@Prop和@Watch装饰器。 https://github.com/kaorun343/vue-property-decorator

安装

Vue的CLI设置

手动设置

NPM

  • 注释:这个库提供了一个装饰器
npm install --save vue vue-class-component

构建设置

  • 要使用Vue类组件,您需要在项目中配置TypeScript或Babel,因为它依赖于ECMAScript阶段1装饰器
  • 它不支持阶段2装饰器,因为TypeScript Transpiler仍然仅支持旧的装饰器规范。
  • 注释:@vue/cli 创建的项目是通过 TS 转义装饰器的

TypeScript

  • tsconfig.json在您的项目根目录上创建并指定experimentalDecorators选项,以便其转译装饰器语法:
{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "strict": true,
    "experimentalDecorators": true
  }
}

Babel

  • 安装 @babel/plugin-proposal-decorators 和 @babel/plugin-proposal-class-properties
npm install --save-dev @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties
  • 然后在项目根目录 .babelrc 上配置
  • 由于Vue类组件仅支持阶段1(旧版)装饰器规范,因此需要legacy和loose选项。
{
  "plugins": [
    ["@babel/proposal-decorators", { "legacy": true }],
    ["@babel/proposal-class-properties", { "loose": true }]
  ]
}

CDN

不同的版本

  • Vue类组件针对不同环境和用途提供不同构建。它可以通过运行时或者编译时让同一个代码模块在使用 CommonJs、CMD 甚至是 AMD 的项目中运行。它没有自己专有的规范,是集结了 CommonJs、CMD、AMD 的规范于一身
  • 注释:UMD 通用模块定义规范(Universal Module Definition)
  • 开发
    • vue-class-component.js (UMD)
    • vue-class-component.common.js (CommonJS)
    • vue-class-component.esm.js (用于捆绑器的ES模块)
    • vue-class-component.esm.browser.js (用于浏览器的ES模块)
  • 生产
    • vue-class-component.min.js (UMD)
    • vue-class-component.esm.browser.min.js (用于浏览器的ES模块)

类组件

data

  • 如果初始值为undefined,则 class 属性代表的 data 将不是响应的,这意味着将不会检测到对属性的更改:
import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class HelloWorld extends Vue {
  // `message` will not be reactive value
  message = undefined
}
  • 为了避免这种情况,您可以使用 null 或使用 data hook 来代替
  • 疑问:同时存在类属性和 data hook 是否合法有效?
import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class HelloWorld extends Vue {
  // `message` will be reactive with `null` value
  message = null

  // See Hooks section for details about `data` hook inside class.
  data() {
    return {
      // `hello` will be reactive as it is declared via `data` hook.
      hello: undefined
    }
  }
}

methods

computed

  • 可以将计算属性声明为类属性 getter / setter:
<template>
  <input v-model="name">
</template>

<script>
import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class HelloWorld extends Vue {
  firstName = 'John'
  lastName = 'Doe'

  // Declared as computed property getter
  get name() {
    return this.firstName + ' ' + this.lastName
  }

  // Declared as computed property setter
  set name(value) {
    const splitted = value.split(' ')
    this.firstName = splitted[0]
    this.lastName = splitted[1] || ''
  }
}
</script>

挂钩

  • data,render 所有 Vue生命周期可以直接声明为类方法,但是您不能通过实例调用它们。
import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class HelloWorld extends Vue {
  // Declare mounted lifecycle hook
  mounted() {
    console.log('mounted')
  }

  // Declare render function
  render() {
    return <div>Hello World!</div>
  }
}

其他选项

  • 对于所有其他选项,请将它们传递给装饰器函数:
<template>
  <OtherComponent />
</template>

<script>
import Vue from 'vue'
import Component from 'vue-class-component'
import OtherComponent from './OtherComponent.vue'

@Component({
  // Specify `components` option.
  // See Vue.js docs for all available options:
  // https://vuejs.org/v2/api/#Options-Data
  components: {
    OtherComponent
  }
})
export default class HelloWorld extends Vue {}
</script>

额外的挂钩

  • 如果您使用Vue Router等Vue插件,则可能希望类组件解析它们提供的钩子。
  • 注释:beforeRouteEnter 等路由守卫是组件级守卫,需要先在装饰器中注册它们,要求装饰器进行相应的处理,处理方式应该跟data、render、生命周期一致
// class-component-hooks.js
import Component from 'vue-class-component'

// Register the router hooks with their names
Component.registerHooks([
  'beforeRouteEnter',
  'beforeRouteLeave',
  'beforeRouteUpdate'
])

import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class HelloWorld extends Vue {
  // The class component now treats beforeRouteEnter,
  // beforeRouteUpdate and beforeRouteLeave as Vue Router hooks
  beforeRouteEnter(to, from, next) {
    console.log('beforeRouteEnter')
    next()
  }

  beforeRouteUpdate(to, from, next) {
    console.log('beforeRouteUpdate')
    next()
  }

  beforeRouteLeave(to, from, next) {
    console.log('beforeRouteLeave')
    next()
  }
}
  • 建议将此注册代码写在单独的文件中,因为您必须在任何组件定义之前注册它们。
  • 注释:这个注册只是对装饰器的修改,并不需要依赖 vue 是否 use 了 vue-router
// main.js

// Make sure to register before importing any components
import './class-component-hooks'

import Vue from 'vue'
import App from './App'

new Vue({
  el: '#app',
  render: h => h(App)
})

自定义装饰器

  • createDecorator 期望将回调函数作为第一个参数,并且该回调函数将接收以下参数:
    • options:Vue 组件选项对象。对此对象所做的更改将影响所提供的组件。
    • key:这个装饰器需要处理的类上的某个属性的名称
    • parameterIndex: The index of a decorated argument if the custom decorator is used for an argument. ?
  • 疑问:一个有参数的装饰器是怎么自定义的?
  • 疑问:parameterIndex 是如果传递到装饰器中的?
// decorators.js
import { createDecorator } from 'vue-class-component'

// Declare Log decorator.
export const Log = createDecorator((options, key) => {
  // Keep the original method for later.
  const originalMethod = options.methods[key]

  // Wrap the method with the logging logic.
  options.methods[key] = function wrapperMethod(...args) {
    // Print a log.
    console.log(`Invoked: ${key}(`, ...args, ')')

    // Invoke the original method.
    originalMethod.apply(this, args)
  }
})

import Vue from 'vue'
import Component from 'vue-class-component'
import { Log } from './decorators'
@Component
class MyComp extends Vue {
  // It prints a log when `hello` method is invoked. 这个装饰器装饰了 hello 这个方法,使它在被调用前先进行日志打印
  @Log
  hello(value) {
    // ...
  }
}

扩展和混合

Extend

  • 每个被继承的类都必须是一个类组件。换句话说,它需要继承Vue构造函数作为祖先并由 @Component 装饰器进行装饰。
// super.js
import Vue from 'vue'
import Component from 'vue-class-component'

// Define a super class component
@Component
export default class Super extends Vue {
  superValue = 'Hello'
}
import Super from './super'
import Component from 'vue-class-component'

// Extending the Super class component
@Component
export default class HelloWorld extends Super {
  created() {
    console.log(this.superValue) // -> Hello
  }
}

mixins

  • 注释:JS 的原型链是单链,混入能够实现多个组件配置混入到一个组件内
  • Vue 类组件提供了 mixins 辅助功能,通过使用 mixins 帮助程序,TypeScript 可以推断混合类型并在组件类型上继承它们。
  • 注释:mixins 应该是把两个构造函数合并成了一个
// mixins.js
import Vue from 'vue'
import Component from 'vue-class-component'
// You can declare mixins as the same style as components.
@Component
export class Hello extends Vue {
  hello = 'Hello'
}
@Component
export class World extends Vue {
  world = 'World'
}

import Component, { mixins } from 'vue-class-component'
import { Hello, World } from './mixins'
// Use `mixins` helper function instead of `Vue`.
// `mixins` can receive any number of arguments.
@Component
export class HelloWorld extends mixins(Hello, World) {
  created () {
    console.log(this.hello + ' ' + this.world + '!') // -> Hello World!
  }
}

类组件的警告

  • Vue类组件通过实例化底层的原始构造函数,将类属性收集为 Vue 实例数据。尽管我们可以像本地类方式那样定义实例数据,但有时我们需要知道其工作方式。 ?
  • 疑问:通过 vue 创建一个实例,然后修改这个实例的原型指向继承的类的实例上?
  • 疑问:extends 的本质是 创建一个 vue 实例,作为 MyComp 的原型?
  • 疑问:以下问题可能都是由于 @Component 内部的逻辑造成的。

this属性初始化器中的值

  • 如果将类属性定义为箭头函数并在其中访问 this,它将无法正常工作。这是因为在初始化类属性时,this 只是Vue实例的代理对象
  • 注释:在非 class 组件中,箭头函数 this 指向 undefined
  • 注释:vue 应该是获取组件选项对象,然后用该对象生成一个组件构造函数,然后再通过该构造函数创建对应的 vue 实例。
import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class MyComp extends Vue {
  foo = 123

  // DO NOT do this
  bar = () => {
    // Does not update the expected property.
    // `this` value is not a Vue instance in fact.
    this.foo = 456
  }
}
  • 注释:过去的理解有误,实例能够从原型上继承属性,但是不代表构造函数中定义的属性在原型上存在绑定。
class New {
  new = "new";
  showThis = () => {
    return this;
  };
}
// 等价于
function(){
  this.new = 'new';
  this.showThis = ()=>{
    return this
  }
}

始终使用生命周期挂钩代替constructor

  • 当调用原始构造函数以收集初始组件数据时,建议不要constructor自己声明:
  • 由于Vue类组件的工作方式,fetch 将被意外调用两次。
  • 建议写生命周期挂钩,例如created,而不是constructor
import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class Posts extends Vue {
  posts = []

  // DO NOT do this
  constructor() {
    fetch('/posts.json')
      .then(res => res.json())
      .then(posts => {
        this.posts = posts
      })
  }
}

props 定义

  • Vue类组件没有提供用于 props 定义的专用 API。但是,您可以通过使用规范 Vue.extend 来做到这一点:
  • 注释:使用 class extend class 的方式,继承来的 props 不能被当前 this 正确识别
<template>
  <div>{{ message }}</div>
</template>

<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'

// Define the props by using Vue's canonical way.
const GreetingProps = Vue.extend({
  props: {
    name: String
  }
})

// Use defined props by extending GreetingProps.
@Component
export default class Greeting extends GreetingProps {
  get message(): string {
    // this.name will be typed
    return 'Hello, ' + this.name
  }
}
</script>
  • 注释:可能这个教程比较旧了,实际还可以通过 @Prop 这个装饰器来完成
import { Component, Prop, Vue } from "vue-property-decorator";

@Component
export default class HelloWorld extends Vue {
  @Prop() private msg!: string;
  doSome = () => {
    console.log(this.msg);
  };
  showThis() {
    console.log(this.msg);
  }
}

属性类型声明

  • 有时,您必须在类组件之外定义组件属性和方法。
  • 例如,Vue的官方状态管理库 Vuex 提供 mapGetters 和 mapActions 帮助程序将商店映射到组件属性和方法。这些帮助程序需要在组件选项对象中使用。
  • 可以将组件选项传递给 @Component 装饰器的参数。但是,当它们在运行时运行时,不会自动在类型级别上声明属性和方法。您需要在类组件中手动声明其类型:
import Vue from 'vue'
import Component from 'vue-class-component'
import { mapGetters, mapActions } from 'vuex'

// Interface of post
import { Post } from './post'

@Component({
  computed: mapGetters([
    'posts'
  ]),

  methods: mapActions([
    'fetchPosts'
  ])
})
export default class Posts extends Vue {
  // Declare mapped getters and actions on type level.
  // You may need to add `!` after the property name
  // to avoid compilation error (definite assignment assertion).

  // Type the mapped posts getter.
  posts!: Post[]

  // Type the mapped fetchPosts action.
  fetchPosts!: () => Promise<void>

  mounted() {
    // Use the mapped getter and action.
    this.fetchPosts().then(() => {
      console.log(this.posts)
    })
  }
}

$refs类型扩展

  • $refs 组件的类型声明为处理所有可能的ref类型的最广泛的类型。
  • 注释:$refs. 没有任何输入提示
  • 可以通过覆盖$refs类组件中的类型来指定特定的引用类型
<template>
  <input ref="input">
</template>

<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class InputFocus extends Vue {
  // annotate refs type.
  // The symbol `!` (definite assignment assertion)
  // is needed to get rid of compilation error.
  $refs!: {
    input: HTMLInputElement
  }

  mounted() {
    // Use `input` ref without type cast.
    this.$refs.input.focus()
  }
}
</script>

挂钩自动完成

  • Vue 的类组件提供了内置的钩子类型,这使得能够自动完成对 data,render 和其他生命周期的钩子类组件声明
  • 要启用它,您需要导入 vue-class-component/hooks
  • 注释:这个文件其实是一个空文件,用来引入对于这些钩子的ts声明。即引用这个库后,在类组件中写 render 等方法时将出现对应提示。并且只在 .tsx 文件中有效,在 .vue 文件中无效。
  • 注释:.vue 文件需要写在装饰器参数中才有相应的提醒
// main.ts
import 'vue-class-component/hooks' // import hooks type to enable auto-complete
import Vue from 'vue'
import App from './App.vue'

new Vue({
  render: h => h(App)
}).$mount('#app')
  • 可以自己手动添加自定义钩子
import Vue from 'vue'
import { Route, RawLocation } from 'vue-router'

declare module 'vue/types/vue' {
  // Augment component instance type
  interface Vue {
    beforeRouteEnter?(
      to: Route,
      from: Route,
      next: (to?: RawLocation | false | ((vm: Vue) => void)) => void
    ): void

    beforeRouteLeave?(
      to: Route,
      from: Route,
      next: (to?: RawLocation | false | ((vm: Vue) => void)) => void
    ): void

    beforeRouteUpdate?(
      to: Route,
      from: Route,
      next: (to?: RawLocation | false | ((vm: Vue) => void)) => void
    ): void
  }
}

——————————————————————————————————————————————————————————————————————————————————

vue-property-decorator

安装

  • 注释:vue-property-decorator 包含了 vue-class-component
npm i -S vue-property-decorator

@Prop(options: (PropOptions | Constructor[] | Constructor) = {}) decorator

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

@Component
export default class YourComponent extends Vue {
  @Prop(Number) readonly propA: number | undefined
  @Prop({ default: 'default value' }) readonly propB!: string
  @Prop([String, Boolean]) readonly propC: string | boolean | undefined
}
  • If you'd like to set type property of each prop value from its type definition, you can use reflect-metadata. 疑问:如果喜欢在 ts 中设置 prop 的类型,可以使用 https://github.com/rbuckton/reflect-metadata
  • 疑问:让 TS 的类型自动写为 prop 的 type,便于在改 ts 编译后这个组件的参数依然能够被正确限制
  • Set emitDecoratorMetadata to true 设置 reflect-metadata 的参数 emitDecoratorMetadata 为 true
  • Import reflect-metadata before importing vue-property-decorator (importing reflect-metadata is needed just once.) 在 vue-property-decorator 前引入 reflect-metadata,全局只需要引入一次
  • Each prop's default value need to be defined as same as the example code shown in above. 如果需要编译后有默认值需要在 @Porps 的参数中定义 default。
  • It's not supported to define each default property like @Prop() prop = 'default value'. 这种写法并不被允许,它被视为在组件内改变了 props
import 'reflect-metadata'
import { Vue, Component, Prop } from 'vue-property-decorator'

@Component
export default class MyComponent extends Vue {
  @Prop() age!: number
}

@PropSync(propName: string, options: (PropOptions | Constructor[] | Constructor) = {}) decorator

  • This way you can interface with the property as it was a regular data property whilst making it as easy as appending the .sync modifier in the parent component. 可以视为在传入 prop 时使用了 .sync
  • 注释:这里的 computed 方法,也是开发双向绑定组件的优秀写法
import { Vue, Component, PropSync } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
  @PropSync('name', { type: String }) syncedName!: string
}
// 等价于
export default {
  props: {
    name: {
      type: String
    }
  },
  computed: {
    syncedName: {
      get() {
        return this.name
      },
      set(value) {
        this.$emit('update:name', value)
      }
    }
  }
}

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

  • 注释:参数1可为 undefined 但是不会在父组件上绑定任何事件,要恢复默认值依然需要输入 'input'
  • 注释:参数2为可选,类型说明没有表现出来
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', // 默认为 value
    event: 'change' // 默认为 input
  },
  props: {
    checked: {
      type: Boolean
    }
  }
}
  • @Model property can also set type property from its type definition via reflect-metadata . @Model 定义的 prop 也能够通过 reflect-metadata 自动生成 type,即上面的例子可以不写 { type: Boolean }

@Watch(path: string, options: WatchOptions = {}) decorator

  • 注释:把一个函数装饰为对当前实例下某个属性的 watch
import { Vue, Component, Watch } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
  @Watch('child')
  onChildChanged(val: string, oldVal: string) {}

  @Watch('person', { immediate: true, deep: true })
  onPersonChanged1(val: Person, oldVal: Person) {}

  @Watch('person')
  onPersonChanged2(val: Person, oldVal: Person) {}
}
// 等价于
export default {
  watch: {
    child: [
      {
        handler: 'onChildChanged',
        immediate: false,
        deep: false
      }
    ],
    person: [
      {
        handler: 'onPersonChanged1',
        immediate: true,
        deep: true
      },
      {
        handler: 'onPersonChanged2',
        immediate: false,
        deep: false
      }
    ]
  },
  methods: {
    onChildChanged(val, oldVal) {},
    onPersonChanged1(val, oldVal) {},
    onPersonChanged2(val, oldVal) {}
  }
}

@Provide(key?: string | symbol) / @Inject(options?: { from?: InjectKey, default?: any } | InjectKey) decorator

import { Component, Inject, Provide, Vue } from 'vue-property-decorator'
const symbol = Symbol('baz')
@Component
export class MyComponent extends Vue {
  @Inject() readonly foo!: string
  @Inject('bar') readonly bar!: string
  @Inject({ from: 'optional', default: 'default' }) readonly optional!: string
  @Inject(symbol) readonly baz!: string

  @Provide() foo = 'foo'
  @Provide('bar') baz = 'bar'
}
// 等价于
const symbol = Symbol('baz')
export const MyComponent = Vue.extend({
  inject: { // 接收父、祖组件的 provide
    foo: 'foo',
    bar: 'bar',
    optional: { from: 'optional', default: 'default' },
    [symbol]: symbol
  },
  data() {
    return {
      foo: 'foo',
      baz: 'bar'
    }
  },
  provide() { // 当前组件传入子孙组件的值
    return {
      foo: this.foo,
      bar: this.baz
    }
  }
})

@ProvideReactive(key?: string | symbol) / @InjectReactive(options?: { from?: InjectKey, default?: any } | InjectKey) decorator

  • These decorators are reactive version of @Provide and @Inject. 装饰 Provide 和 Inject,并使它们具有响应
const key = Symbol()
@Component
class ParentComponent extends Vue {
  @ProvideReactive() one = 'value'
  @ProvideReactive(key) two = 'value'
}

@Component
class ChildComponent extends Vue {
  @InjectReactive() one!: string
  @InjectReactive(key) two!: string
}

@Emit(event?: string) decorator

  • 注释:用来装饰一个函数,在函数的最后 this.$emit 抛出返回值
  • If the return value is a promise, it is resolved before being emitted. 如果返回值是一个 promise 对象,则在 promise then 时 emit then 的结果回值
  • If the name of the event is not supplied via the event argument, the function name is used instead. In that case, the camelCase name will be converted to kebab-case. 如果事件名称没有通过参数名称,那么会使用函数名称转换为 - 链接后的名称
import { Vue, Component, Emit } from 'vue-property-decorator'

@Component
export default class YourComponent extends Vue {
  count = 0
  // 使用函数名为事件名称,没有返回值,默认 emit 函数参数
  @Emit()
  addToCount(n: number) {
    this.count += n
  }
  // 自定义事件名称
  @Emit('reset')
  resetCount() {
    this.count = 0
  }
  // 指定 emit 返回值
  @Emit()
  returnValue() {
    return 10
  }
  // 函数接收原生事件对象时会作为 emit 的第 3 个参数
  @Emit()
  onInputChange(e) {
    return e.target.value
  }
  // 返回是一个 promise 时
  @Emit()
  promise() {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(20)
      }, 0)
    })
  }
}
// 等价于
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    addToCount(n) {
      this.count += n
      this.$emit('add-to-count', n)
    },
    resetCount() {
      this.count = 0
      this.$emit('reset')
    },
    returnValue() {
      this.$emit('return-value', 10)
    },
    onInputChange(e) {
      this.$emit('on-input-change', e.target.value, e)
    },
    promise() {
      const promise = new Promise(resolve => {
        setTimeout(() => {
          resolve(20)
        }, 0)
      })

      promise.then(value => {
        this.$emit('promise', value)
      })
    }
  }
}

@Ref(refKey?: string) decorator

  • 注释:把当前组件中 ref 指向的实例映射到 computed 中,并设置该 computed 不缓存结果(ref 不是响应的)
import { Vue, Component, Ref } from 'vue-property-decorator'

import AnotherComponent from '@/path/to/another-component.vue'

@Component
export default class YourComponent extends Vue {
  @Ref() readonly anotherComponent!: AnotherComponent
  @Ref('aButton') readonly button!: HTMLButtonElement
}
// 等价于
export default {
  computed() {
    anotherComponent: {
      cache: false,
      get() {
        return this.$refs.anotherComponent as AnotherComponent
      }
    },
    button: {
      cache: false,
      get() {
        return this.$refs.aButton as HTMLButtonElement
      }
    }
  }
}

——————————————————————————————————————————————————————————————————————————————————

https://github.com/ktsn/vuex-class/

  • vuex 的使用说明

Installation

npm install --save vuex-class

Example

import Vue from 'vue'
import Component from 'vue-class-component'
import {
  State,
  Getter,
  Action,
  Mutation,
  namespace
} from 'vuex-class'

const someModule = namespace('path/to/module')

@Component
export class MyComp extends Vue {
  @State('foo') stateFoo
  @State(state => state.bar) stateBar
  @Getter('foo') getterFoo
  @Action('foo') actionFoo
  @Mutation('foo') mutationFoo
  @someModule.Getter('foo') moduleGetterFoo

  // If the argument is omitted, use the property name
  // for each state/getter/action/mutation type
  @State foo
  @Getter bar
  @Action baz
  @Mutation qux

  created () {
    this.stateFoo // -> store.state.foo
    this.stateBar // -> store.state.bar
    this.getterFoo // -> store.getters.foo
    this.actionFoo({ value: true }) // -> store.dispatch('foo', { value: true })
    this.mutationFoo({ value: true }) // -> store.commit('foo', { value: true })
    this.moduleGetterFoo // -> store.getters['path/to/module/foo']
  }
}

——————————————————————————————————————————————————————————————————————————————————

https://github.com/rbuckton/reflect-metadata

  • 让 TS 的类型自动写为 prop 的 type,便于在改 ts 编译后这个组件的参数依然能够被正确限制
  • 和开发不太相关,暂时忽略
原文地址:https://www.cnblogs.com/qq3279338858/p/12631728.html