Firebaseのホスティングでバックエンドとフロントエンドの同時実装

記事
IT・テクノロジー

Firebaseのホスティングでバックエンドとフロントエンドの同時実装

Firebaseを利用したWebサービスやWebアプリを開発する場合に、フロントエンドとバックエンドのサービスの両方が必要になる場合はよくあります。
オンライン決済などは一つの例です。殆どの処理をフロントエンドで行うことができますが、一部の処理は外部サービスの秘密鍵(secret key)などが必要なことからバックエンドでの処理が要求される場合があるからです。
あるいは、セキュリティルールをシンプルにするために、UI(ユーザーインターフェース)はフロントエンドで実装して実際の処理はバックエンドで行う場合などです。
実際の開発ではフロントエンドとバックエンドで別々のプロジェクト(フォルダ)で開発する場合が多いですが、小規模な開発を個人で行うような場合は、同じプロジェクト(フォルダ)にまとめてしまった方が便利な場合もあります。
この記事では、一つのプロジェクトフォルダの中にフロントエンドとバックエンドを一緒に入れて実装する方法を紹介します。

Firebaseのファンクション自体が両方同時の実装を想定してます!

Firebaseでバックエンドを実装する場合の基本的な手順は
1.Firebaseのプロジェクトフォルダの作成
2.Firebaseのプロジェクトフォルダに移動
3.「firebase-tools」と「firebase-admin」のインストール
4.Firebaseにログイン(firebase loginの実行)
5.Firebaseフォルダの初期設定(firebase initの実行)
この一連のステップはnpmのコマンドラインから行います。
$ mkdir test-project
$ cd test-project
$ npm install -g firebase-tools
$ npm install --save firebase-admin
$ firebase login
$ firebase init
という感じで実行します。firebase initでホスティング(hosting)とファンクション(functions)を実行すると、プロジェクトフォルダには2つのフォルダが作成されます。
「public」と「functions」です。この設定はfirebase.jsonの設定を変更すれば、変えることができます。しかし、基本は以下のように用途が明確に分けられています。
* public - フロントエンド関連のファイル、HTML/CSS/Javascriptなどのファイル
* functions - バックエンド関連のファイル
基本的に、ReactやVueなどで作られる公開用のファイルはこの「public」フォルダの下もしくは、firebase.jsonを書き換えて「build」や「dist」の下に置きます。「functions」の下にはバックエンドの処理を行うファイルを置くように割り当てられています。
つまり、この基本的なルールを守れば、フロントエンドの処理とバックエンドの処理を同じFirebaseのフォルダにおいて一度に公開(デプロイ)する事ができます。

同時に実装する場合の問題

多くの実装がフロントエンドとバックエンドの実装を分けることが多いのには理由があります。
一番の問題は「ルーティング」の問題です。
ルーティングとは?
これは、WebブラウザのURLで指定された「場所」をブラウザに届ける処理の事です。
WebブラウザがWebサイトのホスティングをしているサーバーに「必要な情報を要求」して、ホスティングしているサーバーが「要求された情報を提供」するというやり取りの事です。
これだけを見ると余り難しくなさそうですよね?
実際は、表示に必要な情報は全てWebサイトのホスティングをしているサーバーにおいてあります。 WebブラウザはURLを指定することで必要な情報を受け取って表示をしています。
したがって、このルーティング処理を行っているのは基本的にWebホスティングをしているサーバーが行っています。
Webサーバーは要求された情報を提供して、要求された情報がない場合はエラー(404エラー)をWebブラウザーに返します。
ReactやVueを使うと事情が変わります!
ところが、ReactやVueのフレームワークを使うと少し事情が変わってきます。 シンプルに、ReactやVueのアプリの中で処理している場合は余り大きな問題ではありません。CLIを使ってフロントエンドのアプリを実装する場合は、「index.html」というファイルを読み込んでいます。(多くのWebホストのサーバーは、URLのフォルダのみの指定の場合には、「index.html」を提供するように設定されています。)
ReactやVueでアプリを作って、サイトのフォルダに置いた場合は、例えば「sv-sw.com」と指定するのと、「sv-sw.com/index.html」と指定するのは同じ「場所」を指定しています。
ところが、ReactやVueの拡張機能として、よく利用される「React Router (react-router-dom)」や、「VUE ROUTER」という機能があります。これを使う場合に問題が起こります。
この2つの拡張機能は、本来はWebサーバーからもらうはずの情報を実は、ReactやVueのアプリが持っていて、Webサーバーから情報をもらわなくても表示ができるようにしたものです。WebブラウザーのURLでアプリが準備した「場所」を指定すると、その情報をWebサーバーからもらってきたときと同じような形で表示します。実際は、その場所は「サーバー上にはない」場所で、ReactやVueのアプリが「仮想的にあるように表示している」状態になっています。
ReactやVueの提供する開発用の「ローカルサーバー」の場合は問題ありません
このReact RouterやVue Routerの機能は、開発用のテスト用で使うローカルサーバ(殆どの場合、「localhost:3000」)は、ReactやVueのアプリを動かす目的だけに作られているので、全てをReactやVueのアプリに任せています。これが、開発用のサーバーで利用した場合は問題なく動く理由です。
ところが、何も設定をしない状態でインターネットに公開すると話が変わってきます。
通常は、何も設定しないでインターネットに公開するとページの切り替えがうまく動かなくなります。理由は、WebブラウザがWebサーバーにリクエストを送ってしまうからです。Webサーバーは指定された「場所」を探しますが、ReactやVueのアプリが用意した「仮想的な場所」なので、Webサーバー上には存在しません。そのために、エラーになってしまうからです。
そのため、サーバー側の設定が必要になります。React Routerを利用する場合の設定方法は以前の記事で投稿しました。詳しくは記事をご覧ください。
簡単に説明すると、Firebaseでホスティングをする場合は、firebase.jsonで設定を行います。
"rewrites": [
    {
        "source": "**",
        "destination": "/index.html"
    }
],
という設定を行います。
この設定の意味は、全ての「場所の要求」("source":"*")を全て、index.html*で処理する("destination":"/index.html")という事です。
全てをReactやVueのアプリで処理してしまう場合はこの設定をすれば問題なく利用できます。

