疑问
- 注释:装饰器传入的组件选项不具有类型定义无法在 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 编译后这个组件的参数依然能够被正确限制
- 和开发不太相关,暂时忽略