SSR モードの Nuxt.js を Firebase に Deploy する Tutorial

## 書いてある事

大きく3点に分けて説明します。

* Firebase で SSR する為に必要な最低限の知識
* Nuxt.js を Express のミドルウェアとして動作させる方法
* Nuxt.js を Firebase にデプロイする時の注意点

なお、SSR ではなく SPA で十分な人だったり Firebase Functions 使わない人には合わない記事かもしれません。

## Firebase で SSR する為に必要な最低限の知識

まずは Firebase functions のプロジェクトを用意します。

```
% mkdir ~/nuxt-ssr-firebase && cd $_
% firebase init functions
```

以下の設問が出てくるので各自でFirebase のプロジェクトを選択、もしくは作成してください

```
? Select a default Firebase project for this directory: 
```

プロジェクトを選ぶと以下の質問に回答します。とりあえず何の事を聞かれているのかまだ分からない状態の人は以下を真似してして下さい。

```
# A functions directory will be created in your project with a Node.js
# package pre-configured. Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions? JavaScript
? Do you want to use ESLint to catch probable bugs and enforce style? (y/N) No
? Do you want to install dependencies with npm now? (Y/n) Yes
```

現時点ではディレクトリ構成は以下のようになっていると思います。`package.json` と `node_moduels` が `~/nuxt-ssr-firebase/functions` に配置されています。

```
% tree -a -L 2 -I 'node_modules' -I '.git'                                            .
├── .firebaserc
├── .gitignore
├── firebase.json
└── functions
    ├── index.js
    ├── node_modules
    ├── package-lock.json
    └── package.json
```

`functions/index.js` を修正します。

```diff
-// exports.helloWorld = functions.https.onRequest((request, response) => {
-//  response.send("Hello from Firebase!");
-// });
+exports.helloWorld = functions.https.onRequest((request, response) => {
+ response.send("Hello from Firebase!");
+});
```

`firebase serve` を使って localhost で firebase functions の動作を確認します。

```
% firebase serve --only functions:helloWorld
functions: helloWorld: http://localhost:5000/nuxt-ssr-xxxx/us-central1/helloWorld
```

確認できたら `firebase deploy` で cloud functions にデプロイして動作確認します。

```
% firebase deploy --only functions:helloWorld
```

おそらく次のようなURL でアクセスできると思います。<https://us-central1-nuxt-ssr-xxxx.cloudfunctions.net/helloWorld>

さて、`async` や `await` といった構文を使いたいと思いますので `functions/package.json` に以下の修正を加えておくとよいでしょう。

```json
 {
   "name": "functions",
   "description": "Cloud Functions for Firebase",
+  "engines": {
+    "node": "8"
+  },
   "scripts": {
     "serve": "firebase serve --only functions",
     "shell": "firebase functions:shell",
```

次に Firebase hosting をセットアップします。

```
% firebase init hosting
? What do you want to use as your public directory? public
? Configure as a single-page app (rewrite all urls to /index.html)? No
```

最初の設問で public と答えてある場合は `functions/public` というディレクトリが作成されていると思います。ちなみに `public404.html`　と `index.html` は後で消します。

```
ls public
404.html index.html
```

次に `firebase.json` に以下の様な rewrites ルールを追加して下さい。

```json
       "firebase.json",
       "**/.*",
       "**/node_modules/**"
+    ],
+    "rewrites": [
+      {   
+        "source": "**",
+        "function": "helloWorld"
+      }   
     ]
   }
 }
```

以下のコマンドを実行して動作確認します。

````
% firebase serve --only functions:helloWorld,hosting
hosting: Local server: http://localhost:5000
functions: helloWorld: http://localhost:5001/nuxt-ssr-xxxxx/us-central1/helloWorld
```

この状態で以下の URL にアクセスして下さい。**public に一致する静的アセットが存在していない場合は Firebase functions に rewrite される**事を確認して下さい。

```
http://localhost:5000/
http://localhost:5000/404.html
http://localhost:5000/hoge
```

ホスティングの優先順位については次の URL で確認して下さい<https://firebase.google.com/docs/hosting/url-redirects-rewrites#section-priorities>

## Nuxt.js を Express のミドルウェアとして動作させる

Nuxt.js アプリケーションをスクラッチから作ります。

```
cd ~/nuxt-ssr-firebase/functions
yarn add nuxt
mkdir -p nuxt-app/pages
touch nuxt-app/pages/index.vue nuxt.config.js
```

create `nuxt.config.js`

```javascript
module.exports = { 
  srcDir: 'nuxt-app',
  buildDir: 'nuxt-dist',
  build: {
    publicPath: '/assets/',
  }
}
```

create `nuxt-app/pages/index.vue`

```vue
<template>
  <h1>Hello world!</h1>
