Vue-class-component
ES / TypeScript decorator for class-style Vue components.
Deprecated ??
2년째 갱신되지 않고 있기에(2022-04-20 기준) 지원 중단에 대한 이야기가 나오고 있다. 그리고 vue3의 Composition API와 함께 사용되는 것(<code>
<script setup>
...
</script></code>)에 대한 타당성(?) ... 같은 것이 논의되고 있다.
Build Setup
TypeScript
Create tsconfig.json
on your project root and specify experimentalDecorators option so that it transpiles decorator syntax:
{
"compilerOptions": {
"target": "es5",
"module": "es2015",
"moduleResolution": "node",
"strict": true,
"experimentalDecorators": true
}
}
Class Component
@Component
decorator makes your class a Vue component:
import Vue from 'vue'
import Component from 'vue-class-component'
// HelloWorld class will be a Vue component
@Component
export default class HelloWorld extends Vue {}
Data
Initial data
can be declared as class properties:
<template>
<div>{{ message }}</div>
</template>
<script>
import Vue from 'vue'
import Component from 'vue-class-component'
@Component
export default class HelloWorld extends Vue {
// Declared as component data
message = 'Hello World!'
}
</script>
The above component renders Hello World!
in the <div>
element as message
is component data.
Note that if the initial value is undefined, the class property will not be reactive which means the changes for the properties will not be detected:
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
}
To avoid this, you can use null
value or use data
hook instead:
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
Components methods
can be declared directly as class prototype methods:
<template>
<button v-on:click="hello">Click</button>
</template>
<script>
import Vue from 'vue'
import Component from 'vue-class-component'
@Component
export default class HelloWorld extends Vue {
// Declared as component method
hello() {
console.log('Hello World!')
}
}
</script>
Computed Properties
Computed properties can be declared as class property 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>
Hooks
data
, render
and all Vue lifecycle hooks can be directly declared as class prototype methods as well, but you cannot invoke them on the instance itself. When declaring custom methods, you should avoid these reserved names.
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>
}
}
Other Options
For all other options, pass them to the decorator function:
<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>
TypeScript Example
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)
}
}
Troubleshooting
속성 변경시 갱신되지 않는 현상
import {Vue, Component} from 'vue-property-decorator';
@Component
export default class extends Vue {
_paused = false;
get status() {
return this._paused ? "A" : "B";
}
}
위와 같은 코드가 있을 때, _
로 시작하는 속성은 get status()
에 반영되지 않더라... _
을 제거하면 된다.
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders
Prop 으로 지정하지 않은, Data member 인데 이 경고가 출력될 경우
컴포넌트를 포함시킬 때, 하위 컴포넌트에서 속성이 중복되면 저런 에러가 출력된다.
<script lang="ts">
import {Component, Prop, Emit} from 'vue-property-decorator';
import VueBase from '@/base/VueBase';
@Component
export default class TableProjects extends VueBase {
// ...
loading = false;
// ...
}
</script>
에러:
vue.esm.js?a026:628 [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "loading"
found in
---> <TableProjects>
<MainAdminProjects> at src/pages/MainAdminProjects.vue
<MainAdmin> at src/pages/MainAdmin.vue
<VMain>
<VApp>
<Main> at src/pages/Main.vue
<VApp>
<App> at src/App.vue
<Root>
vue-devtools를 설치하여 확인해보니, loading
이 data
가 아닌, props
로 등록되어 있었다.
디버깅/추적은 다음과 같이 진행하였다. 위의 에러가 출력되는 console.error
위치에 Breakpoint 를 찍고, 원인이 되는 변수의 할당 위치를 조금씩 추적한다.
결국, vue-class-component의 componentFactory
함수에 도달, 해당 함수에서 다음 위치가 문제로 확인되었다.
// ...
var decorators = Component.__decorators__;
if (decorators) {
decorators.forEach(function (fn) {
return fn(options);
});
delete Component.__decorators__;
} // find super
// ...
위의, Component.__decorators__
에, 다른 컴포넌트에서 선언한 데코레이터들이 모두 (e.g. @Prop
) 모두 모여져 있었고, 다른 컴포넌트에서 @Prop() readonly loading!: boolean;
으로 선언한 데코레이터 선언 함수가 호출되었다.
위의, Component
와 Component.__decorators__
를 선언하는 위치는, vue-class-component 의 createDecorator
함수였고, 구현은 다음과 같다:
export function createDecorator (factory: (options: ComponentOptions<Vue>, key: string, index: number) => void): VueDecorator {
return (target: Vue | typeof Vue, key?: any, index?: any) => {
const Ctor = typeof target === 'function'
? target as DecoratedClass
: target.constructor as DecoratedClass
if (!Ctor.__decorators__) {
Ctor.__decorators__ = []
}
if (typeof index !== 'number') {
index = undefined
}
Ctor.__decorators__.push(options => factory(options, key, index))
}
}
위의 내용으로 미루어 볼 때, Ctor
변수가 모든 컴포넌트에 공유되는 현상이 문제가 되었고, 3~5번째 줄의 target
은 VueComponent
인스턴스 여야 하는데(확인 필요) 클래스의 프로토타입 이였다.
위 내용으로 미루어 보아, 처음 클래스 정의부 에서 VueBase
클래스가 컴포넌트가 아니여서 발생된 문제로 확인되었다.
console.dir(VueBase)
출력이 ƒ VueComponent(options)
로 나와야 하지만, 출력해보니 ƒ VueBase()
로 출력되었다. (참고로 console.dir(Vue)
는 출력이 ƒ Vue(options)
이다.)
최종적으로 아래의 코드를
아래와 같이 바꿨다.
그리고 해결.
TypeScript Getter 오동작 오류
다음과 같이 vue-router 를 사용하는 코드가 있다.
@Component
export default class VueBase extends Vue {
get currentRouteName(): string {
return this.$router.currentRoute.name || '';
}
moveTo(name: string, params?: object) {
if (this.currentRouteName === name) {
return;
}
const rawLocation = {
name: name,
params: params,
} as RawLocation;
this.$router.push(rawLocation).catch((reason: any) => {
if (reason.name !== 'NavigationDuplicated') {
throw reason;
}
})
}
}
위의 currentRouteName
함수는 Getter 로 지정되어 있는데, @Component
를 통해 Computed 함수로 변환된다.
Computed Getter 의 경우 구현체는 아래와 같다:
function createComputedGetter (key) {
return function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
Watcher.prototype.evaluate = function evaluate () {
this.value = this.get();
this.dirty = false;
};
/**
* Depend on all deps collected by this watcher.
*/
Watcher.prototype.depend = function depend () {
var i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
};
코드를 분석하면 알겠지만, watcher.value
값으로 한번 캐싱되고, 종속 변수의 수정 또는 watcher 를 통한 값의 변경(dirty
)이 없으면 캐싱된 내용으로 반환된다.
문제는, this.$router.currentRoute.name
로 지정된 변수는 위의 두 가지 조건에 모두 부합되지 않는다. 이를 해결하려면 this.$router.currentRoute.name
를 @Watch
로 확인하거나, get
을 사용한 computed 적용을 피해야 한다.