SSRで作ったNuxtアプリをFirebase hostingで動的にデプロイする。

#はじめに
何回煎じるんだって言うほどよく見る内容ですが、Firebaseを用いてデプロイする機会があったので、ある程度知識をまとめてみました。
あくまで備忘録として記すので、もしわかりにくい部分が会ったらご指摘お願いします。

また、今回の記事は[この記事](https://agelog.hateblo.jp/entry/2019/11/25/001401)を非常に参考にさせてもらいました。

#使用環境
```
MacBook (version: 10.15.4)
Nuxt　(version: 2.11.0）
firebase sparkプラン
firebase-tools (version: 8.2.0)　
```

#目標
* Nuxtで作ったアプリケーションを、動的コンテンツとしてデプロイしたい。
* さらにBasic認証もかけれれば完璧。

## 0.概観

###デプロイの流れ
<img width="949" alt="スクリーンショット 2020-05-17 19.24.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412378/304121a4-b51b-bbad-fe9a-6fec955bac36.png">



①`Firebase Hosting`にアクセスします。
②`Hosting`されると`Firebase Functions`に必ずリライトします。
③`Nuxt`アプリをレンダリングして、クライアントサイドに返します。

###ディレクトリ構造
以下が最終的なアプリのディレクトリ構造になります。
ただし、今回関係ない部分は省略しています。

```
<プロジェクト名>
├── firebase.json
├── nuxt.config.js
~
~
├── functions
│   ├── index.js
│   └── node_modules
├── plugins
│   └── firebase.js
└── public
　   └── index.html
```

##1.Nuxtアプリ作成
```
yarnの場合
yarn create nuxt-app 生成ファイル名

npmの場合
npx create-nuxt-app <project-name>
```
とりあえずnuxtアプリを生成します。
詳しくは、[こちら](https://qiita.com/sychocola1/items/6aa185960a9f4e40d3d0)をご覧ください。

##2.firebaseの初期化
今回は、`firebase CLI`などは既にインストールされている状態のものとします。
`firebase`の最初の導入に関しては[こちら](https://qiita.com/sychocola1/items/e7ca6a0bac16a9f8f3fa)をご覧ください。

まず、以下のコマンドで`firebase`を初期化します。

```
firebase init
```
そうすると、例の如く何を初期化するか聞かれるので、今回は`Functions`, `Hositng`を選択。

```
? Which Firebase CLI features do you want to set up for this folder? Press Space
 to select features, then Enter to confirm your choices. (Press <space> to selec
t, <a> to toggle all, <i> to invert selection)
❯◯ Database: Deploy Firebase Realtime Database Rules
 ◯ Firestore: Deploy rules and create indexes for Firestore
 ◯ Functions: Configure and deploy Cloud Functions
 ◯ Hosting: Configure and deploy Firebase Hosting sites
 ◯ Storage: Deploy Cloud Storage security rules
 ◯ Emulators: Set up local emulators for Firebase features
```

以降は色々聞かれるので、基本そのままEnter連打で構いません。

また`Hosting`を`init`した時に`/.firebaserc`と`/firebase.json`が生成されます。
念のため説明しておくと、`/firebaserc`はデプロイするプロジェクトを設定しておくファイルで、`/firebase.json`はデプロイ時にどのような操作をするか決定するファイルになります。
`/firebase.json`に関しては 3. で取り扱います。




##3.デプロイ時の操作の決定

 上の 2. で話した通り`/firebase.json`の中身を弄って、デプロイ時に`firebase functions`に操作をリライトできるようにします。

```/firebase.json
{
  "functions": {
    "functions": {
      "source": "functions"
    }
  },
  "hosting": {
    "public": ".nuxt/dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "ssr"
      }
    ]
  }
}
```
このように記述すると、全てのルートで後述する`ssr`関数にリライトすることができます。

##4.Functionsの依存関係の調整

```diff:/functions/package.json
{
  "name": "functions",
  "description": "Cloud Functions for Firebase",
  "scripts": {
    "serve": "firebase serve --only functions",
    "shell": "firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
+ },
+  "engines": {
+    "node": "10"
+  },
  "dependencies": {
    "firebase-admin": "^8.6.0",
    "firebase-functions": "^3.3.0",
+   "nuxt": "^2.10.2"
  },
  "devDependencies": {
    "firebase-functions-test": "^0.1.6"
  },
  "private": true
}
```
`node`のバージョンはデフォルトで、`8`なので`10`にしておきます。
また`functions`内には`nuxt`は入っていないので、

```
cd functions && npm install --save nuxt 
```
をしておきます。


##5.Functionsの内容を記述する
3.で書いたように、`Hosting`されると全てのパスにおいて`functions/index.js`にリライトされます。

```/functions/index.js
const functions = require('firebase-functions');
const { Nuxt } = require('nuxt');
const nuxt = new Nuxt({
    buildDir: 'ssr',
    dev: false
});

exports.ssr = functions.https.onRequest(async (req, res) => {
    await nuxt.ready()
    return nuxt.render(req, res)
})
```
以上で、各々のルートでレンダリングするようにかけています。
4.で記述した`ssr`はここで`exports`したものになります。
またここで指定した`buildDir`はデプロイ時に作成するディレクトリ名になります。
またその際のパスは`/functions/ssr/`となります。

##6.package.jsonでビルドコマンドを変更
今後デプロイがしやすくなるように、コマンドを調整します。


```diff:/package.json
{
  "name": "***********",
  "version": "1.0.0",
  "description": "My cat&#39;s meow Nuxt.js project",
  "author": "Shiba-You",
  "private": true,
  "scripts": {
    "dev": "nuxt",
+   "build": "nuxt build && npm run build:copy:ssr",
+   "build:copy:ssr": "rimraf functions/ssr && mkdirp functions/ssr && cp -R .nuxt/dist functions/ssr/dist",
    "start": "nuxt start",
    "generate": "nuxt generate",
    "lint": "eslint --ext .js,.vue --ignore-path .gitignore ."
  },
  "dependencies": {
    "nuxt": "^2.0.0",
    "@nuxtjs/dotenv": "^1.4.0"
  },
  "devDependencies": {
    "@nuxtjs/vuetify": "^1.0.0",
    "@nuxtjs/eslint-config": "^2.0.0",
    "@nuxtjs/eslint-module": "^1.0.0",
    "babel-eslint": "^10.0.1",
    "eslint": "^6.1.0",
    "eslint-plugin-nuxt": ">=0.4.2"
  }
}
```
以上のように変更・追加することで、`/functions/ssr/dist`に静的ファイル群をコピーすることができます。

##7.実行
現在、`/functions`にいればプロジェクトファイル直下に戻った上で、ビルドしてJSファイルをミニファイします。

```
(functionsにいれば) cd ../ 
npm run build
```

実際にデプロイする前に、プレビューしておきます。

```
firebase serve
```
特に指定しなければ以下のように5000ポートが開いていると思うので、ローカル上で正しく動作できているか確認します。

```
✔  functions: functions emulator started at http://localhost:5001
i  functions: Watching "/Users/**********/*********/functions" for Cloud Functions...
i  hosting: Serving hosting files from: dist
✔  hosting: Local server: http://localhost:5000
```

問題なければ本番サーバーにデプロイします。

```
firebase deploy
```

```
? Would you like to proceed with deletion? Selecting no will continue the rest 
of the deployments. No
i  functions: continuing with other deployments.
✔  functions[ssr(us-central1)]: Successful update operation. 
i  hosting[hall-repoter-dev]: finalizing version...
✔  hosting[hall-repoter-dev]: version finalized
i  hosting[hall-repoter-dev]: releasing new version...
✔  hosting[hall-repoter-dev]: release complete

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/***************/overview
Hosting URL: https://******************.web.app
```
`Hosting URL`に表示されているURLに自分のプロジェクトが動作していたらデプロイ成功です。

#おまけ
以下個人的に詰まった点等を紹介します。
##エラー例①： firebase serveしても期待通りに動いてくれない
<img width="1523" alt="スクリーンショット 2020-05-18 1.54.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412378/e3188c31-418b-3907-6a6a-4edb5811cb70.png">

`firebase serve`しても上のように表示されるだけで期待通りに動いてくれない場合は、`functions`内の依存関係がおかしかったり、必要な物がインストール出来ていない可能性が高いです。

なので、まずは

```
cd functions && npm install firebase
```
をして`firebase`が入っているか確かめます。
それでも治らない場合は

```
npm rm nuxt-sass-resources-loader --save-dev
npm i nuxt-sass-resources-loader
```
をしてみてください。
自分はこれで、エラーから抜け出すことができました。

##エラー例②: firestoreからデータが持ってこれない
これはまた別件になりますが、ローカルでは動いていた`firestore`が動かないことがあります。（自分がそうでした）
なので、動くようにセキュリティルールを変更します。

ちなみに、セキュリティルールは`firebase`のコンソールの「Database」→「ルール」で開くことができます。

<img width="1675" alt="スクリーンショット 2020-05-18 2.02.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412378/6bc9f9b9-f5f3-51fb-0fb8-25c6b174a85b.png">
ここを、以下のように変更します。

```
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write;
    }
  }
}
```

ただし、このルールは誰でもDbを書き換えれるルールなので、その辺は注意してください。

##Basic認証もデプロイしてみる
どうせならBasic認証もあったほうがいいので試してみました。

まずは必要なパッケージをインストールします。

```
cd functions && npm install --save basic-auth-connect express
```

また`functions/index.js`も書き直します。

```functions/index.js
const functions = require('firebase-functions');
const express = require('express');
const basicAuth = require('basic-auth-connect');
const { Nuxt } = require('nuxt');

const nuxt = new Nuxt({
  buildDir: 'ssr',
  dev: false
});

const USERNAME = '任意のユーザーID' 
const PASSWORD = '任意のパスワード'  

const app = express()

app.use(basicAuth(USERNAME, PASSWORD))

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

exports.ssr = functions.https.onRequest(app)

```

以上のように変更するだけで、Basic認証が実装できます。

#まとめ
今回はSSRをデプロイしてみましたが、自分がサーバーやらインフラやらに弱いことを痛感しました。
もう少しその辺を掘り下げて勉強してみます。

#参考
https://agelog.hateblo.jp/entry/2019/11/25/001401
https://qiita.com/sugitata/items/be357fcaab744fd02a4f
https://qiita.com/ykoizumi0903/items/55fd678d261a8c308f0f
https://qiita.com/seya/items/225f859d775b31047000