vueのイベント設定をもっとわかりやすくする

# 問題点
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(
    @click1="onClick1",
    //- イベントの受け取りでtypoして全くイベントが受けとれない
    @cilck2="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>
```

# 解決方法
`props`と同じように、送るイベントも先に指定すればいいんじゃないかと思いました。
そこでイベントを送る専用の設定項目を増やしてこれを元にイベントを送信するプラグインを作りました。

サンプルをcodesandboxに置いたので試してみてください。
https://codesandbox.io/s/o4o538wlr6

これ以降は具体的に設定した内容について説明します。

## 自前で考えた設定方法
`events`というところに`$emit`したい名前の関数を用意し、その名前で`this.$_emits.~`で実行すると、内部でその名前と同じ名前で`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="$_emits.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.$_emits.click1();
    },
    click2() {
      this.$_emits.click2(10);
    },
    click3() {
      this.$_emits.click3(20, 'test');
    }
  }
}
</script>
```

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

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

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

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

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

```vue:EventEmit.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="$_emits.click2(0)") action2
  p no match emit
  //- click3はvalue, textを渡す必要があるので警告が出る
  button(@click="$_emits.click3()") action3
  p unhandled emit
  //- click4はApp.vue側で設定されていないので警告が出る
  button(@click="$_emits.click4()") action4
</template>

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

### 実行結果
とりあえず親コンポーネント、子コンポーネント、イベント名が出ているのでなんとなく分かるんじゃないでしょうか。
（click3の警告は実際にイベントが発生した時じゃないと出ません）
<img width="658" alt="スクリーンショット 2018-08-31 17.10.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/187768/5e61a72e-b0fd-b29d-39e3-5306b11822f4.png">

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

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

### できなかったこと
* 親コンポーネントと子コンポーネントで引数の数が一致しているかのチェック  
  よく分からないけど引数の数を調べる`.length`値が`$listeners`だと`0`になっていて比較することができませんでした。。。まぁ子コンポーネントの`events`から引数一覧は分かるので前よりはわかりやすくなったんじゃないでしょうか。

## プラグインのインストール方法
もしこれを自分のプロジェクトにも入れたい場合は以下のようにしてください。
まずはEventEmitPlugin.jsを用意します。

```js:EventEmitPlugin.js
/**
 * Object.keysが使えない環境があるので自前で用意する
 * @param {Object} obj - オブジェクト
 * @returns {Array<String>} - objのキーリスト
 */
function keys(obj) {
  const _keys = [];
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      _keys.push(key);
    }
  }
  return _keys;
}

export default {
  install: (Vue, options) => {
    Vue.mixin({
      beforeCreate() {
        // eventsオプションを設定していないものは何もしない
        const events = this.$options.events;
        if (!events) {
          return;
        }

        // コンポーネント情報
        const componentTag = this.$options._componentTag;
        const parentTag = this.$options.parent.$options._componentTag;

        // イベントの設定
        const $emits = {};
        for (const key in events) {
          if (events.hasOwnProperty(key)) {
            $emits[key] = (...args) => {
              const emit = (typeof events[key] === 'function') ? events[key] : events[key].emit;
              // 引数の数があっているかチェック
              if (args.length !== emit.length) {
                console.warn(`<${componentTag}>: '${key}' event args list is not match!`);
              }
              this.$emit(key, ...emit(...args));
            }
          }
        }
        this.$_emits = $emits;

        // listenerをきちんとセットしているかのチェック
        const listenerKeys = keys(this.$listeners);
        keys(events).forEach((key) => {
          const index = listenerKeys.indexOf(key);
          if (index >= 0) {
            listenerKeys.splice(index, 1);
          } else {
            const isOptional = typeof events[key] === 'object' && events[key].optional;
            // optionalでないものは警告を出す
            if (!isOptional) {
              console.warn(`<${parentTag}>: <${componentTag}>'s '${key}' listener is not set.`);
            }
          }
        });
        listenerKeys.forEach((key) => {
          console.warn(`<${parentTag}>: '${key}' is not <${componentTag}>'s listener name.`);
        });
      }
    });
  }
};
```

あとはVueでインストールしたらOKです。

```js
import Vue from 'vue';
import EventEmitPlugin from './EventEmitPlugin.js';

Vue.use(EventEmitPlugin);
```

# 最後に
今回のような機能は割と重要なんじゃないかなと思いました。最初Vueを触った時にこの問題が結構クリティカルで、`$emit`するくらいならReactのようにpropsでonHogeとかで受け取った方がいいのではと思ったりしてました。
ただあの`@hoge`でイベントを受け取れるという機能はただのパラメータとイベントを差別化できるプレフィックスでこの機能は残したいなと思って、今回event設定でも簡単なバリデーションチェックができるようにしました。
（onHogeで書いたとしても引数何渡すか問題は相変わらずありますし）

npmパッケージにしてみようかとも思いましたが、設定ルールは本当にこれでいいか不安もありますし、何よりも保守が大変そうなのでそこまではしませんでした。これを参考に誰かがいいパッケージとか作ってくれると嬉しいです。できれば標準でこの機能が欲しいところですが・・・。