酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

2020/05/07 Firebase Hosting + Cloud Function + Express + Firestore を使ってみた

GWが終わってから、なにがとは決めていないけれど Webサービス作るか…… という気持ちがふつふつと沸いてきた。

Node.js はやりたいがサーバーのお守りはやりたくない

なにかしらのWebアプリを作ってみたいと以前から考えていましたが、実行するサーバーなどインフラのメンテナンスのことを考えると億劫で手が出ませんでした。

AWS EC2でサーバー立てて forever.js でNode.jsをデーモン化したりするのは避けたいところです……と考えていたのが2年くらい前のこと。いままで何度かNode.jsに手を出そうとして、まとまった時間が取れなかったりして挫折していました。一番のネックとなっていたのは、やはり書いたプログラムの実行環境が用意できない(あるいは用意するのが面倒くさい)ところでした。

この酢ろぐが pikiwikiからWordPress、WordPressからはてなブログ、と引っ越してきたのもサーバーのお世話をしたくないからです。知り合いから「ブログ落ちてるよー」「再起動したよー」というやりとりはもうしたくないのです。

Firebase Hosting と Cloud Functions でインフラのことを考えなくてよくなった

つい先日 Firebase Hosting と Cloud Functions を使って、動的コンテンツの配信ができることをたまたま知りました。この方法がうまくいけば、前述したインフラ関係を気にすることなく firebase deploy だけで書いたアプリをデプロイすることが可能です。

このドキュメントは TypeScript に対応したものではなかったので、試行錯誤しながら組み立てていくことになりました。

import * as functions from 'firebase-functions';
import * as express from 'express';

const app = express();

app.get('/', (req, res) => {
    const date = new Date();
    const hours = (date.getHours() % 12) + 1;  // London is UTC + 1hr;
    res.send(`
      <!doctype html>
      <head>
      <title>Time</title>
      <link rel="stylesheet" href="/style.css">
      <script src="/script.js"></script>
      </head>
      <body>
      <p>In London, the clock strikes:
      <span id="bongs">${'BONG '.repeat(hours)}</span></p>
      <button onClick="refresh(this)">Refresh</button>
      </body>
    </html>`);
});

app.get('/api', (req, res) => {
    const date = new Date();
    const hours = (date.getHours() % 12) + 1;  // London is UTC + 1hr;
    res.json({bongs: 'BONG '.repeat(hours)});
});

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

上記の index.ts をデプロイして、サンプルが実行されたときにはオォーとなりました。

f:id:ch3cooh393:20200502214008p:plain

さらにexpress-generator を使ってアプリの雛形(スケルトン)を生成する方法が Qiitaで 紹介されていました。

最初は文章ばかりで何を指しているのかわかりませんでしたが、前述の公式ドキュメントを一通り試してから読むと理解できるようになっていました。Expressの雛形を functions にデプロイしてみました。

f:id:ch3cooh393:20200503163420p:plain

うまくいってる!!!

以上で、 Firebase Hosting と Cloud Functions を基盤にして、Express.jsを使ったWebアプリの開発が可能な状態となりました。

Express.jsを使って Webアプリを作りたい

Node.js開発では Express を使うのが当たり前なのか、「Express.jsを使ってWebアプリを作ろう!」的な書籍を発見することができませんでした。比較的、最近執筆された「JavaScriptでのWeb開発 ~ Node.js + Express + MongoDB + ReactでWebアプリを開発しよう 〜 その1 〜(改訂版三版)」を購入して、読み進めることにしました。

Express単体では HTTPリクエストを受けてレスポンスを返す機能しかなく、ミドルウェアを追加して使うことがわかりました。

const app = express();

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

レンダリングエンジンには何を使うべきか?

pug(jade) を使っておけば安牌? htmlエディタでもインテリセンスの効く ejs の方がよいかのか?

とりあえず pug を使うことにした。

ExpressとFirestoreを連携させる

本書では MongoDB を使っていましたが、前述の通りインフラに触れたくないと考えています。Cloud Functionを選択した理由と同じで、ここではFirestoreを使うことにする。MongoDBの記述となんとなくで Firestoreに読み替えて実装していきました。

過去に調べていた「Cloud Functions for FirebaseでFirestoreのデータを返す - 酢ろぐ!」を参考にして、firebase-admin を使うことになるので、firebase-adminsdk.json をダウンロードしてきました。

export GOOGLE_APPLICATION_CREDENTIALS="/Users/ch3cooh/firebase/***/firebase-adminsdk.json"
firebase serve --only functions,hosting

メッセージ投稿画面( messages.pugmessages.js )は雑にこんな感じです。

doctype html
html(lang="ja")
    head
        meta(charset="utf8")
    body
        h1 メッセージ
        h2 メッセージを保存
        form(action="messages" method="POST")
            input(type="text" name="username" placeholder="名前")
            textarea(name="message" placeholder="メッセージ")
            button(type="submit") 送信

        h2 メッセージ一覧
        section
            each msg in messages
                div
                    h3 #{msg.name}
                    p #{msg.message}
const firebaseAdmin = require('firebase-admin');

var express = require('express');
var router = express.Router();

router.get('/', function(req, res, next) {
    const db = firebaseAdmin.firestore();
    const messages = db.collection('messages');

    messages.get()
    .then(snapshot => {
        let list = []; 
        snapshot.forEach(doc => list.push(doc.data()));
        res.render('messages', { messages: list });
    })
});

router.post('/', function(req, res, next) {
    const db = firebaseAdmin.firestore();
    const messages = db.collection('messages');

    messages.add({
        name: req.body.username,
        message: req.body.message,
    })
    .then(ref => {
        res.redirect('/messages')
      });
});

module.exports = router;

実行する。

f:id:ch3cooh393:20200507180353p:plainf:id:ch3cooh393:20200507180357p:plain
Firestoreへのメッセージの追加/メッセージの削除

読んでいてよくイメージができなかったところ

本書はターゲットが Webアプリ開発者なのか Rubyを引き合いに出すことがあり、RoRをよく知らない僕としては「RubyのSinatra」「Ruby on RailsのようなDBのモデル設定」とはどんなものなのか?とちょくちょく調べ物が発生しました。

RubyのSinatraに似たRESTful設計になっており、Node.jsの薄いラッパーとしてシンプルな構成になっています。

Express自体は、WebサーバーとしてHTTPリクエストを取り扱う最低限の機能しかありません。

Ruby on RailsのようなDBのモデル設定や認証機能などは一切備わっておらず、... (略)

Nakano Hitoshi. JavaScript (Japanese Edition) (Kindle の位置No.498-499). Kindle 版.