HTML
JavaScript
DesignPatterns
vue.js
Vuex

2018年Vue.jsとVuexを使ってる人に提案するコンポーネントの分類と設計パターン

はじめに

私はVue.js with Vuexを使った業務で1画面30APIを叩く必要のある画面から、たったの数APIしか叩かないけれど、代わりにUIがとても機能的で複雑な画面まで設計し、構築しました。
その際に得られたノウハウを言語化し、共有出来たらと思います。また、これらのノウハウよりも良いノウハウがあった場合は共有・議論して頂けるととても嬉しいです。

※注釈
この記事はサンプルコードが全然ないです。
OSSの様に気軽に編集リクエストでサンプルコード等を提供頂けたら幸いです!

目次

  1. コンポーネント設計の使い分けの効果
  2. コンポーネントに関わるレンズの種類と重要性
  3. 技術者のレンズから覗く、コンポーネントの分類
  4. プレゼンテーションコンポーネントを更に分類
  5. 複雑なコンポーネントの設計パターンとパターン名
  6. デザイナーのレンズでコンポーネントの分類と技術者レンズからのプラクティス
  7. 技術者レンズでの登場人物

使い分けの効果

簡単に言うと、メンテナンスビリティとデバッグ性の向上です。
基本的に1つコンポーネントは1つの責務だけもつべきです。複数の責務を持つと、1つの責務の更新があった場合(機能の変更や、デザインの変更など)、もう一方の責務をもつコードにまで影響が及んでしまうためです。まぁ、変更が起こらない、又は使い捨てる前提のコンポーネントであれば、複数の責務をもっても良いでしょう。

レンズの種類

フロントエンドのコードを書いていく上で、技術者と大きな関係のあるレンズは2種類あると思います。それは、技術者のレンズと、デザイナーのレンズです。フロントエンドのコンポーネントを構築していく上で、この2つのレンズは本当にとても重要な武器となります。デザインと密結合のコードを書いているから当然でしょう。2つのレンズを利用して設計する事はとても重要ですが、2つのレンズを混同してしまって、どっちのレンズで覗いているのかわからなくなったら、一度冷静になって振り返りましょう。

技術者のレンズ

技術者のレンズは、技術者にとって最も身近なレンズでしょう。技術者のレンズを通して覗く場合は、技術的に良いか、悪いか。どの技術を利用するともっともメリットが多く、現在抱えている問題を解決してくれるのか、テストがしやすいか、必要かどうか。まだまだ視点はあるでしょうが、このような視点から見ることが多いでしょうか?

デザイナーのレンズ

デザイナーのレンズは、多くの技術者にとって、あまり身近でないことが多いレンズでしょう。デザイナーのレンズを通して覗く場合は、ページの装飾、セクションの装飾、各UIコンポーネントの装飾。全体に配置する情報、良いUXを与えられるか。私はデザインは素人ですので、こういうのが頭に浮かんでしまいますが、フロントエンドエンジニアにとって最も重要なのは、このページとセクションと各UIコンポーネントの境界の区切り方でしょうか?デザインの分類・区画化は、とてもフロントエンドのコンポーネントの分類・区画化に有用ですしね。余裕があったら、UXの観点からの改善提案もしたいですね。

技術者のレンズから覗く、コンポーネントの分類

コンポーネント(ただのPureコンポーネント)

今回のコンポーネントは、ViewControllerのイディオムでもあります。
コンポーネントは Vue.js の最も強力な機能の1つです。その機能に対していくつか技術的な制約を設けます。

コンポーネントは単体で1ページを作ることも、1つのセクションを成すことも、専用のVuex moduleを持つこともできます。
許可されていること

  • 独自のDOMのマークアップ
  • 専用のVuex moduleの利用
  • 全てのコンポーネントの利用

禁止していること
* 専用のVuex moduleの2つ以上の所持

プレゼンテーションコンポーネント

これは View のイディオムでもあります。しかし、ViewとVueの発音が似ているため、プレゼンテーションコンポーネントと呼称するのがベターでしょう。

