Lento con forza

大学生気分のIT系エンジニアが色々書いてく何か。ブログ名決めました。

Next.jsのアプリをFirebase Hostingでいい感じに連携する

最近Next.jsとFirebaseばかりやっています。って書きましたが、本当はこの一ヶ月どうぶつの森しかやってませんでした。どうぶつの森のプレイ時間は200時間を超えました。

そろそろエンジニアとしても生活したくなってきたのでエンジニアっぽい話をします。Next.jsで作ったアプリをデプロイする先の選択肢として、Verselを使うと便利です。

SSRが必要なく、静的ファイルをデプロイすれば良い場合はFirebase Hostingなども選択肢に入ります。Firestoreなどと組み合わせて使っている時は、設定を工夫することで便利に使うことができます。今回の記事ではNext.jsで作ったアプリをFirebaseと組み合わせて使う方法を紹介します。

Firebase HostingにNext.jsの静的生成機能を使ってデプロイする

まずは、npm initから始めます。

npm init next-app firebase-hosting-with-next

firebase initでfirebaseの設定も行いましょう。

firebase init

package.jsonにexport用のスクリプトを記述します。

--- a/package.json
+++ b/package.json
@@ -5,7 +5,8 @@
   "scripts": {
     "dev": "next dev",
     "build": "next build",
-    "start": "next start"
+    "start": "next start",
+    "export": "next export"
   },

firebase.jsonにpredeployを追加します。

