Firebaseで動かすNode.jsアプリ入門🔥

はじめに
------------------------------
先日、無料で簡単にNode.jsアプリを公開できるサービス「Glitch」が話題になりましたが、Firebaseを使えば同じようなことが簡単にできます🔥

本記事では何かと便利なFirebase Hosting と Cloud FunctionsでNode.jsアプリ(HTTPリクエスト受け取って静的リソースとかJSONとか返すもの)を動かす方法をご紹介します。
[こちらのFirebaseの中の人のチュートリアル動画](https://www.youtube.com/watch?v=LOeioOKUKI8)が勉強になったのでそれをベースに解説してみます。

### Firebase Hosting + Cloud Functionsを使うことのメリット
初めにどんなメリットがあるかを軽くご紹介します。

* GoogleのCDNの恩恵が自動的に受けられる
  * 後述しますが、動的コンテンツのキャッシュも簡単に作れるのでパフォーマンスの改善に利用できます
* デプロイが楽々、CLIから楽々できます💪
* ほぼ無料で使える (料金プランは[こちら](https://firebase.google.com/pricing/?hl=ja))

> 初回の 2,000,000 回の呼び出し、400,000 GB 秒、200,000 CPU 秒、5 GB のインターネット下りトラフィックが毎月無料で提供されます。

* Firebaseの他のサービスと連携しやすい。HTTPリクエストだけでなく、Realtime DBの変更をトリガーに関数実行できたりするので、表現の幅が広がります。
* SSRもできたりする
* 正直プロダクションで使ったりベンチマーク取ったりはしてないので全力でオススメはできないんですが、
個人利用でサクッとウェブアプリ作りたい、みたいな用途にはFirebaseが提供する便利ツール群にbindされていけば
爆速で実装できるので、もうとりあえずFirebase触ってみればいいと思います。

※Node.jsのAdvent CalendarなのにFirebaseが主役っぽいのは気のせいです。

### 前提
本題にフォーカスしたいため、Firebaseのプロジェクトの作成の仕方・Firebase CLIのインストール方法は割愛します。
他の方の記事ですが、まだ準備できていない方は[こちら](https://qiita.com/kohashi/items/43ea22f61ade45972881)にFirebaseのアカウントの作り方からFirebaseプロジェクトの作成まで詳しくまとまっているのでご参照ください。

Hosting と Cloud Functionsの初期化
------------------------------
では早速作って行きましょう！
まずは適当なディレクトリを作成します。

    mkdir firebase-node
    cd firebase-node
Hostingを初期化します。

    firebase init hosting

と実行するとイカしたFirebaseのAAが出てきます。かっこいいですね🔥
<img width="503" alt="firebase init aa.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/150569/90a7529c-58e8-0f76-2520-bcc0703eb24d.png">

その後いくつか質問が出てきますが、今回はプロジェクトの指定以外は全部Enter連打で問題ないです。

続いてFunctionsも立ち上げます。

    firebase init functions

これで下地は整いました。簡単ですね。
それでは次にサーバー側で動的にコンテンツを生成する方法をご紹介します。

動的なコンテンツ生成
------------------------------
Expressを用いてリクエストを処理します。

まずは`functions`ディレクトリに行ってExpressをインストールしましょう。

    cd functions
    npm install --save express もしくは yarn add express

それから `index.js` を以下のように編集します。

```js:index.js
const functions = require('firebase-functions');
const express = require('express');

const app = express();
app.get('/timestamp', (request, response) => {
  response.send(`${Date.now()}`);
})

exports.app = functions.https.onRequest(app);
```
`/timestamp` にアクセスしたユーザに現在のタイムスタンプを返しています。
また、firebase.jsonも編集します

```json:firebase.json
{
  "hosting": {
    "public": "public",
    "rewrites": [{
      "source": "/timestamp",
      "function": "app"
    }],
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
}
```

変えたところは `rewrites` のところです。
`/timestamp` のルートが指定された時に `app` 関数を実行するようにしています。
ちなみに「とりあえず全てのリクエストを `app` に回したい！」という場合には `"source": "**"` と指定すればよいです。

とりあえずの準備は終わりました。
それではまずはローカルで動かしてみましょう！

    firebase serve --only functions,hosting

他の何かに使っていなければポート5000にサーバーが立てられるのでブラウザでアクセスしてみましょう。
こんな感じに今のタイムスタンプが表示されるはずです。
<img width="403" alt="hello firebase node.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/150569/4641f915-2ab9-bb3a-d760-5d2fdcd579f0.png">
おめでとうございます🎉　それでは次に処理結果をキャッシュする方法について見て行きます
…の前になぜExpressを使う必要があるのか気になる人がいるかもしれないので、それについての説明を書いて見ました。
興味なかったら[次の項](#処理結果をキャッシュする)にお進みください。

コラム: なぜExpressを使うのか
------------------------------
上記の例を見て「別にexpress使わなくてもHostingでルーティング設定すればよくない？」と思ったかもしれません。それはその通りでCloud FunctionsではそもそもHTTPエンドポイントをトリガーに実行できるので無理に使う必要はないのですが、APIのような役割を持たせたい時にExpress（というかWebフレームワーク）を使うことには以下のような利点があると考えています。

#### メリット1:　一つのFunctionで複数のAPIを作ることができる
例えば複数の関数を登録したい場合に、Expressを使わない場合以下のように書くことになります。

```js:index.js
exports.func1 = functions.https.onRequest((req, res) => {
  //...
});

exports.func2 = functions.https.onRequest((req, res) => {
  //...
});

exports.func3 = functions.https.onRequest((req, res) => {
  //...
});
```

そして `firebase.json` の方も書き換える必要があります。

```json:firebase.json
"rewrites": [{
  "source": "/func1",
  "function": "func1"
},{
  "source": "/func2",
  "function": "func2"
},{
  "source": "/func3",
  "function": "func3"
}],
```

これだと管理する対象が二つになってしまい、中々辛みがあります。
そこでExpressをかますと下記のように書けます。

```js:index.js
exports.app = functions.https.onRequest((req, res) => {
  app.get('/api/func1', (request, response) => {
    //...
  })

  app.get('/api/func2', (request, response) => {
    //...
  })

  app.get('/api/func3', (request, response) => {
    //...
  })
});
```

```json:firebase.json
"rewrites": [{
  "source": "/api",
  "function": "app"
}],
```

これの何が嬉しいかと言うとAPIの部分のコードやルーティングは全て `index.js` 内のコードの責務とすることができ、`firebase.json` では一切気にしなくてよくなることです。
`API` という大きな役割だけポンと切り出せるメリットがあります。

#### メリット2:　ExpressのMiddlewareの恩恵が受けられる
便利なのをいくつか挙げると下記のような感じです。

* [CORSをできるようにするやつ]()
* [レスポンスを圧縮するやつ](https://github.com/expressjs/compression)
* [request body をパースしてくれるやつ](https://github.com/expressjs/body-parser)

若干雑になりましたが、以上が私が考えるExpressを使うメリットです。
もちろん状況によっては完全に不必要な場合も多々存在するので適宜ご判断ください。

処理結果をキャッシュする
------------------------------
それでは先ほどの `timestamp` で出力した結果をキャッシュするようにしてみましょう。
と言っても`Cache-Control`用のコードを１行足すだけです。

```js
app.get('/timestamp', (request, response) => {
  response.set('Cache-Control', 'public, max-age=300, s-maxage=600'); //ここを追記しました
  response.send(`${Date.now()}`);
})
```

ローカルで`serve`しても関数の結果はキャッシュされないので、実際にデプロイします。

    firebase deploy --only functions,hosting

デプロイが成功したら以下のようにCLIにURLが表示されるはずなので、そのURL/`timestamp`にアクセスしましょう。
<img width="426" alt="firebase deplyed.png" src="https://qiita-image-store.s3.amazonaws.com/0/150569/860b8e11-b6a0-7b27-c160-064380cf23b4.png">


表示できたらページをリフレッシュしてみてください。値が変わらないことが確認できると思います。キャッシュ成功です！また、このキャッシュは実行したユーザのブラウザだけでなくCDNにもキャッシュされています。
そのため一人でもそのキャッシュを作るリクエストを送れば、同じCDNにアクセスするユーザたちは爆速でレスポンスを受け取ることができます！🏃！
（逆に言うと取り扱いに注意しないと色々やらかす可能性があるわけですが）

Firebase HostingのCache-Controlについてのドキュメントは[こちら](https://firebase.google.com/docs/hosting/functions?hl=ja#managing_cache_behavior)

HTMLテンプレートやJSONを描画する
------------------------------
最後にHTMLテンプレートを描画してみましょう。
今回は `consolidate` というテンプレートエンジンを使います。

    npm install --save consolidate handlebars もしくは yarn add consolidate handlebars

そしてfunctionsディレクトリ内に `views` というディレクトリを作成して、
その中に `index.hbs` というファイルを作成してください。こんな感じのフォルダ構成になります↓

<img width="162" alt="スクリーンショット 2017-12-11 23.42.08.png" src="https://qiita-image-store.s3.amazonaws.com/0/150569/fe9619c6-d769-7890-32b0-40d2cca82a00.png">
できたら `index.hbs` と `index.js` を以下のようにします。

```html:index.hbs
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Firebase Hostingでテンプレート描画するよ</title>
  </head>
  <body>
    <ul>
      {{#each facts}}
      <li>{{text}}</li>
      {{/each}}
    </ul>
  </body>
</html>
```

```js:index.js
const functions = require('firebase-functions');
const express = require('express');
const engines = require('consolidate');

const app = express();
app.engine('hbs', engines.handlebars);
app.set('views', './views');
app.set('view engine', 'hbs');

const facts = [
  {text: "1+1 = 2"},
  {text: "真実はいつも一つ"},
  {text: "工藤新一はコナン"}
];

app.get('/', (request, response) => {
  response.set('Cache-Control', 'public, max-age=300, s-maxage=600');
  response.render('index', { facts });
})

exports.app = functions.https.onRequest(app);
```
もはや返しているのが `timestamp` でないので `firebase.json` も書き換えます。(とりあえず全部`app`に行くようにしています)

```json:firebase.json
{
  "hosting": {
    "public": "public",
    "rewrites": [{
      "source": "**",
      "function": "app"
    }],
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
}
```

また、firebaseは動的に生成するファイルより静的なファイルを優先して返すため、`public` フォルダー内にある `index.html` を削除します。

これで準備はできたので再び `firebase serve --only functions,hosting` をしましょう！
<img width="213" alt="template demo.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/150569/ad1a95d7-fdfd-05fc-bac5-cc3fade534fe.png">

無事にテンプレートが表示できたと思います。
また、JSONを返したい場合も楽々で単純に `response.json()` してあげるだけです。

```js
app.get('/json', (request, response) => {
  response.set('Cache-Control', 'public, max-age=300, s-maxage=600');
  response.json(facts);
})
```

これでWebアプリっぽいことは一通りできるようになりましたね🎊
よければこれを機に色々触ってみてください！


これから
------------------------------
雑ですがもっと色んなもの触ってみたい人向けに何個か他の方の記事を貼ります。
###SSRしたい！
* [Next.js を Firebase hosting で動かしてSSRする](https://qiita.com/mizchi/items/60722563b8a938e7336f)
* [Vue.js サーバサイドレンダリング on Cloud Functions for Firebase](https://qiita.com/koki_cheese/items/64f429ac764983525dcc)

###GraphQLエンドポイント建てたい！
* [Firebase Functions 上に GraphQL サーバーを実装する](https://qiita.com/mizchi/items/8036b20d814921625a76)