自作でVueのカスタムデコレータを作る方法についてまとめます。
実装はvue-property-decoratorを参考にしています。vue-property-decoratorのライブラリのコードを一部読んでみた記事はこちらです。
また、今回実装したコードはこちらに載せてます。
https://github.com/pei223/vue-custom-property-decorator-samples
目次
前提
Vueの環境は揃っていること前提です。
vue-class-componentのcreateDecorator関数を使用します。
npm i vue-class-component
デコレータ関数自体は、createDecorator関数でデコレータを作成して返すようにします。createDecoratorに渡す関数で自作デコレータを作成するという感じです。
createDecoratorに渡す関数の第一引数であるcomponentOptionsにmethodsやwatchなどの関数が入ります。第二引数はデコレータ指定された関数名が入ります。
componentOptionsに色々指定することでmethodsやwatchなどをラッピングできます。
import { createDecorator } from 'vue-class-component'
export function CustomDecorator(options: MultipleMethodWrapOption = {}) {
return createDecorator(function(componentOptions, methodName) {
// ここでmethodsやwatchをラッピングした処理で上書きする
})
}
methodsをラッピングする
今回の例は元のmethods関数を指定した回数実行するデコレータ。以下のようにデコレータ指定するとonClickを3回実行するようになる感じです。
<button @click="onClick">test</button>
...
counter = 0
@MultipleCallMethodWrap({count: 3})
onClick() {
// testボタンをクリックすると3回呼ばれる
console.log('onClick called')
this.counter += 1
}
実際のデコレータ作成の実装をみていきます。
元の関数をcomponentOptions.methods[関数名]で取得し、componentOptions.methods[関数名] = <ラッピングした関数>でラッピングした関数をmethodsに設定できます。
ここでは18~20行目が該当しており、count引数に指定した数だけ実行するようにしています。
beforeDestroyなども同様にcomponentOptions.beforeDestroyで元の関数を取得でき、componentOptions.beforeDestroy = <新しいbeforeDestroy関数>でカスタマイズできます。
import { createDecorator } from 'vue-class-component'
type MultipleMethodWrapOption = {
count?: number
}
/**
* decorator of a wrapping multiple call method
* @param options multiple call options
*/
export function MultipleCallMethodWrap(options: MultipleMethodWrapOption = {}) {
const count = options.count || 1
return createDecorator(function(componentOptions, methodName) {
if (!componentOptions.methods) {
// 到達しない
return
}
const originMethod = componentOptions.methods[methodName]
componentOptions.methods[methodName] = function(...args) {
for (let i=0;i<count;i++) {
originMethod.apply(this as any, args)
}
}
const originBeforeDestroy = componentOptions.beforeDestroy
componentOptions.beforeDestroy = () => {
if (originBeforeDestroy) {
originBeforeDestroy()
}
// cleaning
}
})
}
Watchをラッピングする
次はWatchをラッピングする実装。
今回の例はデコレータ指定するとWatch関数が2回呼ばれるデコレータです。
<input v-model="word" />
...
word = ""
@TwiceCallWatch("word")
onWordChange() {
// wordが変化するごとにcounterが+2になる
this.counter += 1
}
Watchのラッピングは、componentOptions.mixinsでwatchを上書きすることで実現できます。
まず、Watchをラッピングした処理を(this as any)[getCallbackName(methodName)] = <処理>でVueインスタンスに定義します。
(this as any)はVueのインスタンスなので、同じ関数名になると上書きされてしまうためgetCallbackNameのような処理で関数名重複を避ける必要があります。
componentOptions.mixinsにWatchラッピング処理を追加します。watch: {[watchVar]: <ラッピング処理>}でWatch処理を上書きできます。
ここでは(this as any)[getCallbackName(methodName)]()を2回指定することで、元の関数を2回呼ぶラッピング処理になります。
import { createDecorator } from 'vue-class-component'
// To avoid name conflict in Vue instance
const getCallbackName = (methodName: string) => {
return `${methodName}_doubleCallWatch`
}
/**
* decorator of watch function wrapping call twice
* @param watchVar variable name to observe
*/
export function TwiceCallWatch(
watchVar: string,
) {
return createDecorator((componentOptions, methodName: string) => {
componentOptions.mixins = componentOptions.mixins || []
componentOptions.mixins = componentOptions.mixins.concat([
{
created() {
;(this as any)[getCallbackName(methodName)] = () => {
// call watch function twice
;(this as any)[methodName]()
;(this as any)[methodName]()
}
},
beforeDestroy() {
// do nothing
},
},
])
componentOptions.mixins = componentOptions.mixins!.concat([
{
watch: {
// Set watch function
[watchVar]: function () {
;(this as any)[getCallbackName(methodName)]()
},
},
},
])
})
}