[Vue] 自作・カスタムデコレータを作ってみる

自作で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)]()
          },
        },
      },
    ])
  })
}

コメントを残す

メールアドレスが公開されることはありません。