JavaScript
Node.js
Firebase
FirebaseHosting
FirebaseCloudFunctions
Node.jsDay 16

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

More than 1 year has passed since last update.

はじめに

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

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

Firebase Hosting + Cloud Functionsを使うことのメリット

初めにどんなメリットがあるかを軽くご紹介します。

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

初回の 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のインストール方法は割愛します。
他の方の記事ですが、まだ準備できていない方はこちらにFirebaseのアカウントの作り方からFirebaseプロジェクトの作成まで詳しくまとまっているのでご参照ください。

Hosting と Cloud Functionsの初期化

では早速作って行きましょう!
まずは適当なディレクトリを作成します。

mkdir firebase-node
cd firebase-node

Hostingを初期化します。

firebase init hosting

と実行するとイカしたFirebaseのAAが出てきます。かっこいいですね🔥
firebase init aa.PNG

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

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

firebase init functions

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

動的なコンテンツ生成

Expressを用いてリクエストを処理します。

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

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

それから index.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も編集します

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にサーバーが立てられるのでブラウザでアクセスしてみましょう。
こんな感じに今のタイムスタンプが表示されるはずです。
hello firebase node.PNG
おめでとうございます🎉 それでは次に処理結果をキャッシュする方法について見て行きます
…の前になぜExpressを使う必要があるのか気になる人がいるかもしれないので、それについての説明を書いて見ました。
興味なかったら次の項にお進みください。

コラム: なぜExpressを使うのか

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

メリット1: 一つのFunctionで複数のAPIを作ることができる

例えば複数の関数を登録したい場合に、Expressを使わない場合以下のように書くことになります。

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 の方も書き換える必要があります。

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

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

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) => {
    //...
  })
});
firebase.json
"rewrites": [{
  "source": "/api",
  "function": "app"
}],

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

メリット2: ExpressのMiddlewareの恩恵が受けられる

便利なのをいくつか挙げると下記のような感じです。

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

処理結果をキャッシュする

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

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にアクセスしましょう。
firebase deplyed.png

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

Firebase HostingのCache-Controlについてのドキュメントはこちら

HTMLテンプレートやJSONを描画する

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

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

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

スクリーンショット 2017-12-11 23.42.08.png
できたら index.hbsindex.js を以下のようにします。

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>
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に行くようにしています)

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

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

これで準備はできたので再び firebase serve --only functions,hosting をしましょう!
template demo.PNG

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

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

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

これから

雑ですがもっと色んなもの触ってみたい人向けに何個か他の方の記事を貼ります。

SSRしたい!

GraphQLエンドポイント建てたい!