Vueコンポーネントのイベント設定をするプラグインを作る

# 始めに
前にVueのイベントの設定に関する話を書きました。

+ [vueのイベント設定をもっとわかりやすくする](https://qiita.com/wintyo/items/87565805174dca8c2105)

この時はプラグインのファイルをローカルで作って使うようにしてましたが、パッケージにしました。ここではこのパッケージの使用方法について説明します。基本的に使い方は前回の記事と同じですが、少しだけ名前が違っていたりするので注意してください。

+ https://www.npmjs.com/package/@wintyo/vue-event-emission-plugin
+ [サンプルコード](https://codesandbox.io/s/eventemissionplugin-sample-4zbv8)

# 作成の背景
過去の記事でも書きましたが改めてこちらでも書きます。Vueのイベント送信では`this.$emit`を使いますが、これは文字列で定義するため以下のようなエラー問題があります。

+ イベント名を間違える
+ 親コンポーネントでイベントの設定を忘れる
+ イベントになんの引数を入れていいか分からなくなる

```vue:イベントの設定が分からなくなる例（子コンポーネント編）
<template lang="pug">
div
  button(@click="onClick1") click1
  button(@click="onClick2") click2
  button(@click="onClick3") click3
  //- こっちでも送れるからどこで送信しているか探すの大変
  button(@click="$emit('click1')") click1
  //- click3はnumber, stringを送るべきなのに引数が足りてない
  button(@click="$emit('click3', 10)") click3
</template>

<script>
export default {
  methods: {
    onClick1() {
      this.$emit('click1');
    },
    onClick2() {
      // click2のtypoで送信失敗
      this.$emit('cilck2', 10);
    },
    onClick3() {
      this.$emit('click3', 10, 'text');
    }
  }
};
</script>
```

```vue:イベントの設定が分からなくなる例（親コンポーネント編）
<template lang="pug">
div
  Component(
    //- イベントの受け取りでtypoして全くイベントが受け取れない
    @cilck1="onClick1",
    @click2="onClick2",
    //- click3のイベントを設定し忘れる
    //- @click3="onClick3",
    //- そんなイベントは設定していないのでイベントは受け取れない
    @click4="onClick4"
  )
</template>

<script>
import Component from './Component.vue';

export default {
  component: {
    Component
  },
  methods: {
    onClick1() {
    },
    onClick2(value) {
      console.log(value);
    },
    // 本当にvalue, textであっていたか調べにくい
    onClick3(value, text) {
      console.log(value, text);
    },
    onClick4(value, text) {
      console.log(value, text);
    }
  }
};
</script>
```

これの問題を解決するためにoptionに`events`を用意し、そこで設定したメソッドだけ呼び出すプラグインを作りました。

# 使用方法
まずパッケージをインストールします。

```bash
$ yarn add @wintyo/vue-event-emission-plugin
```

プラグインとしてインストールします。

```javascript:インストール
import Vue from 'vue';
import EventEmissionPlugin from '@wintyo/vue-event-emission-plugin';
Vue.use(EventEmissionPlugin);
```

Vueコンポーネントの`events`というところに`$emit`したい名前の関数を用意し、その名前で`this.$events.~`で実行すると、内部でその名前と同じ名前で`this.$emit`します。
event情報は全て`events`で定義しているため、methodsに書こうがtemplateに書こうが送信するイベント一覧は見失わなくなると思います。

```vue：設定例
<template lang="pug">
div
  p method emit
  button(@click="click1") action1
  button(@click="click2") action2
  button(@click="click3") action3
  br
  p direct emit
  button(@click="$events.click2(0)") action2
</template>

<script>
export default {
  // $emitされるイベント一覧（親コンポーネントが受け取れるイベントが一目でわかる）
  events: {
    click1: () => [],
    click2: (value) => [value],
    click3: (value, text) => [value, text]
    // 内部ではこんな感じでキー名と一致したeventsの関数の実行結果を$emitしている
    // const key = 'click1';
    // this.$emit(key, ...events[key](...args));
  },
  methods: {
    click1() {
      this.$events.click1();
    },
    click2() {
      this.$events.click2(10);
    },
    click3() {
      this.$events.click3(20, 'test');
    }
  }
}
</script>
```

## イベントのバリデーションチェック
今回作成したプラグインは以下のようなチェックをします。

+ 子コンポーネントで実行する`$events.~`の引数の数が合わない時に警告（型チェックまでは無理でした）
+ 親コンポーネントでlistenerの受け取り忘れを警告
+ 子コンポーネントに存在しないevent名をlistenerに登録した場合に警告
+ 子コンポーネントで定義していないeventを実行した場合は実行時エラー（そもそもメソッドがないので）

### 例
```vuejs:App.vue（親コンポーネント）
<template lang="pug">
div
  EventEmission(
    @click1="onClick1",
    @click2="onClick2",
    @click3="onClick3",
    //- unhandleEventは`EventEmit`で定義されていないので警告が出る
    @unhandleEvent=""
  )
</template>

<script>
import EventEmission from './components/EventEmission.vue';

export default {
  name: "App",
  components: {
    EventEmission
  },
  methods: {
    onClick1() {
      console.log('click1');
    },
    onClick2(value) {
      console.log('click2', value);
    },
    onClick3(value, text) {
      console.log('click3', value, text);
    }
  }
};
</script>
```

```vuejs:EventEmission.vue（子コンポーネント）
<template lang="pug">
div
  p method emit
  button(@click="click1") action1
  button(@click="click2") action2
  button(@click="click3") action3
  br
  p direct emit
  button(@click="$events.click2(0)") action2
  p no match emit
  //- click3はvalue, textを渡す必要があるので警告が出る
  button(@click="$events.click3()") action3
  p unhandled emit
  //- click4はApp.vue側で設定されていないので警告が出る
  button(@click="$events.click4()") action4
</template>

<script>
export default {
  events: {
    click1: () => [],
    click2: (value) => [value],
    click3: (value, text) => [value, text],
    click4: () => []
  },
  methods: {
    click1() {
      this.$events.click1();
    },
    click2() {
      this.$events.click2(10);
    },
    click3() {
      this.$events.click3(20, 'test');
    }
  }
}
</script>
```

### 実行結果
イベントの設定が上手くいっていない場合は親コンポーネント、子コンポーネント、イベント名が警告として出力されます。
（click3の警告は実際にイベントが発生した時じゃないと出ません）

![スクリーンショット 2020-05-04 23.08.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187768/4a8d16d3-bf35-3f7a-7f2f-09c97938f829.png)

### オプション設定
今のままだとイベントの設定が必須になってしまうので、以下のようにしてオプションで設定できるようにしました。

```vuejs:オプション設定
<script>
export default {
  events: {
    click1: () => [],
    click2: (value) => [value],
    click3: (value, text) => [value, text],
    click4: () => [],
    // オブジェクトで渡す
    optionalEvent: {
      emit: (value) => [value],
      optional: true  // オプション設定
    }
  },
};
</script>
```

# できなかったこと
## 型定義
Vue.jsのoptions APIの型推論を調べてみましたが、かなり複雑でとても推論できるようなものではありませんでした(汗)。なので残念ながらTypeScriptでもanyです。まぁでもイベント名を誤った場合はメソッドが呼べずに実行時エラーになって気づくのでとりあえず大丈夫だと思っています。

+ [Vue.js options APIの型推論について調べてみた](https://qiita.com/wintyo/items/1d51a49e86332a12ee93)

# 終わりに
以上でプラグインの紹介でした。イベントの設定に悩んでいる方はぜひ使ってみてください。