問題点
vueのイベントはthis.$emit
で気軽に送ることはできますが、気軽にできることで以下のような問題が起きると思います。
こういったエラーは全く警告を出してくれないので相当ハマってしまうと思います。
- イベント名を間違える
- 親コンポーネントでイベントの設定を忘れる
- イベントになんの引数を入れていいか分からなくなる
<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>
<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に書こうが送信するイベント一覧は見失わなくなると思います。
<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を実行した場合は実行時エラー(そもそもメソッドがないので)
例
<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>
<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の警告は実際にイベントが発生した時じゃないと出ません)
オプション設定
今のままだとイベントの設定が必須になってしまうので、以下のようにしてオプションで設定できるようにしました。
<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を用意します。
/**
* 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です。
import Vue from 'vue';
import EventEmitPlugin from './EventEmitPlugin.js';
Vue.use(EventEmitPlugin);
最後に
今回のような機能は割と重要なんじゃないかなと思いました。最初Vueを触った時にこの問題が結構クリティカルで、$emit
するくらいならReactのようにpropsでonHogeとかで受け取った方がいいのではと思ったりしてました。
ただあの@hoge
でイベントを受け取れるという機能はただのパラメータとイベントを差別化できるプレフィックスでこの機能は残したいなと思って、今回event設定でも簡単なバリデーションチェックができるようにしました。
(onHogeで書いたとしても引数何渡すか問題は相変わらずありますし)
npmパッケージにしてみようかとも思いましたが、設定ルールは本当にこれでいいか不安もありますし、何よりも保守が大変そうなのでそこまではしませんでした。これを参考に誰かがいいパッケージとか作ってくれると嬉しいです。できれば標準でこの機能が欲しいところですが・・・。