はじめに
先日、無料で簡単に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が出てきます。かっこいいですね🔥
その後いくつか質問が出てきますが、今回はプロジェクトの指定以外は全部Enter連打で問題ないです。
続いてFunctionsも立ち上げます。
firebase init functions
これで下地は整いました。簡単ですね。
それでは次にサーバー側で動的にコンテンツを生成する方法をご紹介します。
動的なコンテンツ生成
Expressを用いてリクエストを処理します。
まずはfunctions
ディレクトリに行ってExpressをインストールしましょう。
cd functions
npm install --save express もしくは yarn add express
それから 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も編集します
{
"hosting": {
"public": "public",
"rewrites": [{
"source": "/timestamp",
"function": "app"
}],
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}
}
変えたところは rewrites
のところです。
/timestamp
のルートが指定された時に app
関数を実行するようにしています。
ちなみに「とりあえず全てのリクエストを app
に回したい!」という場合には "source": "**"
と指定すればよいです。
とりあえずの準備は終わりました。
それではまずはローカルで動かしてみましょう!
firebase serve --only functions,hosting
他の何かに使っていなければポート5000にサーバーが立てられるのでブラウザでアクセスしてみましょう。
こんな感じに今のタイムスタンプが表示されるはずです。
おめでとうございます🎉 それでは次に処理結果をキャッシュする方法について見て行きます
…の前になぜExpressを使う必要があるのか気になる人がいるかもしれないので、それについての説明を書いて見ました。
興味なかったら次の項にお進みください。
コラム: なぜExpressを使うのか
上記の例を見て「別にexpress使わなくてもHostingでルーティング設定すればよくない?」と思ったかもしれません。それはその通りでCloud FunctionsではそもそもHTTPエンドポイントをトリガーに実行できるので無理に使う必要はないのですが、APIのような役割を持たせたい時にExpress(というかWebフレームワーク)を使うことには以下のような利点があると考えています。
メリット1: 一つのFunctionで複数のAPIを作ることができる
例えば複数の関数を登録したい場合に、Expressを使わない場合以下のように書くことになります。
exports.func1 = functions.https.onRequest((req, res) => {
//...
});
exports.func2 = functions.https.onRequest((req, res) => {
//...
});
exports.func3 = functions.https.onRequest((req, res) => {
//...
});
そして firebase.json
の方も書き換える必要があります。
"rewrites": [{
"source": "/func1",
"function": "func1"
},{
"source": "/func2",
"function": "func2"
},{
"source": "/func3",
"function": "func3"
}],
これだと管理する対象が二つになってしまい、中々辛みがあります。
そこでExpressをかますと下記のように書けます。
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) => {
//...
})
});
"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
にアクセスしましょう。
表示できたらページをリフレッシュしてみてください。値が変わらないことが確認できると思います。キャッシュ成功です!また、このキャッシュは実行したユーザのブラウザだけでなくCDNにもキャッシュされています。
そのため一人でもそのキャッシュを作るリクエストを送れば、同じCDNにアクセスするユーザたちは爆速でレスポンスを受け取ることができます!🏃!
(逆に言うと取り扱いに注意しないと色々やらかす可能性があるわけですが)
Firebase HostingのCache-Controlについてのドキュメントはこちら
HTMLテンプレートやJSONを描画する
最後にHTMLテンプレートを描画してみましょう。
今回は consolidate
というテンプレートエンジンを使います。
npm install --save consolidate handlebars もしくは yarn add consolidate handlebars
そしてfunctionsディレクトリ内に views
というディレクトリを作成して、
その中に 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>
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
に行くようにしています)
{
"hosting": {
"public": "public",
"rewrites": [{
"source": "**",
"function": "app"
}],
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}
}
また、firebaseは動的に生成するファイルより静的なファイルを優先して返すため、public
フォルダー内にある index.html
を削除します。
これで準備はできたので再び firebase serve --only functions,hosting
をしましょう!
無事にテンプレートが表示できたと思います。
また、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エンドポイント建てたい!