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

# はじめに
[コミュニケーションクラウド](https://www.motivation-cloud.com/communication-cloud/)の新規開発からエンジニアとして参画している井川拓信です。

この記事は、「[モチベーションクラウドシリーズ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/mcs)」の12日目の記事となります。

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

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

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

# 状態管理の設計
![状態管理パターン.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114918/34264e4e-a671-6afb-3630-523806e73b68.jpeg)

状態管理の設計は[Vuex](https://vuex.vuejs.org/ja/)の設計を一部カスタマイズして使ってます。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にアップロードに必要な３件のAjaxを順次実行するメソッドをupload(file)メソッドとして実装することで、Actionsはどのような通信が必要かを意識することなく、State/Gettersからのデータ取得やCommit/Dispatchの呼び出しの状態管理へ集中することが出来ます。

# ルーティング
![Vueのパッケージ構成-ルーティング (3).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114918/cdbc6ca2-b657-aabb-8b30-ae890dcea07c.jpeg)

「認証チェック」、「認可チェック」、「ページのタイトルを変更」などのルーティングを横断する機能は、Vue Routerの[ナビゲーションガード](https://router.vuejs.org/ja/guide/advanced/navigation-guards.html)を使用して実現しています。

「認証チェック」、「認可チェック」は[router.beforeEach](https://router.vuejs.org/ja/guide/advanced/navigation-guards.html#%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E3%83%93%E3%83%95%E3%82%A9%E3%83%BC%E3%82%AC%E3%83%BC%E3%83%89)、「ページのタイトルを変更」は[router.afterEach](https://router.vuejs.org/ja/guide/advanced/navigation-guards.html#%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E3%81%AA-after-%E3%83%95%E3%83%83%E3%82%AF)を使用しています。

「認証チェックは必要か」、「必要な権限は何か」、「ページのタイトル」は何かなどの値は[ルートメタフィールド](https://router.vuejs.org/ja/guide/advanced/meta.html)として、ルーティングに設定することができます。

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

# Ajaxの抽象化
![Vueのパッケージ構成-Ajax.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114918/e933afef-dac8-50dc-5a72-3781a260b5d5.jpeg)

Ajaxには[axios](https://github.com/axios/axios)を使用しています。[axiosはVue.jsが一般的なアプローチ](https://jp.vuejs.org/v2/cookbook/using-axios-to-consume-apis.html)として、提案しているライブラリです。

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

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

# 入力検証
入力検証には[validatorjs](https://github.com/skaterdav85/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で入力検証して、検証結果を参照しやすくした