このコンポーネントは、絶対に自分自身にAPIとの通信をするコードを持っていません。これはとても重要なことです。ただpropsにある値を利用し、相対するViewを見せてくれます。
初期表示時ではこのpropsが与えられたら、必ずこのViewが表示されるのを保証されます。
その後のこのアクションがあったら、こう表示される、デザインの再現性を保証します。

実際プロジェクトを作成していく際にはほとんどのコンポーネントをこのプレゼンテーションコンポーネントで作成できます。その場合は技術的に良いコードになるでしょうさ。ほら、reduxのドキュメントにもそのような引用がありますよ。なんたって、責務が1つで明確ですし、テストしやすいですから。

I suggest you to start building your app with just presentational components first

また、当然ですが、プレゼンテーションコンポーネントはFlux、Vuexの事を知りません。
プレゼンテーションコンポーネントは自身で状態を持つものと持たないものがあります。
自身の状態とはそのインスタンス内部に変動する可能性のある変数が定義されているかどうかで決まります。つまり、dataの事です。ですのでcomputedや、filtersmethodsなどは違います。
例:

data(){
  return {
    // 何かアクションがあったら変わっちゃうかも(´・ω・`)
    myName: 'はろーわーるど',
  }
}

注釈

この2つはとても命令形プログラミングや、関数型プログラミングの分類に似ています。
どちらの方がより良いのかは製作要件によって異なりますが、正しく見極めてプログラミングすると良いでしょう。

これらはどっちの方が優れているなどはないですが、このパターンのコンポーネントには、こちらの方がマッチしている等の判断ができることはよくあります。それらを見極めて使いましょう。

コンテナコンポーネント

