🔦

vue3で実装するFlashMessage(テストもあるよ)(typescriptだよ)

2022/10/02に公開

動機

vue3を利用してライブラリ単体でも実装しやすくて、いろんな機能を使える練習問題にちょうどよさそうな題材ということでFlashMessage(ToastMessageとも言われているアレ)を思いつきました。

同様の実装は軽くググっただけでもライブラリとして溢れています。自前で実装するよりそちらを利用することも実業務では多いかもしれません。

しかしある程度切り出しやすい粒度の機能であれば自前で実装した方が長く見てもコスト安の場合もあるのでこれを機に実装してみました。

作ったものと各種ライブラリバージョン

github: https://github.com/michihiko-karino/vue3-flash-message

vue: 3.2.37
vite: 3.1.0
vitest: 0.23.2
typescript: 4.6.4

実装ポイント解説

作りましただけでは技術記事としてアレなので工夫点などを解説します。

基本的なことばかりですが誰かのためになれば幸いです。

teleportを使おう!

teleportはVue3から追加された機能で、Vue2ではPortalVueなどのプラグインで実現されていました。

teleportはコンポーネントをHTML構造からテレポートさせる機能であり、モーダルなどがよく例にだされます。

FlashMessageもモーダルと同じように画面のFixedな位置に、色々なコンポーネントから呼び出される可能性のあるコンポーネントのため、同様にteleportを使いましょう。

https://github.com/michihiko-karino/vue3-flash-message/blob/main/src/components/FlashMessage.vue#L18-L26

teleportを使うとSpecで実際に表示されているかの検証がちょっとめんどくさくなってしまいます。

https://test-utils.vuejs.org/guide/advanced/teleport.html#interacting-with-the-teleported-component

しかしこれには回避方法があります。テストの段落で説明します。

主なロジックはcomposablesにしてしまおう!

https://github.com/michihiko-karino/vue3-flash-message/blob/main/src/composables/useMessage.ts#L36-L62

リアクティブ変数とそれを変化させるメソッドをVueコンポーネントから別のモジュールに切り出すことで、テストしやすさと型をシンプルにします。

FlashMessageという機能自体がシンプルなおかげでもありますが、リアクティブ変数が3つ、メソッドが2つだけで実装できてしまうのは嬉しいですね。

provide/injectの活用と注意

FlashMessageを各コンポーネントに使ってもらうために、メッセージを表示するメソッドと消すメソッドを各コンポーネントから参照できるようにする必要があります。

そのためにprovide/injectを使いました。

provide/injectはコンポーネントの親子関係を部分的に無視し、親が提供したデータを子孫全体で利用できるようにする機能です。

簡易ストア実装や、アプリコンテキストなどで使われますが、今回のような閉じた機能を作る際にも使えるものだと思っています。

injectするときの型引数を工夫しよう

injectはそのinjectが何を返すかを指定できる型引数を渡せます。

https://github.com/michihiko-karino/vue3-flash-message/blob/main/src/composables/useMessage.ts#L15-L24

FlashMessageを使う側のコンポーネントにはリアクティブ変数を参照されても困るので、メソッドだけが定義されたMutations型を使い、

https://github.com/michihiko-karino/vue3-flash-message/blob/main/src/composables/useMessage.ts#L26-L34

FlashMessageコンポーネント自体は、自分を表示するor消すメソッドに関心がないのでMessageState型を使います。

https://github.com/michihiko-karino/vue3-flash-message/blob/main/src/components/FlashMessage.vue#L7

こんな風にprovideされた値全てを取り出すのではなく必要なものだけにするといいでしょう。

注意: provideした値はprovideしたコンポーネント自身はinjectできない

provide/injectには辛いな〜と思う仕様があります。

provide/injectのキーにSymbolが使えること、もしくはその値自身がある程度の 機能 をもつ場合、それらをまとめて別のモジュールとして定義したくなります。そしてprovideするだけのメソッドを定義したくなります。

当然ですがprovideするコンポーネント自体がその値を利用したい場合もあるでしょう。しかしながらモジュールの中でprovideされた場合コンポーネントは値への参照を失います。

この仕様のため私はprovide/injectを利用する場合はapp level provideを推奨します。これは文字通りcreateAppの返却値でprovideする方法です。

今回の例でもapp level provideを使っています。

https://github.com/michihiko-karino/vue3-flash-message/blob/main/src/messagePlugin.ts#L5-L10

テストを書く

teleportのSpecにハマった

teleportを使っている時wrapper.html()では意味のある内容が表示されません。詳しくは↓のリンクを見てほしいです。

https://test-utils.vuejs.org/guide/advanced/teleport.html#interacting-with-the-teleported-component

ですが内部のDOM的にはちゃんと描画されているのでdocument.〇〇でHTMLを直接見てあげれば問題ありません。

https://github.com/michihiko-karino/vue3-flash-message/blob/main/test/components/FlashMessage.spec.ts#L12-L39

composablesのテストは書きやすいし、モックしやすい

teleportprovide/injectが登場するモジュールのSpecはセットアップが必要になります。

しかしuseMessage.tsのようにVue実装でないモジュールに切り出してしまえばシンプルなSpecになりますし、セットアップも必要ありません。

https://github.com/michihiko-karino/vue3-flash-message/blob/main/test/composables/useMessage.spec.ts

網羅的なSpecはこちらで書き、コンポーネントのSpecでは代表的な例だけを検証すれば良さそうです。

またモジュールに切り出すことでモジュールモックもしやすくなります。

https://github.com/michihiko-karino/vue3-flash-message/blob/main/test/components/MeesageControlls.spec.ts#L7-L12

↑のようにモジュールごとモックすることで、実際にメッセージがHTML上で表示されているかを確認せず、メソッドが呼ばれたかを検証するだけで済むようになり、セットアップコードをへらすことができます。

https://github.com/michihiko-karino/vue3-flash-message/blob/main/test/components/MeesageControlls.spec.ts#L19-L25

最後に

CompositionAPIやVue3からの各種機能があるおかげでVue単体で実装できるものも増え、テストもしやすくなりました。

今回実装したFlashMessageは文字列を表示するだけの単純なものですが、ライブラリ標準機能を使いこなす、テストしやすいモジュールを考えることは普遍的な技術です。

定期的にこういうことをやっていこうと思いました。

以上

Discussion