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

22

10月

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

2019年10月22日 CSR, Nuxt.js, SSR

はじめに

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目的ということであれば、以下のプリレンダリングも参照してみるといいと思います。

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のライフサイクル

file

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

初回アクセスやリロード時には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のライフサイクルの理解に繋がれば幸いです。