--- a/firebase.json
+++ b/firebase.json
@@ -4,6 +4,7 @@
     "indexes": "firestore.indexes.json"
   },
   "hosting": {
+    "predeploy": "yarn build && yarn export",
     "public": "out",
     "ignore": [
       "firebase.json",

あとはデプロイすれば完成ですね!

firebase deploy

デプロイ完了後、HostingのURLを開けばNext.jsの初期ページが表示されます。

f:id:kouki_dan:20200430195506p:plain

さて、これで良いのでしょうか?

完全に静的なページであればこれで十分です。しかし、Firestoreなどで動的なデータを表示したい場合はFirebaseの設定を行う必要があります。

Firebase Hostingの機能を使ってFirebaseの初期設定を行う

WebアプリでのFirebaseの設定方法は大きく分けて2種類あります。1つ目はFirebaseのスクリプトを読み込み、firebase.initializeApp(firebaseConfig); でFirebaseを初期化する方法です。

この方法は単純なのですが、コードに設定が現れてしまいます。開発用と本番用で別のFirebaseプロジェクトを利用している場合などは、何かしらの工夫をして設定を出し分けなくては行けません。

もう一つの方法として、Firebase Hostingを利用している場合に使える方法があります。Firebase Hostingを利用する場合、/__/firebase/init.json という特別なURLから、認証情報が含まれた初期化用のjsonファイルを読み込むことができます。Firebase Hostingはデプロイ先のプロジェクトを自動的に認識し、適切な認証情報をアプリケーションに付与することができます。

この方法を利用することで、同じコードベースを複数のプロジェクトにデプロイすることが容易になります。Firebase Hostingとこの初期化方法を組み合わせることができるととても便利です。

firebase.google.com

Next.jsとこの機能を組み合わせて初期化できるようにしていきましょう。

まずは、Firebaseを利用できるようにnpmからインストールします。

npm install firebase@7.14.2 --save

インストール中に、表示するためのデータをFirebaseコンソールから用意しましょう。今回はdataコレクションにnameを持ったドキュメントを一つ生成しました。

f:id:kouki_dan:20200430193854p:plain

Firebaseの初期化は_app.jsで行います。componentDidMountで/__/firebase/init.jsonを読み込み、読み込みが終わったらページを読み込みます。ファイルを作成し、以下のように記述します。

// pages/_app.js
import React from "react";
import firebase from "firebase/app";
import App from "next/app";


class MyApp extends App {
  constructor(props) {
    super(props);
    this.state = {
      firebaseInitialized: false,
    };
  }

  componentDidMount() {
    fetch("/__/firebase/init.json").then(async response => {
      firebase.initializeApp(await response.json());
      this.setState({
        firebaseInitialized: true
      });
    });
  }

  render() {
    const { Component, pageProps } = this.props;
    if (!this.state.firebaseInitialized) {
      return <></>;
    }
    return (
      <Component {...pageProps} />
    );
  }
}

export default MyApp;

_appにこの設定をしておけば、各ページはFirebaseが初期化された状態でロードされます。index.jsを編集して、先ほどコンソールからデータベースに追加したデータを読み込んでみましょう。

--- a/pages/index.js
+++ b/pages/index.js
@@ -1,6 +1,15 @@
 import Head from 'next/head'
+import firebase from 'firebase';
+import { useEffect, useState } from 'react';

 export default function Home() {
+  const [name, setName] = useState("");
+  useEffect(() => {
+    const db = firebase.firestore();
+    db.collection("data").get().then( snapshot => {
+      setName(snapshot.docs[0].data()["name"])
+    })
+  }, [])
   return (
     <div className="container">
       <Head>
@@ -12,6 +21,9 @@ export default function Home() {
         <h1 className="title">
           Welcome to <a href="https://nextjs.org">Next.js!</a>
         </h1>
+        {name && <h2>
+          Your name is {name}
+        </h2>}

firestore.rulesで読み込みを許可するのを忘れないようにしましょう。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read;
      allow write: if false;
    }
  }
}

これをFirebase Hostingにデプロイすることで、プロジェクトの設定を(.firebaserc以外には)コードベースに書くことがなく、Firebaseの初期設定を行うことができます。

firebase deploy

f:id:kouki_dan:20200430200120p:plain

Firestoreから取得したデータを表示できました!

もっと便利に開発する

ここまでの設定で、デプロイはとても便利に行えるようになりました。しかし、開発時はどうでしょうか?Next.jsのアプリは next dev を使うことで、ホットリロードが有効な状態で開発できます。しかし、next dev で起動した開発用サーバーは、/__/firebase/init.json が配信されないため、Firebaseの機能を使うことができません。

Firebase Hostingを使う方法はどうでしょう。Firebaseにはローカルで動かすためのコマンド firebase serve が存在しています。firebase serve で起動した開発用サーバーでは /__/firebase/init.json を読むことができます。これを使うことで手元でもFirebaseの設定が有効な状態で開発できます。 ただ、これは firebase.json に設定したディレクトリをサーブしているだけなので、確認のたびに yarn build && yarn export を行う必要があります。これでは不便ですね。

これらを組み合わせることができれば便利に開発ができそうです。どのようにすると良いのでしょうか。

これはNext.jsの機能を使うことで解決可能です。Custom Routesを使うことで実現していきましょう。これはまだ実験的機能なので、experimentalネームスペースの中にあります。しかし、開発時しか使われないので問題になることはないでしょう。

next.config.js ファイルを作成し、以下のように記述します。

module.exports = {
  experimental: {
    rewrites() {
      return [
        // Use rewrite to fetch a Firebase config file from Firebase Hosting
        // This only works `yarn dev` and does not works in production by `yarn export`
        {
          source: '/__/firebase/init.json',
          destination: 'http://localhost:5000/__/firebase/init.json'
        },
      ];
    },
  }
}

このように記述することで、Next.jsの開発用サーバーの /__/firebase/init.json にきたリクエストで、Hostingの同じパスへのレスポンスとして返すことができるようになります。

あとは npm-run-all と組み合わせて、yarn dev時にFirebase Hostingも起動するようにしましょう。必要なコンポーネントをインストールします。

npm install firebase-tools npm-run-all --save-dev

package.jsonのスクリプトを書き換え、npm-run-allとfirebaseを利用する形に書き換えます。

--- a/package.json
+++ b/package.json
@@ -3,7 +3,9 @@
   "version": "0.1.0",
   "private": true,
   "scripts": {
-    "dev": "next dev",
+    "dev:hosting": "firebase serve --only hosting",
+    "dev:next": "next dev",
+    "dev": "run-p dev:*",
     "build": "next build",
     "start": "next start",

この設定をすると、開発時は yarn dev を使えます。

yarn dev

http://localhost:3000 を開くと、ローカルで動作し、Firebaseの設定も有効になっていることを確認できます。

f:id:kouki_dan:20200430202452p:plain

ソースコードを編集すると、ブラウザにすぐに反映されます。

f:id:kouki_dan:20200430202548p:plain

これで開発時も便利に開発を行うことができるようになりました。

ここまでのコードはこちらのリポジトリで公開しています。参考にしていただけると嬉しいです。

github.com

まとめ

Firebase Hostingでは設定を配信するためのURLがあり、これを使うことでコードベースに設定を依存させることなく動作させることが可能です。静的に生成されたjsファイルでは、しばしば設定をどうするかが問題になります。例えばAPIの接続先をJavaScriptに埋め込んでしまうと、開発用、ステージング、本番それぞれ別の成果物が必要になります。Firebase Hostingのように特定のURLから設定を配信することは、この解決策として有用なパターンの一つではないでしょうか。今回書いたコードは環境に依存していなく、firebase deployでのデプロイ先を別のプロジェクトに変更するだけで、同じコードベースでかつbuildし直すこともなく複数環境にデプロイ可能です。

実際に自分のアプリに導入するとなれば、初期化のjsonファイルをどのようにCDNやサーバーから配布するかと言った課題は残りますが、Firebase Hostingのこのパターンは静的なサイトを配信する上でとても有用だと思います。名前があるなら誰か教えてください。