疑問
- 注釋:裝飾器傳入的組件選項不具有類型定義無法在 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
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) {
// ...
}
}
以下為原文——————————————————————————————————————————————————————————————————————————————
發布為 NPM 包的官方聲明文件
- Vue 不僅僅為 Vue core 提供了針對 TypeScript 的官方類型聲明,還為 Vue Router 和 Vuex 也提供了相應的聲明文件。
推薦配置
- 疑問: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
}
}
}
}
——————————————————————————————————————————————————————————————————————————————————
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']
}
}
——————————————————————————————————————————————————————————————————————————————————
- 讓 TS 的類型自動寫為 prop 的 type,便於在改 ts 編譯后這個組件的參數依然能夠被正確限制
- 和開發不太相關,暫時忽略