Vueで開発したアプリケーションの構造


はじめに

コミュニケーションクラウドの新規開発からエンジニアとして参画している井川拓信です。

この記事は、「モチベーションクラウドシリーズ Advent Calendar 2019」の12日目の記事となります。

対象とする読者

  • Vue.jsでアプリケーションの新規開発をしようと思ってる人
  • Vue.jsで開発しているアプリケーションの構造で悩んでいる人

概要

CommunicationCloudは2019年2月から開発を始め、2019年8月に正式リリースしました。
2019年2月の新規開発から携わり、CommunicationCloudのフロントエンドの基底処理の設計/製造を行った私から、CommunicationCloudのフロントエンドの構造をご紹介します。

設計方針

  • Vue CLIで初期化したアプリケーションを拡張して開発する
  • 責務をモジュールごとに分離できるようにして、テストを容易にする
  • 共通的かつ横断的な機能を一元管理できるようにする

状態管理の設計

状態管理の設計はVuexの設計を一部カスタマイズして使ってます。Vuexと違う部分としては、Actionsが直接Backend API/S3などの永続化のためのシステムと連携せずにDAO(Data Access Object)を経由するようにしていることです。

Actionsは、「Commitの実行」、「State/Gettersからデータを取得」、「他Actionsの呼び出し」など責務が多くなりがちです。DAOに、「URL/クエリパラメータ/リクエストボディの生成」、「レスポンスのハンドリング」、「HTTPステータスエラー時の制御」の責務を分離することで、Actionsが肥大化しないようにしています。
通信制御をDAOに抽象化することで、一度の操作で複数のBackend APIを呼び出す必要がある場合も、DAO内に影響範囲を抑えることができ、Actionsに簡単に利用できるメソッドを提供できます。

ユニットテスト時には、ActionsでAjaxの考慮を行う必要がなくなるため、テストの責務が分離するメリットがあります。

例としては、ファイルアップロードの機能を実装するために、3件のAjaxを順次実行するとします。
1. Backend APIからS3にアップロードするための署名付きURLを取得する。
2. S3にファイルをアップロードする。
3. Backend APIにファイルをアップロードしたことを通知する。

DAOにアップロードに必要な3件のAjaxを順次実行するメソッドをupload(file)メソッドとして実装することで、Actionsはどのような通信が必要かを意識することなく、State/Gettersからのデータ取得やCommit/Dispatchの呼び出しの状態管理へ集中することが出来ます。

ルーティング

「認証チェック」、「認可チェック」、「ページのタイトルを変更」などのルーティングを横断する機能は、Vue Routerのナビゲーションガードを使用して実現しています。

「認証チェック」、「認可チェック」はrouter.beforeEach、「ページのタイトルを変更」はrouter.afterEachを使用しています。

「認証チェックは必要か」、「必要な権限は何か」、「ページのタイトル」は何かなどの値はルートメタフィールドとして、ルーティングに設定することができます。

ナビゲーションガードとルートメタフィールドを組み合わせて使用することで、ルーティングでフィルタを実現できます。ルーティングでフィルタを実装することで、viewsの各コンポーネントは画面の機能にのみ責務を持てば良いようになります。

Ajaxの抽象化

Ajaxにはaxiosを使用しています。axiosはVue.jsが一般的なアプローチとして、提案しているライブラリです。

axiosにはInterceptorsという機能があります。axiosのInterceptorsを利用することで「requestに認証のためのJWTを設定」、「通信中表示の切り替え」、「エラー制御」などをaxiosが呼び出されたときに実行させることができます。

Ajaxを実行する際、横断的に行う制御をaxiosに寄せることで、DAOはモデルに対する操作にのみ責務を持てば良いようになります。

入力検証

入力検証にはvalidatorjsを使用しています。
validatorjsを採用した理由は、「フレームワーク/ライブラリに依存していない純粋なプログラムによる入力検証」ということです。そのため、Vuexで入力検証を行い、検証結果を管理することができます。
Vuexで検証結果を管理することができるため、検証結果をコンポーネントをまたいで利用することが容易になっています。

ディレクトリ構成

|--App.vue … ルートコンポーネント
|--assets … 共通CSSやメディアファイル
|--common … 汎用的なクラス・関数
|  |--auth … 認証
|  |--error … エラークラス
|  |--http … Ajaxオブジェクト
|--components
|  |--系統の分類 … メンバー用/管理用/個人設定などの分類
|  |  |--モデルの分類 … カテゴリ/タグなどの分類
|  |  |  |--画面の分類 … カテゴリ一覧/新規カテゴリなどの分類
|  |  |  |  |--パーツ.vue … ヘッダ/検索フォーム/一覧などのパーツ毎のコンポーネント
|  |--common … グローバルヘッダなどのコンポーネント
|  |--ui … 各InputやButtonなどを抽象化したコンポーネント
|--dao … DAO(Data Access Object)
|  |--index.js … モデル毎のファイルをまとめるためのオブジェクト
|  |--モデル毎.js … モデル毎の処理
|--filters … Vue.jsのFilter
|--main.js … Vue.jsの初期化
|--plugins … Vueのプラグイン
|--router … Vue Router
|  |--index.js … Vue Routerの初期化
|  |--config.js … Vue Routerの初期化設定
|  |--filters … Vue Routerのフィルタ
|  |--routes … ルーティングの設定
|--store … Vuex
|  |--index.js … Vuexの初期化
|  |--modules … Vuexのモジュール用ディレクトリ
|  |  |--系統の大分類 … メンバー用/管理用/個人設定などの分類
|  |  |  |--モデル毎の分類 … カテゴリ/タグなどの分類
|  |  |  |  |--モデルに対する画面の分類.js … カテゴリ一覧/新規カテゴリなどの分類
|  |  |--common … 画面毎ではないモジュール
|--util … ユーティリティクラス・関数
|--validator … 入力検証(validatorjs)
|  |--index.js … validatorjsの初期化
|  |--lang_*.js … 言語毎の入力検証エラーメッセージの定義
|  |--rules … カスタム入力検証ルール
|--views … ルーティングで表示されるコンポーネント
|  |--系統の大分類 … メンバー用/管理用/個人設定などの分類
|  |  |--App.vue … グローバルヘッダ/ネストしたルーティング画面などの表示
|  |  |--モデル毎の分類 … カテゴリ/タグなどの分類
|  |  |  |--モデルに対する画面の分類.vue … カテゴリ一覧/新規カテゴリなどの分類

まとめ

  • DAO経由で永続化処理を行い、状態管理から永続化処理の責務を分離した
  • ルーティングにフィルタ機能を作成して、ルーティングで「認証」、「認可」、「ページのタイトルを変更」などを行った
  • axiosのInterceptorsを利用して、AjaxにJWTを設定したり、通信表示の切り替え、共通エラー制御などを行った
  • Vuexで入力検証して、検証結果を参照しやすくした