SPA をつくるときに私が忘れがちなフロントエンドの対応

SPA（シングルページアプリケーション）をつくる場合、そうでない場合とくらべると、バックエンドで対応してたことをフロントエンドで考えなくてはいけなかったり、非同期通信や非同期遷移などの処理を考える必要もでてきます。

ビルド環境の構築、UI の実装、マイクロインタラクションの追求などなどフロントエンドで考えることがどんどんと増えてきています。そのうえで SPA となると上記の対応により、フロントエンドで考えることが山のように増え、忘れてしまいがちなことが私はよくあります。まあ、そんなに SPA の制作経験がないからというのもありますが。

そのような忘れがちな工程の内容と、私がよく使用する環境として **Nuxt.js**（執筆時の時点でのバージョンは 2.8.1）での対応方法を中心に述べていきます。

## 1. エラーページ
初期表示に必要な API を叩いても正常に返えらない場合や、ルーティング対象外のページにアクセスされた場合の表示を考える必要があります。

### 対応方法
Nuxt ではページでエラーを起こした時に使用されるテンプレートが用意されています。
エラーページを表示する際は `context` にある `error` キーを使います。

- [エラーページについて - Nuxt.js](https://ja.nuxtjs.org/guide/views/#%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%9A%E3%83%BC%E3%82%B8)
- [エラー処理について - Nuxt.js](https://ja.nuxtjs.org/guide/async-data/#%E3%82%A8%E3%83%A9%E3%83%BC%E5%87%A6%E7%90%86)

また、ルーティングのフォールバックとして、ページが存在しない場合やエラー起きた際に表示する為のページとして `200.html` が出力されます。これも上記のエラーページのテンプレートが使用されます。

- [フォールバックについて - Nuxt.js](https://ja.nuxtjs.org/guide/routing/#spa-%E3%83%95%E3%82%A9%E3%83%BC%E3%83%AB%E3%83%90%E3%83%83%E3%82%AF)

デフォルトの `200.html` を参照してくれる構成もありますが、対象のファイルを参照するようにバックエンドの担当者に伝えましょう。


## 2. ナビゲーションガード
入力中に変更している内容が保存されていない状態でユーザがページを離れるなどの、内容が消えてしまう操作をした場合にそれを通知して、その操作を取り消すことができるようにすることが望ましいです。

### 対応方法
Nuxt.js に含まれている **Vue Router** には**ナビゲーションガード**という機能があるのでそれを使います。これはブラウザの URL の変化を検知して、そのまま進めることも取り消すこともできます。また、ブラウザの「戻る」や「進む」などの操作にも対応しています。

- [ナビゲーションガードについて - Vue Router](https://router.vuejs.org/ja/guide/advanced/navigation-guards.html)


## 3. ブラウザ履歴によるナビゲーション
データの持ち方によるのですが、ブラウザの戻るなど想定外の操作によるページ遷移をすると、渡すはずのデータを送ることができずに、情報が消えたり、エラーになってしまうことがあります。
主な例としてはエントリーフォームの確認画面から、ブラウザバックで入力画面に戻る操作が考えられます。

### 対応方法
これは明確な答えを持ち合わせていないのですが、Nuxt.js では主に 2 のつ方法が考えられます。

ひとつは、 `context` にある `from` から遷移元の `route` インスタンスを取得する方法です。ただこれは、`route` インスタンスだけなので使える条件が限られます。

- [context について - Nuxt.js](https://ja.nuxtjs.org/api/context/)
- [router.push について - Vue Router](https://router.vuejs.org/ja/guide/essentials/navigation.html)

もうひとつは、`store` に格納する方法です。Nuxt.js には **Vuex** が含まれているのでそれを使います。`store` はページコンポーネントが切り替わっても勝手にリセットされることはないのでデータを維持できます。

- [Vuex について - Nuxt.js](https://ja.nuxtjs.org/guide/vuex-store/)

他にも方法はあるかと思いますし、どこまで対応するかや `store` の使いどころなどと合わせて、設計段階で決めておくとよいかと思います。

## 4. 待機時間中のビュー
初回ロード時、非同期遷移時、非同期通信時など DOM の構築やビューの表示が終わった状態での待機時間が多くあります。その間のどのような対応をするかを考える必要があります。

初回ロードについて補足ですが、SPA でなくても必ず必要な待機時間です。ただ、SPA の場合は合わせて非同期通信をする必要がほとんどです。必要ファイルのロードと DOM の構築に、ビューに必要なデータを取得する非同期通信をあわせて初回ロードとして話を進めます。ただし、SSR をする場合は初回の非同期通信は基本的に発生しません。

### 4-1. 初回ロード時の対応方法
Nuxt.js には**ローディングインジケータ**が用意されおり、`LoadingIndicator` プロパティを設定することで対応できます。内蔵されているいくつかのインジケータを切り替えることもできますが、**カスタムインジケータ**を用意することもできます。

- [ローディングインジケータについて - Nuxt.js](https://ja.nuxtjs.org/api/configuration-loading-indicator/)
- [カスタムインジケータのサンプル - GitHub Gist](https://gist.github.com/syuji-higa/eae8ea0f568b8113dd3863571022bfe7)

### 4-2. 非同期遷移時の対応方法
Nuxt.js には `loading` プロパティが用意されており、初期状態で有効になっています。ですので、特に対応せずともよいのですが、**プログレスバーのカスタマイズ**や**独自のコンポーネント**を使用することも可能です。

- [loading プロパティについて - Nuxt.js](https://ja.nuxtjs.org/api/configuration-loading)

### 4-3. 非同期通信時の対応方法
まず、ビューのパターンとしても全体を覆う、一部分だけ覆う、インジケータだけ表示するなど状況に合わせていろいろとありえます。なので、対応方法も状況に合わせてとしてか書きようがないかと思っています。

ただ、全体を覆うパターンかつ loading プロパティの対応と同様のビューでよい場合は、同じように呼び出すことで対応できます。また、独自のコンポーネントであれば少し内容を変えて表示することも容易です。


## 5. 非同期通信時のエラーの形式統一
帰ってきたエラーが API からのエラーであれば、決められた形式で統一されているのでよいのですが、サーバからのエラー、ネットワークエラーなどもありえます。それらを毎回個別にエラーハンドリングすると大変です。ですので、エラーの形式を統一すると楽になります。

### 対応方法
対応方法といっても、単純に存在し得るエラーの形式を網羅して同じ形式に丸め込む関数をつくり、エラーのレスポンス時に通すようにするだけの話です。

- [エラーの形式を統一する関数のサンプル - GitHub Gist](https://gist.github.com/syuji-higa/a0e54e4fabc1cdadcd05efa6af474ebd)

## 6. 非同期遷移時の解析タグのイベント
非同期遷移時には特別な処理をしないとページビューを取得できな解析タグがほとんどかと思います。

### 対応方法
解析タグは **GTM（Google Tag Manager）** かつ Nuxt.js の場合は `@nuxtjs/google-tag-manager` モジュールを使うとよいと思います。

- [Google Tag Manager](https://tagmanager.google.com/)
- [@nuxtjs/google-tag-manager - GitHub](https://github.com/nuxt-community/modules/tree/master/packages/google-tag-manager)

非同期遷移時にカスタムイベントを送る必要があり、さらに、カスタムイベント名を GTM 側で受け取れるように設定する必要があります。

また、デフォルトではカスタムイベントを送らないです。`pageTracking` オプションをつかうことで対応できるのですが、これにはひとつ問題があります。Nutx.js は **Vue Meta** を使用しており、それにより `title` を更新していますが、更新タイミングが遅いためにページ遷移時に前の `title` が送られてしまいます。

- [Vue Meta](https://vue-meta.nuxtjs.org/)

それより遅いタイミングでカスタムイベントを送るため、ページコンポーネントで `mounted` を使い、つぎに `nextTick` でDOM の更新サイクル後にして、さらに `setTimeout` で非同期にした上で少し遅らせる（調べきれてないですが、非同期にするだけでは確実ではない時があった為）対応をするとよいです。

- [mounted - Vue.js](https://jp.vuejs.org/v2/api/#mounted)
- [nextTick - Vue.js](https://jp.vuejs.org/v2/api/index.html#vm-nextTick)
- [setTimeout - MDN web docs](https://developer.mozilla.org/ja/docs/Web/API/WindowTimers/setTimeout)
- [Nuxt.js GTM 設定サンプル - GitHub Gist](https://gist.github.com/syuji-higa/72b9f345b9d9225d0d1bfc87a9702e5d)

あと、この対応ではエラー画面時に対応できていないので、エラー時にカスタムイベントを送るとよいです。このとき、URL が同じになるのでカスタムイベント名はエラー用のものにすると振り分けができます。
エラーのトラッキングイベントも同様に GTM 側で設定が必要です。Google Analytics 場合はページビューに設定すると、エラーなのか判定ができないので、トラッキングイベントを設定するのがオススメです。


## 7. モックサーバ
既存の API や先に API が完成している場合は必要ないかもですが、API 制作と並行してフロントエンドの作業をすすめる場合はモックサーバがないと色々と辛みがでてきます。
Swagger 2.0 や OpenAPI 3.0 などのスペックの `example` をもとに用意するのが楽だと思います。しかし、フロントエンド開発時に間に合わにないことや、仕様変更時の修正タイミングのズレによりエラーが発生することなどが考えられます。ですので、ローカルにスペック依存ではないモックサーバを用意すると、スムースに作業できると考えられます。

- [Swagger 2.0 について - Swagger](https://swagger.io/docs/specification/2-0/basic-structure/)
- [OpenAPI 3.0 について - Swagger](https://swagger.io/docs/specification/basic-structure/)

ローカルにスペック依存ではないモックサーバをたてるメリットとしては以下があります

- **モックの内容を変更できる**
- **レスポンスを変更してエラーを返せる**
- **レスポンスまでの時間を伸ばして非同期通信時に時間かかる場合の確認ができる**

### 対応方法
スタティックなレスポンスを返す API をつくると考えればよいです。レスポンスの遅延やエラーを返す時に、簡単に書き換えれるような設計にするとよいです。何でつくってもよいのですが、Nuxt.js であれば Node.js でつくるのが早いかもです。

- [モックサーバのサンプル - GitHub Gist](https://gist.github.com/syuji-higa/1e324ee5f5576effe93d4a5c1f1869c9)  
  簡易的なものなので、エンドポイントが多い場合はファイルを分割するとよいと思います


## 8. API プロキシサーバ
ローカルから API にアクセスすると CORS（オリジン間リソース共有） の問題でアクセス制限がかかることがあります。
その場合に、ローカルを何らかの制限のもとに許可してもうらう方法や、`@nuxtjs/proxy` を使ったり、BFF（Backends For Frontends） をするならそれで問題は解決するかと思います（BFF したことないので曖昧）。BFF をしない場合はローカルにプロキシサーバをたててアクセスする方法が便利かなとは思います。（サーバでもプロキシする必要がある場合は別ですが）

- [CORS について - MDN web docs](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)
- [@nuxtjs/proxy](https://github.com/nuxt-community/proxy-module)

プロキシサーバをたてるメリットとしては以下があります

- **常にリクエストとレスポンスのログを表示することができる**  
  （JavaScript でログを取ることもできますが、複数箇所に書いたり、最終的記述を削除したりするのは面倒ですので）
- **モックサーバがローカルにある場合に、同じポートにしておけば切り替えが簡単**  
  （dev サーバを更新せずに API だけを切り替えられる）

### 対応方法
プロキシサーバに向けて API を叩き、プロキシサーバで正しいエンドポイントへアクセスし直すだけですね。それにリクエストとレスポンスのログをとるようにします。モックサーバ同様に何でつくってもよいのですが、Nuxt.js であれば Node.js でつくるのが早いかもです。

- [プロキシサーバのサンプル - GitHub Gist](https://gist.github.com/syuji-higa/2a06ff350281f70a9d664599771b7087)


## 9. 環境ごとのビルド
API の URL を開発時にモックサーバに向けたりするのは面倒ですし、ステージング環境や本番環境のデプロイごとに API の URL を変更するのは危ないですよね。環境設定ファイルを用意して、API の向き先を変えると楽で安全になります。

### 対応方法
まず、環境設定ファイルを用意します。プロジェクトに含めずに、環境ごとにひとつの環境設定ファイルを置く方法もありますが、個人的にはプロジェクトで全ての環境の環境設定ファイルを用意することが多いですのでそちらで考えます。Git のブランチごとに変える方法や、少しであればコマンド内に直接記述する方法もあります。

環境ごとに色々と方法はあると思いますが、Node.js であれば `cross-env` で npm scripts コマンド実行時に環境変数を与えて、`dotenv` により読み込む環境設定ファイルを振り分けることができます。そこで得た API のベース URL を **Axios Module** に設定することで、環境ごとに API の URL を変更してビルドすることができます。

- [Axios Module](https://axios.nuxtjs.org/)
- [Nuxt.js での環境設定ファイル振り分けサンプル - GitHub Gist](https://gist.github.com/syuji-higa/cde49aae4945695a737905bd22cdc946)


# 所感
ここまで書いた私が忘れがちな対応は、全てなくてもサイトとして成り立つものではあります。それでもよりよいサイトをつくること、より開発しやすいプロジェクトにすることなどを考えると、しっかりと対応していきたいと個人的には思っています。まだまだ忘れていることや知らなくて対応できてないことがありそうな気はしてますけれども。