FirebaseにホストしていたSPA modeのNuxt.jsをSSRで稼働させるためにやったこと

Qiita初投稿です。
最近のWebフロントエンドの技術について勉強しています。
趣味プロジェクトをFirebase Hosting上にNuxt.jsがSPA modeで稼働させていたのですが、今回はFirebase Cloud FunctionsでNuxt.jsのSSR modeで動作させるようにしてみました。
前提条件は以下の通りです:

- node 8.12.0, npm 6.4.1
- Nuxt.js 2.0.0
- Firebase Hosting および Cloud Functionsを使用する
- @nuxtjs/google-analytics などいくつか依存するnpmモジュールを使用している
- Window.localStorage へのアクセスがあり、client固有の操作が必要

ソースコードとアプリはそれぞれ以下のURLにあります。

- Github: https://github.com/yysaki/shikibetsuhyo/
- アプリ: https://shikibetsuhyo.yysaki.com

SSR modeを動作させるまでに何箇所かハマりどころがあったので、備忘録を兼ねて書きます。

## まず `nuxt` コマンドがSSRモードで動くようにする

Nuxt.js をSPA modeで動かしている分にはあまり気にしないで済むのですが、SSRをするとなるとクライアントサイド、サーバサイド双方の処理で同等の結果が得られるように記述をしなければなりません。
[あーありがち - Vue.js x SSRメモ](http://aligach.net/diary/20180503.html) の記事にあるように、

> Server Side では data reactivity は不要であり、VM life cycle は以下の 2つしかない。
> 
> - beforeCreate
> - created

であるので、双方で行う処理はこの中に入れる必要があります。
今回は、localStorageからGet/Setするクライアントサイド固有の処理が必要なので、created時点でハイドレートされたものとクライアントサイドのデータを一致させるようにして、描画時に行う処理はmountedイベントに設定しました。
またイベントフックで発生するfunctionには念のためprocess.clientで判定するようにしました。
SSR特有になるライフサイクルの知識はNuxt.jsの公式ガイドを読んでもあまり手に入らないので、先の引用した記事にもあるように [Vue SSR ガイド | Vue.js サーバサイドレンダリングガイド](https://ssr.vuejs.org/ja/)　を読むと捗りました。

この準備ができてからNuxt.js をSPAからSSRに変更させておきます。

``` package.json
"dev": "nuxt",
"build": "nuxt build",
```

## デプロイ準備として `firebase serve` が動くようにする

ここではやることがいくつかあります。
まず、Cloud Functionsの最新であるnode js 8(まだβだけど)を使えるようにenginesを指定しておきます。
参考: [Firebase Functions で Node.js 8 使えるようになったぞ〜〜〜！！！](https://qiita.com/jgs/items/f8b781801f0b1646cab7)

``` functions/package.json
"engines": {
  "node": "8"
},
```

次にサーバサイドのエントリであるfunctions/index.jsを用意します。今回の場合以下のようにしました: 

``` functions/index.js
const functions = require("firebase-functions")
const { Nuxt } = require("nuxt")
const express = require("express")
const app = express()

const nuxt = new Nuxt({ buildDir: "nuxt", dev: false })

function handleRequest(req, res) {
  res.set("Cache-Control", "public, max-age=300, s-maxage=600")
  return new Promise((resolve, reject) => {
    nuxt.render(req, res, promise => {
      promise.then(resolve).catch(reject)
    })
  })
}

app.use(handleRequest)
exports.ssr = functions.https.onRequest(app)
```

全てのルーティングをNuxt.jsに任せたいのでfirebase.json のリライトルールを設定でこれを呼び出すように指定する必要があります。

``` firebase.json
    "rewrites": [{
      "source": "**",
      "function": "ssr"
    }],
```

なお、Nuxt.js の設定をSSRに変更したので、ビルド先のデフォルト値が `dist/` から `.nuxt/dist` に変更されます。
SPA時はpublicのディレクトリを `dist/` に指定していましたが生成されなくなるため、別のものにしてやる必要があります。(ここではpublic/ にします)

``` firebase.json
  "hosting": {
    "public": "public",
```

また functions/index.jsの通りbuildDirを参照できるよう ビルド先を設定します。
参考: [API: buildDir プロパティ - Nuxt.js](https://ja.nuxtjs.org/api/configuration-builddir/)

``` nuxt.config.js
module.exports = {
  buildDir: "functions/nuxt",
```

これで準備が整いました。 `put push origin master` してTravis CIにデプロイさせます。

※2019/03/10 追記:
[GithubにpushしたらFirebaseにデプロイされるようにする - Qiita](https://qiita.com/yysaki/items/adc586426f7118f517ec) にTravis CIの設定について記事を書きました。

## 1度目のデプロイ、コケる
[ログが見ると、functions/index.jsでrequireしているnuxtがみつからないと言われ失敗します。](https://travis-ci.org/yysaki/shikibetsuhyo/builds/444041105)
Cloud Functionsでは functions/package.json にある依存関係を見るため失敗してしまいます。
functions/ディレクトリを `npm install --save nuxt@2.0.0` を実行して再度デプロイします。

## 2度目のデプロイ、コケる
[今度はデプロイに成功しましたが、サーバにアクセスすると500エラーとなってしまいました。](https://travis-ci.org/yysaki/shikibetsuhyo/builds/444044060)
少し悩みましたが、これは1度目のデプロイ時の似た話でNuxt.jsが動作する時の依存モジュールをfunctions/package.jsonに設定していないのが現任でした。
参考 [Nuxt.jsとFirebaseによるSPA × SSR × PWAでnode_modulesパッケージを追加する際の注意点](https://qiita.com/sugitata/items/be357fcaab744fd02a4f)
同様に./package.jsonで依存しているものを一通りインストールしておきます。

## 3度目のデプロイ、今度は成功

[これでgit pushしたところ、ようやく想定通り動くアプリがデプロイできました。](https://travis-ci.org/yysaki/shikibetsuhyo/builds/444049419)

アプリURLを開いてChromeの「ページの表示を開く」を見ると、しっかりレンダリングされたHTMLが表示されています。
またサーバサイドで"Cache-Control"レスポンスヘッダを設定しているので、画像の様に何度かアクセスするとCDNにキャッシュされていることが観測できます。
参考: [Cloud Functions による動的コンテンツの配信  |  Firebase](https://firebase.google.com/docs/hosting/functions?hl=ja)

![スクリーンショット 2018-10-21 12.28.59.png](https://qiita-image-store.s3.amazonaws.com/0/50400/14c97ad6-7219-a48f-377e-c6eac3479680.png)

## 感想
以上のように、Firebaseを使ってNuxt.jsのSSRを稼働させることができました。
SPA->SSRに変更するため頭を悩まされることが多かったですが、Vue.jsおよびNuxt.jsやFirebase自体の理解が浅かったのが原因ですので作業を進めるにつれ勉強になりました。
デプロイしてからのホスティングサーバ自体は簡易ですし、ありがたいことに無料枠の範囲内でここまで遊べているので、趣味のぽっと出の開発でも参入がしやすくなったなと思いました。

以上です。

## その他参考URL
- [nuxt.js + firebase (cloud functions) で最小構成SSR | 丸ノ内テックブログ](https://marunouchi-tech.i-studio.co.jp/5237/)
- [実際Nuxt.jsとExpressでどうやってSSRやるか理解してましたか？自分はよくわかってなかったです【解説】](https://qiita.com/takato0903/items/35171e70ed49d225fadf)
- [Nuxt.jsとFirebaseを組み合わせて爆速でWebアプリケーションを構築する](https://qiita.com/potato4d/items/cfddeb8732fec63cb29c)