MVCのControllerと類似しています。
コンテナコンポーネントが知っていること

  • Flux、Vuexの事を知っています。
  • 子の相対的な関係性を知っています。(アトミックデザインでも定義されていますね!
  • 子の相対的な関係性を知っているので、大体のコンテナコンポーネントはステートフルであります。
  • プレゼンテーションコンポーネントとコンテナコンポーネントの両方を子に含むことができます
  • ラッピングdiv(よくあるボーダー、パディング用の奴)、を除いて独自のDOMマークアップはありません。

ざっくり説明

よくあるコンテナコンポーネントのパターンはとてもよくできていていると思います。
しかし、コンテナコンポーネントとプレゼンテーションコンポーネントが1体1の関係にあった場合、プレゼンテーションコンポーネントをコンテナコンポーネントから切り離す理由はテストのしやすさぐらいになるでしょう。React with Reduxでは違うでしょうが、Vue.js with Vuexでは、記述するのに増えるコードの量と比べ、見返りは少ないでしょう。

しかし、労力に対する見返りが見合うケースもあります。Vue.js with Vuexでは、ここからがコンテナコンポーネントの本領です。
例えば不具合が発生するととても困ってしまう、個人情報やコアドメインに関するアプリケーションの作成時などです。個人情報が多分に含まれる、個人情報編集画面や、メールの送信画面などでしょうか。もし送りたいメールが他の人に対して送られてしまったり、他人の個人情報を好きに見れてしまったら目も当てられませんね。
その際はコンテナコンポーネントとして切り分け、しっかりとプレゼンテーションコンテナコンポーネントと、Vuexをテストしてあげましょう。
また、コンテナコンポーネントは後ほど出てくる フォームコンポーネントと相性がよいでしょう。フォームコンポーネントは基本的に不具合が発生すると困ると思いますので。

プレゼンテーションコンポーネントを更に分類

ステートフルコンポーネント

自身で状態を持つプレゼンテーションコンポーネントの事を、ステートフルコンポーネントや、ステートフルプレゼンテーションコンポーネントなどと呼ぶことができます。
ステートフルコンポーネントは、Form本体にとても向いています。プレゼンテーションコンポーネント自身にバリデーションのエラーや成功等情報等持ちたいでしょう?errors等と命名されたdataに包んであげてはどうでしょうか。

ステートレスコンポーネント

自身が状態を持たないプレゼンテーションコンポーネントの事は、ステートレスコンポーネントや、純粋コンポーネントなどと呼ぶことができます。
ステートレスコンポーネントは、本当に使い勝手がよく、このpropsが注入されたら、必ずこう表示されていて、コンポーネント自身が自身の状態を変える事がない事が保証されています。
これは、ボタンや、ナビゲーションバー、Formのプレビュー画面等、幅広いコンポーネントにとても向いているでしょう。

複雑なコンポーネントの設計パターン

例えば、以下のような要件が降りてきたとしましょう。

  • 下書きが書ける
  • 初期表示時に下書き状態のメールがあったら表示する
  • プレビューができる
  • 今すぐ投稿ができる
  • 投稿予約ができる
  • 初期表示時に投稿予約されている記事があったらプレビュー状態で投稿予約時間を表示する
  • 投稿予約されている記事を表示してる場合は、下書きに戻すや、削除、今すぐ投稿するのボタンなどがある
  • 下書きメールにも削除ボタンがある
  • 記事のタイトルや、文書にはバリデーションがある

とてもじゃないですが状態がおおすぎて、1プレゼンテーションコンテナコンポーネントとしてかけないと思います。
なぜなら、下書き状態と、プレビュー状態、投稿予約状態でデザインも機能要件も違うからです。
ですので、3つのデザイン要件を満たす 下書きコンポーネント 、プレビューコンポーネント、送信予約コンポーネントをまず作成します。
これらの3つのコンポーネントは以下のパターンで作成できるでしょう。

下書きコンポーネント: ステートフルコンポーネント

プレビューコンポーネント: ステートフルコンポーネント (なぜなら、投稿予約時間に関する情報を保持しておく必要があるからです。それ以外に関しては触れないでおくのがベターでしょう。

送信予約コンポーネント: ステートレスコンポーネント

そして、それらを統合する、記事予約コンポーネントを作成します。記事予約コンポーネントは、これら3つのコンポーネントの相互作用だけに注力します。この際にセクションコンポーネントパターン、コンテナコンポーネントパターンを使っても良いですし、ステートフルコンポーネントで作ってもよいでしょう。しかし、ラッピングDOM以外のマークアップは許可しません。相互作用と全体の状態の管理にだけに注力しましょう。

そして、このコンポーネント設計パターンはMediator パターンととても類似しています。このMediatorパターンを参考にすれば、この場合はどうしたら良いのかなど、複雑なコンポーネントの設計にとても役立つでしょう。

命名

このパターンのコンポーネントは、フォームコンポーネントと命名することができるでしょう。
なぜなら、この複雑なコンポーネントの設計パターンには、大抵フォームが関与していると思います。ですので、身近なフォームという用語をいれると良いのかなと思いました。まぁフォーム以外でもこのパターンが有用なケースはありますが、その場合はプレゼンテーションコンポーネントで適用すると良いのかなと思いました。
これは技術者のレンズから覗いた場合の考察です。デザイナーのレンズからは、ただのUIコンポーネントで変わらないでしょう。

デザイナーのレンズでコンポーネントの分類と技術者レンズからのプラクティス

ページ

コンポーネントでページを作成する場合、これは技術者のレンズから覗いた場合、 ViewController と同じ役目を果たすと思います。
コンテナーコンポーネント + プレゼンテーションコンポーネントで構築することもできますが、コンポーネントで構築する方が、Vueらしく速度もでるのでベターでしょう。

レイアウトの上にコンテンツを当てはめて構築されるページは、特別に扱われるべきです。
Nuxtではpagesのディレクトリの下に管理され、私が所属しているプロジェクトではrailsのディレクトリ構成パターンを写生し、pagesの下に展開しています。
ページは大抵の場合が表示するコンテンツをVuexに依頼したり、UI部品の相対的な関係を自身の状態として定義したりします。
補足すると、UI部品の相対的な関係の状態はコンポーネント自身のプレゼンテーション知識なので、コンポーネント内部で保持したいです。

プラクティス

特定ページでしか使う予定のないコンポーネントは、専用のディレクトリに定義して区画化すると良いでしょう。その特定ページでしか使う予定のないコンポーネントを他のページでも使いたくなった場合は、共通で使われるコンポーネントディレクトリにリファクタリングして移動してあげましょう。

また、私はとある条件が満たされた場合、よくマークアップされたhtmlをそのままページを構成する最上位コンポーネントにリファクタリングして貼り付けます。
その際にセクションを表すコンポーネントや、共通で使われるコンポーネントに一切整理しないでリリースさせます。
その条件はざっくり言うと以下の3つです。

  • 複雑なUI部品が存在しない
  • たいしたVueによるUI部品のコントロールがない(シンプルなロジックが少数しか存在しないケースの事ですね
  • ほとんどの行数がDOMのマークアップだった。

などの場合です。
よく見ると同じ事を言ってると思うでしょうが、つまり言いたいことは1つです。
シンプルなページは、何も問題が発生しません。問題が発生する、又は発生したら、適したパターンを適用させましょう。
ですので、複雑なUI部品などは、コンポーネントに切り分け、シンプルなのは切り分けなくても良いでしょう。

一応コンポーネント化して他の部分でも使いまわしたい!というケースがあるでしょうが、デザインによっては、そうするべきであることはとても稀です。大抵はページ専用だけど、一応共通化しておこうと思う要素は、大半が再利用される場面に出会わないでしょう。
また、そのように切り分けたせいで、コードの見通しがとても悪くなり、デザインやっぱ修正したい、色々変更したいけど、部品化されすぎて、これ作り直したほうが早くない?って事になることが多いです。

セクション

コンポーネントで1セクションを作成する場合、これは技術者のレンズから覗いた場合、 ViewController と同じ役目を果たすと思います。
コンポーネントまたは、コンテナーコンポーネント + プレゼンテーションコンポーネント、好きな方で構築することができます。構築しようとしているセクションがフォームの場合、コンテナーコンポーネント + フォームコンポーネントで構築すると、移植性が高く、テストが容易で堅牢なコンポーネントにできるでしょう。

プラクティス

リッチな画面等では、特定の画面で30APIとか、もしくはもっと利用する場合があるでしょう。
そのような場面に遭遇した際に、このセクションの概念はとてもコンポーネントの分解に役立つでしょう。
なぜなら、そのような画面では色んなドメイン知識がセクション内にラッピングされているからです。たまに各セクションで相互作用がある場合があるでしょうが、それはprops down, event upで簡単に解決できます。
このパターンを適用した場合、ページを構成するコンポーネントが各セクションの配置と相互作用にのみ、注力できます。
また、セクションが使っているコンテンツでは依存を排除しきれいに コンテナーコンポーネント + フォームコンポーネント で作成した場合、コンテナーコンポーネントはページに依存しないので、そのセクションを他のページで特定のアクション発火時にモーダルで表示するなど、他のページでの使い回しがとても容易に実現できます。しかも同じ堅牢性で。

UI部品

ここは省略します。

技術者レンズでの登場人物

  • コンポーネント
  • プレゼンテーションコンポーネント
    • ステートフルコンポーネント
    • ステートレスコンポーネント    
  • コンテナーコンポーネント
  • フォームコンポーネント

超シンプルなサンプル図

こんな感じのページを作成する場合、ページの装飾によりますが、多分こんな感じで切り分けて作成すると思います。その際にテストはプロフィール入力フォームのコンテナーコンポーネントとフォームコンポーネントで作成するでしょうか。
Untitled (8).png

参考資料

  1. Presentational and Container Components
  2. Vue インスタンス
  3. [redux] Presentational / Container componentの分離 - react-redux.connect()のつかいかた
  4. Mediator パターン