框架-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 編譯后這個組件的參數依然能夠被正確限制
  • 和開發不太相關,暫時忽略


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM