miyazaki-dev

Nuxt.js(Vue.js)SSRのライフサイクルを完全に理解したい(wip)

JavaScript

最近、Nuxt.jsで開発をする機会をいただいたもののnuxtのライフサイクルになかなかつまずいたのでまとめてみました。

fetchやmounted、createdなどはなんとなくわかったものの、middrewareやpluginsが混ざると本当によくわからなくなる。。

まだまだnuxt.js2ヶ月と超絶初心者で網羅できてないライフサイクル + 間違いもあるかと思いますので、随時更新予定です。

検証するライフサイクル

SSR(サーバーサイドレンダリング)とCSR(クライアントサイドレンダリング)で頭がごちゃごちゃになるかと思いますので 別々で書いていきます。

SSRは、URLで直接アクセスされた際に、動くもの。 CSRは、nuxt.js上のアプリケーション上でリンククリックされた際にに動くもの。

として考えています。

検証コードは下記の通り。

念のため、全部に process.server でサーバーで実行してるかどうか判断。

  middleware: 'pageMiddleware',
  data() {
    return {
      hoge: 'hogehoge'
    }
  },
  asyncData () {
    if (process.server) {
      console.log(`SSR in asyncData | this.hoge error`) // hogeはundefinedエラー
    } else {
      console.log(`CSR in asyncData | this.hoge error`) // hogeはundefinedエラー
    }
  },
  fetch () {
    if (process.server) {
      console.log(`SSR in fetch | `, this.hoge, this.computedHoge)
    } else {
      console.log(`CSR in fetch | `, this.hoge, this.computedHoge)
    }
  },
  validate({ params, query, store }) {
    if (process.server) {
      console.log(`SSR in validate | `, this.hoge, this.computedHoge)
    } else {
      console.log(`CSR in validate | `, this.hoge, this.computedHoge)
    }
    return true
  },
  beforeCreate () {
    if (process.server) {
      console.log(`SSR in beforeCreate | `, this.hoge, this.computedHoge)
    } else {
      console.log(`CSR in beforeCreate | `, this.hoge, this.computedHoge)
    }
  },
  created () {
    if (process.server) {
      console.log(`SSR in created | `, this.hoge, this.computedHoge)
    } else {
      console.log(`CSR in created | `, this.hoge, this.computedHoge)
    }
  },
  beforeMount () {
    if (process.server) {
      console.log(`SSR in beforeMount | `, this.hoge, this.computedHoge)
    } else {
      console.log(`CSR in beforeMount | `, this.hoge, this.computedHoge)
    }
  },
  mounted () {
    if (process.server) {
      console.log(`SSR in mounted | `, this.hoge, this.computedHoge)
      console.log(this.computedHoge())
    } else {
      console.log(`CSR in mounted | `, this.hoge, this.computedHoge)
    }
  },
  beforeUpdate() {
    if (process.server) {
      console.log(`SSR in beforeUpdate | `, this.hoge, this.computedHoge)
      console.log(this.computedHoge())
    } else {
      console.log(`CSR in beforeUpdate | `, this.hoge, this.computedHoge)
    }
  },
  updated() {
    if (process.server) {
      console.log(`SSR in updated | `, this.hoge, this.computedHoge)
      console.log(this.computedHoge())
    } else {
      console.log(`CSR in updated | `, this.hoge, this.computedHoge)
    }
  },
  beforeDestroy() {
    if (process.server) {
      console.log(`SSR in beforeDestroy | `, this.hoge, this.computedHoge)
      console.log(this.computedHoge())
    } else {
      console.log(`CSR in beforeDestroy | `, this.hoge, this.computedHoge)
    }
  },
  destroyed() {
    if (process.server) {
      console.log(`SSR in destroyed | `, this.hoge, this.computedHoge)
      console.log(this.computedHoge())
    } else {
      console.log(`CSR in destroyed | `, this.hoge, this.computedHoge)
    }
  },
  computed: {
    computedHoge () {
      return `computed_${this.hoge}`
    }
  }

SSRのライフサイクル

結果から書くと

------ サーバーサイド ------
→ plugin
→ middreware
→ validate
→ asyncData
→ fetch
→ beforeCreate
→ computed
→ created
------ クライアントサイド ------
→ plugin
→ beforeCreate
→ computed
→ created
→ beforeMount
→ mounted

となりました。

各々の実行する目的等まとめておきます。

plugin

割と共通処理を書きたい場合は、pluginsに記述する場合が多いけれど、初回はサーバーサイドでもクライアントサイドでも呼び出されるので注意が必要かも。

もし同じファイルでサーバーとクライアントで処理を分けたい場合には、process.server 等で条件分岐が必要になりそうです。

そうでないならば、

export default {
  plugins: [
    { src: '~/plugins/both-sides.js' },
    { src: '~/plugins/client-only.js', mode: 'client' },
    { src: '~/plugins/server-only.js', mode: 'server' }
  ]
}

https://ja.nuxtjs.org/api/configuration-plugins/

のようにしてサーバーと分けてあげれば良さそう。

またファイル名で規約作っても行けるらしい。

export default {
  plugins: [
    '~/plugins/foo.client.js', // クライアントサイド限定
    '~/plugins/bar.server.js', // サーバーサイド限定
    '~/plugins/baz.js' // クライアントサイドとサーバーサイド両方
  ]
}

参照:https://ja.nuxtjs.org/guide/plugins/

nuxtServerInit

こちらはサーバーでSSRの初回にのみ読み込まれる関数。