</template>
```

この状態で `yarn run nuxt` を実行して動作確認をして下さい。

## Express のミドルウェアとして動作させる

次に `functions/nuxt-server.js` を作成します。

```javascript
const { Nuxt, Builder } = require('nuxt')
const app = require('express')()

const port = process.env.PORT || 3000

const config = require('./nuxt.config.js')
config.dev = process.env.NODE_ENV === 'development'
const nuxt = new Nuxt(config)

app.get('/api/ping', (req, res) => {
  res.json({ ping: 'pong' }); 
});

app.use(async (req, res) => {
  await nuxt.ready()
  nuxt.render(req, res)
})

// Build only in dev mode with hot-reloading
if (config.dev) {
  new Builder(nuxt).build()
    .then(listen)
    .catch((error) => {
      console.error(error)
      process.exit(1)
    })
}

function listen () {
  console.log('=== listen ===')
  // Listen the server
  app.listen(port, '0.0.0.0')
  console.log('Server listening on `localhost:' + port + '`.')
}

module.exports = app
```

`NODE_ENV=development node nuxt-server.js` を実行した場合は nuxt.config.js で指定した `srcDir` を Hot Reloading します。`node nuxt-server.js` を実行する場合は最初に `yarn nuxt build`　を実行してから nuxt.config.js で指定した `buildDir` にファイルを生成して置く必要があります。

次に `functions/index.js` を編集します。

```javascript
 const functions = require('firebase-functions');
+const nuxtServer = require('./nuxt-server');
 
+
+exports.nuxtServer = functions.https.onRequest(nuxtServer)
```

`yarn nuxt build` を実行した後で `firebase serve --only functions:nuxtServer` を実行して動作確認をします。

## Nuxt.js を Firebase にデプロイする時の注意点

ところで以下のファイルは Firebase Hosting から配信すべきファイルですが、今のままだとFirebase Functions が動的に配信を行っています。

```
% tree nuxt-dist/dist/client/
nuxt-dist/dist/client/
├── 3ea0a886292a84605c64.js
├── 70744f471384cdfbb566.js
├── 7d3682722017aec6af25.js
├── LICENSES
└── afc01472d20742d8c1ac.js
```

なのでこちらのファイルは `public/assets` に移動します。以下のコマンドを実行してください。

```
rm -fr ../public/*
cp -R nuxt-dist/dist/client/ ../public/assets
```

次に `firebase.json` を以下の様に修正します。

```diff
     "rewrites": [
       {   
         "source": "**",
-        "function": "helloWorld"
+        "function": "nuxtServer"
       }   
     ]
```

この状態で `firebase serve --only functions:nuxtServer,hosting` を実行して動作確認をして下さい。ちなみに public/assets のファイルを Hosting できている場合は console に以下の様なメッセージが表示されます。

```
127.0.0.1 - - [25/Dec/2018:02:50:39 +0000] "GET /assets/3ea0a886292a84605c64.js HTTP/1.1" 200 143472 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"
```

もし functions が動的に返している場合は以下の様に表示されます。

```
[hosting] Rewriting /assets/3ea0a886292a84605c64.js to local function nuxtServer
info: User function triggered, starting execution
info: Execution took 17 ms, user function completed successfully
127.0.0.1 - - [25/Dec/2018:02:51:24 +0000] "GET /assets/3ea0a886292a84605c64.js HTTP/1.1" 200 - "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"
```

こちらのコマンドは `functions/package.json` にまとめて記述した方が良いでしょう。

```diff
-    "logs": "firebase functions:log"
+    "logs": "firebase functions:log",
+    "prebuild": "rm -fr ../public/*",
+    "build": "nuxt build",
+    "postbuild": "cp -R nuxt-dist/dist/client/ ../public/assets"
```

Windows User の方などは cpx, rimraf などを適宜活用頂ければと思います。

最後に `firebase deploy --only functions:nuxtServer,hosting` を実行してデプロイが出来ていることを確認して下さい。

## まとめ

というわけで上記の構成でプロジェクトを作成した場合は以下の様に作業する事ができます。

* 開発時: `NODE_ENV=development node nuxt-server.js`
* デプロイ前の動作確認: `firebase serve --only functions:nuxtServer,hosting`
* デプロイ: `firebase deploy --only functions:nuxtServer,hosting`

実際に業務で開発を行う場合はデプロイ先を `staging`, `production` と分けた方が良いと思いますし、Firebase と Nuxt.js のディレクトリ構成などはまだ検討の余地があると思いますが、その辺りについては後世のエンジニアがきっと素晴らしい記事を書いてくれると思います。