Nuxt.jsでVue.jsのSSRのWebアプリケーションの制作についてまとめ

## はじめに
この記事は[Linkbal Advent Calendar 2019](https://qiita.com/advent-calendar/2019/linkbal)の3日目の記事です。
今年は弊社のサービスのリニュアルで`Vue.js`初心者の僕は`Nuxt.js`でプロジェクト最初からリリースまで携わりました。
今回はその経験と学んだことについて紹介したいと思います。

## TL;DR
この記事は僕の経験からいくつかメモや学んだことを共通したいものです。
一番伝えたいのはこの[注意点](#注意点)です。

## 項目
- [事前に知っておくべきこと](#事前に知っておくべきこと)
- [簡単設定でカスタマイズ、拡張しやすい](#簡単設定でカスタマイズ拡張しやすい)
- [便利なモジュール](#便利なモジュール)
- [注意点](#注意点)


## 事前に知っておくべきこと

Nuxt.jsはVue.jsに基づいたフレームワークなので、もちろん事前にVue.js公式ライブラリ（vue, vuexやvue-router）の基礎知識が必要です。その以外に、Vueと同様、Nuxt.jsもライブサイクルがありますので、
以下のNuxtのサーバーサイドレンダリング（SSR）ライブサイクルを知っておいたほうがいいと思います。

![nuxt-lifecycle.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117115/c8dc2a5c-7f61-d806-ed7c-19651af489f5.png)


参考: https://medium.com/@onlykiosk/the-complete-nuxt-guide-940751e1a6a5

- `nuxtServerInit`
このアクションは`Vuex`のストアに定義されたら、リクエストが来たら、ルートを問わずにNuxt.jsはそれを呼び出します。これはユーザー認証やサーバーサイドから共通のデータの取得などのために、使われます。

僕の場合は`nuxtServerInit`にリクエストのUserAgentでデバイスの判定、ユーザー認証や全ページの共通のデータ取得の処理を入れました。

**注意**: Nuxtの公式のドキュメントにも書いてありますが、 この`nuxtServerInit`アクションはPromiseを返すかasync/awaitを使う必要です。
>Note: Asynchronous nuxtServerInit actions must return a Promise or leverage async/await to allow the nuxt server to wait on them.

- `middleware`: 上記の図に書いてあるNuxt.jsのデフォルトのmiddlewareの以外に、カスタマイズのmiddlewareを作り、使うことができます。
- `validate`: ページのパラメーターやクエリーのバリデーション
- `asyncData`と`fetch`はページレンダリングする（コンポーネントを初期化する）前に、非同期の処理でデータを取得するためのメソッドです。

## 簡単設定でカスタマイズ、拡張しやすい
Nuxt.jsのメインの設定ファイル`nuxt.config.js`で簡単に色々な設定を変更、追加できます。例えば: ページのhead（metaやtitleなど）、環境変数やwebpackなど

### Router
`Nuxt.js`は`pages`ディレクトリにコンポーネントを定義したら、自動的にルーティングを生成してくれるって`Nuxt.js`の一つ特徴ですが、
実際のアプリケーションを作る時に、リダイレクトのルートや一つページ（コンポーネント）に対する複数ルートなど場合があるでしょうか？
その時に、Routerはカスタマイズが必要になりますね。

`Nuxt.js`は`vue-router`を使っているので、`vue-router`のフォーマットと以下のような簡単設定でカスタマイズできます。

また、全体ページに適用したい`middleware`は`nuxt.config.js`にも定義できます。

```javascript
// router/customRoutes.js
const CUSTOM_ROUTES = [
  // 既存のページの別ルート
  {
   name: 'custom-event-page',
   path: '/custom/',
   component: '@/pages/event.vue'
  },
  // リダイレクトのルート
  {
   path: '/source_page/:id?',
   redirect: {
    path: '/destination_page/:id?',
    query: { hoge: 'fuga'}
   }
  }
]
```


```javascript
// nuxt.config.js
const customRoutes = require('./router/customRoutes')

module.exports = {
 router: {
    middleware: 'commonMiddleware', // middleware/commonMiddleware.js
    extendRoutes (routes) {
      customRoutes.forEach(route => {
        routes.push(route)
      })
    }
  },
}
```

### Plugins
`Nuxt.js`プラグインは`Vue.js`アプリケーションがインスタンス化される前に実装されます。
外部のパッケージやVueプラグインを`Nuxt.js`アプリケーションで使用したい場合、プラグインを使う必要です。

SSRをサポートしないVueプラグインがありますので、その時にクライアント側のみ使うようにする必要です。

```javascript
// nuxt.config.js
  plugins: [
    { src: '~/plugins/no-ssr-plugin', ssr: false },
  ],
```

そして、Vueインスタンスに注入したい場合、プラグインを使う必要です。
僕の場合は、ページの`asyncData`でエラーの時に、エラーのハンドリングと非同期の処理でデータを取得するため使いました。

```javascript
// plugins/error-handler.js
export default ({ store }, inject) => {
 inject('error', async (errorCode) => {
    await errorHandler(store, errorCode)
    // エラーコードによりエラーのページにリダイレクトする
    error({ statusCode: errorCode })
  })
}
```

```javascript
// pages/component.vue
asyncData ({ app, store, params }) {
   // エラーの場合
   await app.$error(errorCode)
}
```

### Modules

`Nuxt.js`のコア機能を簡単に拡張し、インテグレーションを加える`Nuxt.js`の拡張機能です。
この拡張機能のおかげで、Nuxtコミュニティが広くなって、たくさん便利な機能がシェアされています。

次はいくつか便利なモジュールを紹介したいと思います。

## 便利なモジュール

[Nuxtコミュニティ](https://github.com/nuxt-community)でたくさん便利なモジュールを作ってくれて、シェアされています。
その中に使っていた2つ`Nuxt.js`チームの公式モジュールを紹介したいと思います。

### [@nuxt/axios](https://github.com/nuxt-community/axios-module)
簡単に`Axios`と`Nuxt.js`とを統合するモジュールです。

こちらは`Nuxt.js`で簡単に`Axios`が使えるだけではなく、プラグインで`axios`の`interceptors`を登録できます。

```javascript
// plugins/axios

export default function ({ $axios, res, store }) {
  // リクエストの前処理
  $axios.onRequest(config => {
    config.withCredentials = true
    return config
  })

  // エラーをハンドリングする
  $axios.onError(error => {
    const code = parseInt(error.response && error.response.status)
    if (code === 500) {
      // handle error
    }
    if (process.env.NODE_ENV !== 'production') console.error(error)
  })

  // 返ってくる時の処理
  $axios.onResponse(response => {
    // SSRの時に、responseのヘッダーをチェックして、クライアント側に返すヘッダーをセットする
    if (response.headers && response.headers['xxxx'] && process.server) {
      res.setHeader('yyyyy', response.headers['yyyy'])
    }
  })
}
```

### [@nuxtjs/google-tag-manager](https://github.com/nuxt-community/modules/tree/master/packages/google-tag-manager)

`Nuxt.js`アプリケーションでGoogle Tag Manager（GTM）やGoogle Analytics（GA）が使えるようにするモジュールです。

`Vue.js`のSSRのWebアプリケーションですが、初回のアクセスだけサーバーサイドレンダリングされて、その以降通常の`Vue.js`のアプリケーションになるので、GTMやGAタグのスクリプトをここままに使えないです。

このモジュールを使って、GTMやGA側で以下の設定を適切に変更すれば、問題なく使えます。

GTMやGA側の設定は詳しくないけど、主な変更点はイベントトリガーだと思います。
このモジュールのデフォルトのページビューイベントは`nuxtRoute`です。

## 注意点
この記事で一番伝えたいことは`Nuxt.js`の使用の時の注意点です。以下の2つあります。

### Vuexストアの`state`の値は`function`でなければならない
`Vue.js`アプリケーションでVuexストアの`state`定義はObjectで普通じゃないでしょうか？`Vuex`の公式exampleもそうですし。

ただし、`Nuxt.js`のSSRでそうするとで不要な共有状態が発生する可能性があります。この問題が発生すれば、サーバーサイド側から取ってくるデータを対象外（ユーザー）に送ってしまう可能性があります。

詳しくはこの`Nuxt.js`の[githubのissue](https://github.com/nuxt/nuxt.js/issues/2508)を見てください。

> Regardless of the mode, your state value should always be a function to avoid unwanted shared state on the server side.

↑この注意点は[Nuxt.js](https://nuxtjs.org/guide/vuex-store/#activate-the-store)公式ページにも記載してあります。

参考: https://nuxtjs.org/guide/vuex-store/

- ストア定義の例

**BAD**

```javascript
// store/state.js
export default {
  state_hoge: null,
  state_fuga: null
}
```

```javascript
// store/index.js
import Vuex from 'vuex'
import state from './state'
import getters from './getters'
import mutations from './mutations'
import * as actions from './actions'

const createStore = () => {
  return new Vuex.Store({
    state,
    getters,
    mutations,
    actions
  })
}

export default createStore
```

**GOOD**

```javascript
// store/state.js
export default () => ({
  state_hoge: null,
  state_fuga: null
})
```

```javascript
// store/index.js
import Vuex from 'vuex'
import createState from './state'
import getters from './getters'
import mutations from './mutations'
import * as actions from './actions'

const createStore = () => {
  return new Vuex.Store({
    state: createState(),
    getters,
    mutations,
    actions
  })
}

export default createStore
```

### asyncDataのデータが再利用されてしまう

詳しくはこの`Nuxt.js`の[githubのissue](https://github.com/nuxt/nuxt.js/issues/3441)を見て下さい。

簡単説明なら、以下のようです。

`event`ページの`asyncData`関数がこんな感じです。

```javascript
asyncData ({ $axios }) {
   const res = await $axios.$get('/hoge_api_path')
   const data = { common: res.data.common }
   if (res.data.special_flg) {
     data.isSpecial = true
     data.specialData = res.data.special_data
   }
}
```

1. まず、 `events/123`にアクセスしたら、`asyncData`の返り値は以下の通りです。

```javascript
{
  common: "hogehoge",
  isSpecial: true,
  specialData: "hogedata"
}
```
2. 次、`events/456`にアクセスしたら、`asyncData`の返値は以下の通りです。

```javascript
{
  common: "fugafuga"
}
```

`Nuxt.js`は`asyncData`のデータとVueコンポーネントに定義しているデータにマージして、Vueコンポーネントのデータになる仕組みで、再利用されてしまうので、`events/456`ページのデータは以下のようになってしまいます。

```javascript
{
  common: "fugafuga",
  isSpecial: true,
  specialData: "hogedata"
}
```

ですから、`asyncData`の返り値はifで分けず、絶対に同じフォーマットみたいでなければならない。

**GOOD**

```javascript
asyncData ({ $axios }) {
   const res = await $axios.$get('/hoge_api_path')
   return {
     common: res.data.common,
     isSpecial: res.data.is_special,
     specialData: res.data.is_special ? res.data.special_data : null
   }
}
```

## 終わり
今回は自分のメモみたいで`Nuxt.js`でVue.jsのSSRのWebアプリケーションの制作について学んだことや得たことを紹介しました。
ご拝読いただきありがとうございます。

次回は`Nuxt.js`/`Vue.js`のWebアプリケーションのパフォーマンスの最適化について書く予定です。