// /store/index.js
export const actions = {
  nuxtServerInit ({ commit }, { req }) {
  }
}

と呼び出してあげれば良いのでなかなか便利。 auth情報とかまず必要なデータを読み込んであげるイメージ。

ちなみにどこかで見ましたが store/index.js 以外では読み込まれないぽい。

middreware

middrewareは基本的には権限周りの処理を任せることが多い(?)

リダイレクト系はこの辺で管理してあげると読みやすくなりそう。

マイページをみるためにはログインしてるとか。ログインしていなければredirectでloginページに飛ばしてあげる等が可能。

// middreware/authenticated.js
export default function ({ store, redirect }) {
  // ユーザーが認証されていないとき
  if (!store.state.authenticated) {
    return redirect('/login')
  }
}

https://ja.nuxtjs.org/api/pages-middleware

特定のページのみで行いたい場合は pages/hogehoge.vue

// pages/hogehoge.vue
export default {
  middleware: 'authenticate',
}

とか記述してあげれば良さげ。

全体にかけたい場合は、nuxt.config.js

// nuxt.config.js
export default {
  middleware: 'authenticated'
}

やってあげれば良さげ。

ちなみに想像通りですが

middrewareはnuxt.config.js → pages/hogehoge.vue で

実行されるのでnuxt.config.jsで書かれたmiddrewareが最優先される。

validate

middlewareで権限まわりやってあげて 今後はパラメータのバリデーションしてあげる。 booleanを返すだけでエラーページが表示されるようになる。

async validate({ params, query, store }) {
  // await の処理
  return true // params バリデーションを通過したとき
  return false // Nuxt.js がルートをレンダリングするのを中止して、エラーページを表示させる
}

特定のページ (pages/fugafuga.vue) へのリダイレクトとかできないかなーと思ったけど、特にそういう記述は見当たらなかった。

asyncData

サーバーで情報を取りに行くときに使うもの。

store(vuex)にはため込まず、そのページでapi叩いてとる。 コンポーネント を作成するために必要なものをとる。

users/:iduser 取得する際にこの user はページによって変わってくるのでvuex storeに入れず、にasyncDataで使ってあげるイメージ。

fetch

apiから取得してvuex storeにデータを貯めておきたいときに使うやつ

マスターデータとか一覧の取得のときに使うはず。

export default {
  async fetch ({ store, params }) {
    await store.dispatch('GET_STARS');
  }
}

beforeCreate

インスタンスの生成前

(TODO 使いこなせてないため、今後記載)

created

インスタンスを生成した後

(TODO 使いこなせてないため、今後記載)

beforeMount

elementへのマウントされる前

(TODO 使いこなせてないため、今後記載)

mounted

elementへのマウント後

(TODO 使いこなせてないため、今後記載)

methods

動的にstate変えたりする関数 jQueryではonclickとかに相当する。

beforeUpdate

input等のデータが更新される前

(TODO 使いこなせてないため、今後記載)

updated

input等のデータが更新された後

(TODO 使いこなせてないため、今後記載)

beforeDestroy

インスタンスが削除される前

(TODO 使いこなせてないため、今後記載)

destoryed

インスタンスが削除された後

SSRでの注意事項

plugin, beforeCreate, computed, createdはサーバーサイドでもクライアントサイドでも呼び出されるので注意が必要。

process.server 等で分岐して記述しなければ2回SSR時に必ず呼び出されてしまう。

CSRのライフサイクル

上記でも書きましたが、nuxtページ内遷移。

<nuxt-link></nuxt-link> 等で遷移したときに動くライフサイクルは下記の通り

CSRのライフサイクルは下記の通り

------ サーバーサイド ------
------ クライアントサイド ------
→ middleware
→ validate
→ asyncData
→ fetch
→ beforeCreate
→ computed
→ created
→ beforeMount
→ mounted

各々のメソッドについては上記で記載したので割愛。

CSRで記述されるメソッドに関してはSSRでもCSRでも動いてしまうため process.server で明示的に書いてあげた方が予期せぬバグは避けされそう。

pluginも呼び出されているのかと思ったんですが、SSRでサーバーサイドとクライアントサイドで呼び出すのみでCSR時には実行されてない。

SSR, CSRのライフサイクルまとめ

plugin, beforeCreate, computed, createdはSSR時に2回呼び出される

middleware, validate, asyncData, fetch, beforeCreate, computed, created, beforeMount, mountedはクライアントサイドでも呼び出される。

クライアントサイドで初期に実行したい場合

SSRで初期に実行する際には、 nuxtServerInit を使えば良いんですが、クライアントで初期に実行したいとなるとデフォルトでは関数がないようです。 SPAでの開発となると nuxtServerInit が使えない。

その場合には、下記のようなライブラリも利用するのがありかなと思います。

https://github.com/potato4d/nuxt-client-init-module

ただこれまで記載したライフサイクルをみる限り、pluginもクライアントサイドで初期に実行されているので modeをclientにしてあげれば良いのかなと。

// plugins/nuxt-client-init.js
export default async context => {
  return await context.store.dispatch("nuxtClientInit", context);
};
// nuxt.config.js
module.exports = {
  plugins: [
    { src: '~/plugins/nuxt-client-init.js', mode: 'client' }
  ]
}
// store/index.js
export default {
  nuxtClientInit({ commit, state }, { app }) {
    // hogehoge
  }
}

一旦のまとめはここまで。

// TODO コンポーネントを利用時のコンポーネント含むライフサイクル等も別途まとめてみる。

何か間違い等ありましたらTwitterまでご連絡くれると幸いです。