Nuxt.jsのSSR/CSR処理について

はじめに

Nuxt.js の SSR/CSR の処理がどう動いているか(ライフサイクル)、また、安全な処理を書くにはどうしたらいいのか、いまいち分かっていない方や曖昧な方も多いと思います。 今回の記事では、Nuxt.js での開発における SSR/CSR 処理とセキュアなデータの取り扱いについて少し書きます。

ライフサイクルを理解し、Nuxt.js でどのように情報を扱うべきか検討しやすくなります。 AWS や GCP、Firebase、Azure など、クラウドサービスと連携するにあたりセキュアな情報の取り扱いで悩むことが出てくると思います。 AWS でいうと AppSync や API Gateway、S3 などの各種サービスに接続するための認証情報をどのようにフロントエンドで安全に扱うか、という点は気になるポイントだと思います。

この記事は直接ある特定のサービスへの接続方法のソリューションを記載した記事ではありませんが、概念的に Nuxt.js のライフサイクルと、そこに絡めて極力セキュアにデータを扱うための設計の勘所が伝われば幸いです。

SSR、CSR とは

SSR

Server Side Rendering(サーバーサイドレンダリング)の略。 SSR とは簡単に書くとサーバー側で DB などの処理を行い、HTML の DOM を構築してブラウザに渡すことです。 よく WEB サイトで使われる PHP ではサーバー側で PHP 処理を行い HTML を構築し、ブラウザに渡します。<p><?php echo 'ssr'; ?></p>といった書き方をするとサーバー側で<p>ssr</p>に置き換えてブラウザに渡るイメージです。 また、SEO のために SSR にするという記事を見かけることが多いと思います。メリットとしては SEO 観点もあるでしょうし、クライアント側で全ての DOM を構築すると JS のダウンロード →DOM 構築と、クライアント側での処理時間が長くなる傾向にあるため、結果として初期描画が遅くなることがあります。そのため、UX の観点においても SSR は一定の効果があると思われます。

※現在はGoogle BotはJavaScriptの解釈ができるためSSRが必須というわけではないようです。SEOのためにSSRが必須という安直な考えは避けたほうが懸命かもしれません。 作成するWEBアプリケーションはなぜSSRではければならないのか、設計段階で考慮しておくことが大切になるでしょう。 SEO目的ということであれば、以下のプリレンダリングも参照してみるといいと思います。 https://ssr.vuejs.org/guide/scaling-up/ssr.html#ssr-vs-プリレンダリング-事前描画

CSR

Client Side Rendering(クライアントサイドレンダリング)の略。 先程の SSR とは逆で、クライアント側(ブラウザ)で動作する部分のことを指します。 例として通常の WEB サイトで以下のような記述を行う場合は CSR ということになります。

<p id="mode"></p>
<script>
  var target = document.getElementById('mode')
  if (target != null) target.innerHTML = 'csr'
</script>

nuxt.js で言うと、mounted()は CSR 処理です(created()は SSR でも呼ばれます)。 documentwindowは CSR でのみ使えますので、これらを使うライブラリがcreated()で呼ばれるとエラーになります。createdで使う場合は SSR か CSR どちらで動作させるか明確にしましょう。


created() { // or mounted()
  if (process.client) {
    const target = document.getElementById('mode')
    if (target != null) target.innerHTML = 'csr'
  }
}

※Vue.js、Nuxt.js においては上記のような DOM 操作は殆どすることはありませんが、今回は例ということで従来の JS 相当の処理にて記述しています。

Nuxt.js の SSR(universal)モードの注意点

まずはじめに、Nuxt.js は SSR と CSR の境界が曖昧で、明確に分けて設計・実装することが慣れるまでは意外と難しかったりします。例えば created()は SSR でも CSR でもどちらでも動きますので、created で初期化処理をする際、初回アクセス時は 2 回同じ処理をしていることはご存知ない方もいらっしゃるかもしれません。

ここでは初回アクセスとリロード時のライフサイクル、内部ナビゲーションでの遷移時のライフサイクルについて説明します。

Nuxt.js のライフサイクル

初回アクセス、リロード時

初回アクセスやリロード時には SSR 処理と CSR 処理がどちらも動作します。 plugins と created(beforeCreate)が 2 回走る点に注意です。

認証系は middleware や plugins に記述することが多いかもしれませんが、middleware の場合は内部ナビゲーション遷移時は CSR 側でしか呼ばれないため、どちらの処理も書いておく必要があります。 plugins の場合は、内部ナビゲーション遷移時は呼ばれないので注意が必要です。

if (process.server) {
  // SSRでの認証
} else {
  // CSRでの認証処理
}

以下に処理順序を書きますが、一部 beforeEach など除外しているものもあります。

処理順序(上から順に処理されます)

ここから SSR

  • nuxtServerInit  (SSR)
  • plugins  (SSR)
  • middleware  (SSR)
  • asyncData  (SSR)
  • fetch  (SSR)
  • beforeCreate  (SSR)
  • created  (SSR) ここから CSR
  • plugins  (CSR)
  • beforeCreate  (CSR)
  • created  (CSR)
  • beforeMount  (CSR)
  • mounted  (CSR)

内部ナビゲーション時

上記以外の画面遷移時には、CSR の処理のみが走ります。 plugins は動かないため、どのページに遷移しても共通の処理を行いたい場合は middleware などを検討しましょう。

処理順序(上から順に処理されます)

  • middleware  (CSR)
  • asyncData  (CSR)
  • fetch  (CSR)
  • beforeCreate  (CSR)
  • created  (CSR)
  • beforeMount  (CSR)
  • mounted  (CSR)

plugins と middleware

plugins と middleware に関しては、後述の mode 設定しない限りは SSR/CSR どちらでも処理が走る可能性があります。そのため、書いたコードが SSR、CSR どちらで動いて欲しいのかを考えなければ意図しない動作を引き起こしかねません。例えばルーティングの制御(例えば認証状態の監視など)をする際は SSR/CSR 双方で動作するべきと考えます。

ただし、サーバーサイドで実施すべき処理、例として挙げると DB からのデータ取得やセキュアにデータをやり取りするケースにおいては極力サーバーサイドでのみ動作するように設計するべきです。 なぜか?Nuxt.js を始めクライアントサイドでセキュアな情報を扱うのは簡単ではありません。クライアントサイドにも認証に必要な情報を持っておく必要があるためです(例えば API Secret key を内部に保持しておく必要があるなど)。そのため、セキュアな情報は極力サーバーサイドでのみ管理しておくのが安心です。 どうしてもの場合にはクライアントサイドでの取り扱い方法を検討しましょう。

環境変数について

セキュアな情報は環境変数(process.env)で管理すればいいのでは?という話もあります。 CI サービスから環境変数を設定する方法、Elastic Beanstalk から環境変数を設定する方法など、方法はいくつか考えられるでしょう。 ベタにコードにキーを載せては駄目なので外部からアプリケーションの起動時に注入しよう、と何となく認識している方も中にはいらっしゃるのではないでしょうか。

ここで、Nuxt.js の場合は、以下公式にもある通り process.env はビルド時に定義した値に置き換えられてしまいます。

ビフォー

if (process.env.test == 'testing123')

アフター

if ('testing123' == 'testing123')

このことから、例えば AWS の Elastic Beanstalk などの環境変数を取得するのに process.env.XXX という取得が使えないため注意が必要です。ビルド時に定義値に変換されるためです。process.env.XXX でアクセスしてもundefinedになるはずです。(ちなみに Elastic Beanstalk では単一 Docker 環境の場合 nuxt start の時点で環境変数がアプリケーションに渡されます。) そのため、環境変数については nuxt-env などのパッケージを導入し対応することが多いかと思います。nuxt-env を使用する際の注意点として、変数をセキュアに扱うためには Server Only としておく必要があるのは頭に入れておく必要があります。 client 側でも環境変数を有効にした場合はグローバルに環境変数が展開されるため、大事な環境変数が丸見えになってしまいます。見られても問題のない変数のみクライアントサイドで扱うようにしてください。 (server only でない環境変数は CSR でも情報を扱えるようグローバルに変数を持ち回します)

Nuxt.js で少しでもセキュアに情報を扱うためにできること(一例)

plugins と middleware の SSR/CSR 処理を明確に分ける

例えば plugins や middleware では、mode=”client”を設定していれば CSR 時のみ、mode=”server”を設定していれば SSR 時にのみ呼び出されるようになります。 作成したプラグインやミドルウェアが SSR/CSR どちらで動作して欲しいのか、設計時点で考慮しておくことが必要です。 問題発生時の影響範囲を少しでも狭めるためにも、必要最小限の実装を心がけましょう。

SSR と CSR の責務を明確に分ける

ここまでで記述した通り、Nuxt.js では記述した処理は SSR でも CSR でも処理が呼ばれ、どのタイミングで何が呼ばれているか、いまいちピンと来ないと思います。 そのため意図しない動きになることも多々あります。created 二重処理で data がおかしくなってしまった、意図せずいつの間にかセキュアな情報を CSR でも扱ってしまっていた、といったことは比較的よく起こり得ることだと思います。

簡単に WEB アプリが作れるというのが Vue.js や Nuxt.js の特徴でもありますが、プロダクトレベルに昇華させるためには当然 SSR と CSR のライフサイクルを十分に理解しておく必要があります。

「型安全」ということで TypeScript 化させることが流行っています(流行りというよりは必然なのでしょうが)。それ自体大事なことではありますが、Nuxt.js の動作原理を理解しておくこともアプリケーションを開発する上で非常に重要であると考えます。

この記事が少しでも Nuxt.js のライフサイクルの理解に繋がれば幸いです。

関連記事