フロントエンドとバックエンドがFirebaseのプロジェクトフォルダを共有する場合は?

ところが、同じプロジェクトフォルダ内でフロントエンドとバックエンドがそれぞれ別々に色々な「場所」を処理する場合は、上のようなシンプルな設定では上手くいきません。
これが、多くの場合は、フロントエンドとバックエンドでプロジェクトを分ける理由です。
しかし、実装がシンプルな場合は設定もそれほど複雑にはなりません。その場合は同じプロジェクトで済ませてしまう方が管理も簡単になる場合があります。
以前紹介した、「Firebaseのバックエンドを利用したお問合せフォーム」の様な場合です。この場合は、スタティックなHTMLファイルとバックエンドのサービスだったので余り複雑ではありませんでした。
この実装のHTMLファイルの部分を、ReactやVueのアプリで置き換えた場合を考えます。
例として、Reactでお問い合わせのフォームを作成する。フォームの処理は、Reactではなく、バックエンドで行ってセキュリティルールを単純化するという実装を考えてみます。
import React from "react";
class QueryForm extends React.Component {
  render():JSX.Element {
    return (
      <form action="/post" method="post">
        <div>
          <label>E-Mail</label>
          <input type="text" name="email" />
        </div>
        <div>
          <label>Name</label>
          <input type="text" name="name" />
        </div>
        <div>
          <label>Subject</label>
          <input type="text" name="subject" />
        </div>
        <div>
          <label>Message</label>
          <textarea name="message"></textarea>
        </div>
        <button type="submit">Submit</button>
      </form>
    );
  }
}
トップレベルの、App.tsxは、以下のようにReact Routerを使っているとします。
import React from "react";
import { BrowserRouter, Link, Switch, Route } from "react-router-dom";
import HomePage from "../pages/homepage";
import QueryForm from "../pages/queryform";
export default class App extends React.Component {
  render(): JSX.Element {
    return (
      <div className="App">
        <BrowserRouter>
          <Switch>
            <Route path="/" exact>
              <HomePage />
            </Route>
            <Route path="/query">
              <QueryForm />
            </Route>
          </Switch>
        </BrowserRouter>
      </div>
    );
  }
}
Reactアプリなので、基本は公開用のイメージの中の「index.html」が、2つのページの処理を行います。「"/"」と「"/query"」の仮想の2つのページはReactアプリが処理します。
お問合せフォームの中身は、HTTPの「POST」メソッドでバックエンドに送られます。従ってバックエンドでは「"/post"」の処理が必要になります。
const app = express();
    app.post("/post", (req:express.Request, res:express.Response) => {
        .....
        // フォームのデータの処理をここに記述
    })
のようにすれば、送られてきたフォームの処理を行うことができます。
この設定を、firebase.jsonで行います。
{
  "functions": {
    "predeploy": [
      "npm --prefix \"$RESOURCE_DIR\" run lint",
      "npm --prefix \"$RESOURCE_DIR\" run build"
    ]
  },
  "hosting": {
    "public": "build",
    "rewrites":[
        {
            "source":"/post",
            "function":"app"
        },
        {
            "source": "**",
            "destination":"/index.html"
        }
    ],
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
}
このファイルは上から順番に処理されるので、「/query」はバックエンドの、「app」というファンクションで処理して、それ以外は、「index.html」で処理するという設定にすれば良いことになります。 また、Reactの場合には公開用のイメージが「build」のフォルダの下に作られるので、「"public"」の設定も「"build"」に変更した方が便利です。
この場合、ページがない場合の処理は、Reactアプリで行うことになります。

テスト時の注意

この方法を使った場合には、開発時のテストに注意が必要です。 Reactの開発環境で「npm start」で起動したローカルサーバーは基本的に機能しません。 このサーバーではバックエンドのサービスが動いていないからです。
ローカルでテストを行う場合には、Firebaseのテスト用のサーバーを「firebase serve」で起動して使用する必要があります。このテスト用のローカルサーバーはバックエンドのサービスも起動しているので、全ての機能を使えます。
実行の前に、Reactの公開用のイメージを「npm run build」で作成する必要があります。
Reactの開発環境が提供する、ローカルサーバーと違って、ソースコードの変更を保存しただけでは、その変更が反映されないので注意が必要です。少し不便ですが、同じフォルダ内で全ての開発検証が行えます。

まとめ

FirebaseでWebホスティングをする場合、フロントエンドとバックエンドを同じプロジェクトフォルダーでインターネットに公開することもできます。
Webホスティングの設定が面倒になるので分ける場合も多くなります。しかし、実装がシンプルな場合は、ソースコードの管理などがシンプルになるために一緒のプロジェクトで扱う方が便利な場合もあります。
その場合は、どこで何を処理するのかを一つ一つ順番に、firebase.jsonの「rewrite」の部分に記述すれば、問題なく実装できます。処理は上から順番に行われるので、「"**"」のようなワイルドカードを使ったまとめた記述は一番最後に書きます。
Firebaseのホスティングの仕組みを理解すれば、複雑に思える設定も意外に簡単にできるものです。
サービス数40万件のスキルマーケット、あなたにぴったりのサービスを探す