クモのようにコツコツと

フロントエンドエンジニア イイダリョウの技術ブログ。略称「クモコツ」

【Firebase】Realtime DatabaseにFetch APIでCRUD操作する

Firebaseの続きです。前回はRealtime Databaseにブラウザから直接CRUD操作しました。この時はset()などRealtime Databaseに予め用意されたメソッドを使いました。その後、APIのURLがあることがわかったので、Fetch APIによるCRUD操作にトライ。それではいきましょう!

【目次】

※参考:前回記事
【Firebase】Realtime Databaseにブラウザから直接CRUD操作する - クモのようにコツコツと]

※参考:ネイティブJSでいろいろやってみたシリーズまとめ
qiita.com

前回のおさらい

Realtime Databaseにブラウザから直接CRUD操作 https://cdn-ak.f.st-hatena.com/images/fotolife/i/idr_zz/20210107/20210107072918.jpg

CRUD操作はFirebase Realtime Databaseで用意された下記のメソッドを使った。

  • Create(追加):set()
  • Read(取得):on()
  • Update(更新):update()
  • Delete(削除):remove()

詳細は前回の記事を参照

※参考:【Firebase】Realtime Databaseにブラウザから直接CRUD操作する - クモのようにコツコツと

Realtime DatabaseにREST APIのURLがある!?

その後、Realtime Databaseの「REST」のドキュメントを読んでいるとこんな記述を発見!

Firebase Realtime Database はクラウドホスト型データベースです。データは JSON として保存され、接続されたすべてのクライアントとリアルタイムに同期されます。

Firebase Realtime Database のあらゆる URL は REST エンドポイントとして使用できます。それには、.json を URL の末尾に追加してお気に入りの HTTPS クライアントからリクエストを送信するだけです。

※参考:REST API のインストールと設定  |  Firebase

なんと!?FirebaseにはREST APIのエンドポイントになるURLが用意されていると!そしてそのURLは.jsonを末尾に追加するだけと!?


前回作ったビートルズCRUDのDBはhttps://kumokotsu-test-default-rtdb.firebaseio.com/memberというURLなのだが f:id:idr_zz:20210112070711j:plain このURLをブラウザで開くと、ログインしているとFirebaseのコンソールに転送される。*1

このURLの末尾に.jsonを追加してみると… f:id:idr_zz:20210112071502j:plain おお、データだけ表示された!

※参考:https://kumokotsu-test-default-rtdb.firebaseio.com/member.json

さらに深い階層にすると下層データのみを取得できる! f:id:idr_zz:20210112071548j:plain

※参考:https://kumokotsu-test-default-rtdb.firebaseio.com/member/1.json


REST APIについてはこちらも参照

※参考:REST APIとは何かを調べまくったらようやくイメージができてきたのでまとめた - クモのようにコツコツと

Realtime DatabaseにFetch APIでCRUD操作できる?

RESTのドキュメントにはターミナルからcurlコマンドでCRUD操作する方法が書かれている。

※参考:データの保存  |  Firebase
※参考:データの取得  |  Firebase

以前、curlコマンドを使ってJSON ServerにでCRUDした。同じようなことがRealtime Databaseでもできると。

※参考:【擬似NoSQL】ターミナルからAPIモックのJSON ServerにCRUDする - クモのようにコツコツと


ただ、Realtime DatabaseにREST APIのURLが用意されているということは、このURLを元にFetch APIによるCRUD操作ができるということでは!?

少なくともGET通信すればデータの取得は確実にできるはず!その他、POST(追加)、UPDATE(更新)、DELETE(削除)もできるのであろうか?(ドキドキ…)。

HTMLファイル作成

以前、JSON ServerにブラウザからFetch APIを使ってCRUD操作した時の「今日の一句」をベースにして作ってみる。

※参考:【JS】Fetch APIを使ってJSON ServerにCRUDする - クモのようにコツコツと

新たに「fetch.thml」というHTMLを作成

    <section>
        <h1>今日の一句</h1>
            <section>
                <h2>一句詠む</h2>
                    <input type="text" name="ikku" size="30" maxlength="30" class="postIkku">
                    <input type="button" value="送信" class="postBtn">      
            </section>
            <section>
                <h2>過去の一句</h2>
                <ul class="ikkuList"></ul> 
            </section>
    </section>

なお、CSSの設定は最低限にしたのでこのファイルの中にstyleタグで書いている。

※参考:firebase-hosting-test/fetch.html at main · ryo-i/firebase-hosting-test · GitHub

JS:初期設定

Firebase初期化(不要だった)

ここからはJSファイルの設定。こちらは長いので「fetch.js」という別ファイルを作って読み込んだ。

※参考:firebase-hosting-test/fetch.js at main · ryo-i/firebase-hosting-test · GitHub

下記はFirebaseを認識する初期化設定だが、今回も前回と同様不要のようだった。

       // Config
        /* const config = {
            apiKey: "AIzaSyBju9iq3ug6gJMqyVsoGX_YByHt6L3Dh0c",
            authDomain: "kumokotsu-test.firebaseapp.com",
            databaseURL: "https://kumokotsu-test-default-rtdb.firebaseio.com",
            storageBucket: "kumokotsu-test.appspot.com"
        };
        if (firebase.apps.length === 0) {
            firebase.initializeApp(config);
        };

        const db = firebase.database(); */

全体をコメントアウトしても普通に動いた。今回はまったくの新規ファイルのため、その他のファイルとの依存関係はない。つまり、DBルールが読み書きOKにしていると初期化は不要、ということのようだ。

グローバル変数(DOM取得他)

冒頭でグローバル変数を設定

        const postIkku = document.querySelector('.postIkku');
        const postBtn = document.querySelector('.postBtn');
        const ikkulist = document.querySelector('.ikkuList');
        const url = 'https://kumokotsu-test-default-rtdb.firebaseio.com/ikkuList';
        const fullUrl = url + '.json';
  • 変数postIkkuでDOM.postIkkuを取得
  • 変数postBtnでDOM. postBtnを取得
  • 変数ikkulistでDOM. ikkuListを取得
  • 変数urlでAPIのURLを取得
  • 変数fullUrlurl.jsonを追加

DOM操作をするためにHTML上のDOMを取得。

Fetch APIで通信できるURLはDBのURLに.jsonを追加したもの。データの全体はfullUrlでいいが、個別のデータは間にパス/hogeを差し込む必要があるため、このような設定になった。

JS:CRUD操作

ここからCRUD操作。結論からいうとGET以外のPOST、UPDATE、DELETEも動いた!ただし、JSON-ServerとはJSONデータの構造がちょっと違ったため、処理内容は少し変えている。

データの追加:Create

最初はCRUDのC、データの追加(Create)

        // Create
        const createFetch = () => {
            const thisUrl = fullUrl;
            const data = {
                ikku: postIkku.value
            };
            fetch(thisUrl, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(data)
            }).then((response) => {
                if(!response.ok) {
                    console.log('Create error!');
                    throw new Error('error');
                }
                console.log('Create ok!');
                return response.json();
            }).then((data)  => {
                const thisId = data.name;
                getFetchData(thisId);
            }).catch((error) => {
                console.log(error);
            });
        };
  • 変数createFetchの値は無名関数
  • 変数thisUrlfullUrlを取得
  • 変数dataは連想配列でikkuキーにpostIkkuの入力値valueを入れる
  • fetch()の第一引数はthisUrlで第二引数は連想配列、ここにオプションを設定
    methodPOST通信
    headersContent-Typejson
    bodydataを送る(JSON.stringify()でJSON形式に変換)
  • 1つ目のthen()はレスポンス設定
    もしレスポンスがokでなかったらコンソールにCreate error!と表示してerrorに進む
    コンソールにCreate ok!と表示してレスポンスを次のthen()json形式で送る
  • 2つ目のthen()はデータ処理
    変数thisIddatanameを取得(これがID番号になる)
    getFetchData()を実行(引数はthisId
  • 最後のcatch()はエラー処理(引数はerror
    コンソールにエラー内容errorを表示

Realtime DatabaseのPOST通信のレスポンスはIDだけだった(IDはランダムな文字列)。

{"name":"xxxxxxxx(ID文字列)"}

そのため、このIDを元にgetFetchData()で個別のデータを取得する。


getFetchData()の内容

        const getFetchData = (thisId) => {
            const thisUrl = url + '/' + thisId + '.json'
            console.log('Create Id->' + thisId);
            return fetch(thisUrl).then((response) => {
                if(!response.ok) {
                    console.log('Read error!');
                    throw new Error('error');
                } 
                console.log('Read ok!');
                return response.json();
            }).then((data)  => {
                postIkku.value = '';
                appendList(thisId, data);
            }).catch((error) => {
                console.log(error);
            });
        };
  • 変数getFetchDataの値は無名関数(引数はthisId
  • 変数thisUrlurlとスラッシュ'/'とthisId.json
  • コンソールでIDを表示
  • 変数dataは連想配列でikkuキーにpostIkkuの入力値valueを入れる
  • returnfetch()を返す。引数はthisUrl
  • 1つ目のthen()はレスポンス設定
    もしレスポンスがokでなかったらコンソールにRead error!と表示してerrorに進む
    コンソールにRead ok!と表示してレスポンスを次のthen()json形式で送る
  • 2つ目のthen()はデータ処理
    postIkkuの値をvalueを空に
    appendList()を実行(引数はthisIddata
  • 最後のcatch()はエラー処理(引数はerror
    コンソールにエラー内容errorを表示

GET通信はfetch()の初期設定のためオプション設定は不要。GET通信で一句の内容が返ってくる。

{"ikku":"(一句)"}

createFetch()で得たidgetFetchData()で得たikkuを元にappendList()を実行する

fetch()の前にreturnを入れてプロミス全体を返したのはcreateFetch()getFetchData()が非同期で並行に動き、処理の順番が前後することがあったため。

こちらのQ&Aが参考になった。

※参考:JavaScript - [fetch api]returnをしてもundefinedになる|teratail

プロミスの直列処理

※参考:Promiseとthenのメソッドチェーン(直列・並列・値の受け取り・引数) - Qiita


最後にボタンのクリックイベント

        postBtn.addEventListener('click', createFetch, false);

postBtnをクリックしたらcreateFetchを実行

データの取得:Read

次はCRUDのR、データの取得(Read)

        // Read
        const readFetch = () => {
            const thisUrl = fullUrl;
            fetch(thisUrl).then((response) => {
                if(!response.ok) {
                    console.log('Read error!');
                    throw new Error('error');
                } 
                console.log('Read ok!');
                return response.json();
            }).then((data)  => {               
                Object.keys(data).forEach((key) => {
                    appendList(key, data[key]);
                });
            }).catch((error) => {
                console.log(error);
            });
        };
  • 変数readFetchの値は無名関数
  • 変数thisUrlfullUrl
  • fetch()を実行(引数はthisUrl
  • 1つ目のthen()はレスポンス設定
    もしレスポンスがokでなかったらコンソールにRead error!と表示してerrorに進む
    コンソールにRead ok!と表示してレスポンスを次のthen()json形式で送る
  • 2つ目のthen()はデータ処理
    Object.keys()dataを取得
    forEach()で連想配列を処理をループ(引数key) でappendList()を実行、引数はkeydata[key]`
  • 最後のcatch()はエラー処理(引数はerror
    コンソールにエラー内容errorを表示

先ほどはIDを元に個別のデータを取得したが今回はfullUrlで全体のデータを取得する。

レスポンスはこのような連想配列になっている。カンマ区切りになっている。これを元にループしたい。

{
  "xxxxxxxx(ID文字列)":{"ikku":"(一句)"},
  "xxxxxxxx(ID文字列)":{"ikku":"(一句)"},
  "xxxxxxxx(ID文字列)":{"ikku":"(一句)"}
}

連想配列のループ処理はいくつかある(配列で使っているlengthは連想配列では動かない)。

※参考:JavaScriptで配列やオブジェクトをループする良い方法を真剣に検討してみた - Qiita

forEach()が良さそう。キー名(ID文字列)でループできそう。

forEach()は、子供の正確なパスを知らずに子供に到達するために使用されます。

※参考:javascript — Firebase Databaseからデータを取得する方法は?

forEach()Object.keys()でキー名key(ID文字列)にアクセスできた!data[key]で値を取得。

連想配列のキーを配列として取得するには次のようにObject.keys関数が使えます。

※参考:JavaScriptで連想配列に対してforEachループを使う方法 | PisukeCode - Web開発まとめ

取得したキーkeyと値data[key]appendList()の引数に入れて実行。


最後にreadFetch()を実行。ページ読み込み時とリロード時に実行される。

        readFetch();

データの更新:Update

次はCRUDのU、データの更新(Update)

        // Update
        const updateFetch = (thisLi) => {
            const thisId = thisLi.dataset.id;
            const thisUrl = url + '/' + thisId + '.json'
            const updateArea = thisLi.querySelector('.updateArea');
            const updateIkku = thisLi.querySelector('.updateIkku').value;
            const data = {
                ikku: updateIkku
            };           
            fetch(thisUrl, {
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(data)
            }).then((response) => {
                if(!response.ok) {
                    console.log('Update error!');
                    throw new Error('error');
                } 
                console.log('Update ok!');
                return response.json();
            }).then((data)  => {
                console.log('Update Id->' + thisId);
                console.log('Updata Ikku->' + data.ikku);
                thisLi.firstChild.textContent = data.ikku;
                thisLi.removeChild(updateArea);
            }).catch((error) => {
                console.log(error);
            });
        };
  • 変数updateFetchの値は無名関数(引数はthisLi
  • 変数thisIdthisLiのデータ属性data-idの値を取得
  • 変数thisUrlurlthisIdのパスを合わせたURLを作成
  • 変数updateAreaでDOM.updateAreaを取得
  • 変数updateIkkuでDOM. updateIkkuのフォーム入力値を取得
  • 変数dataは連想配列でikkuキーにupdateIkkuを入れる
  • fetch()の第一引数はthisUrlで第二引数は連想配列、ここにオプションを設定
    methodPUT通信
    headersContent-Typejson
    bodydataを送る(JSON.stringify()でJSON形式に変換)
  • 1つ目のthen()はレスポンス設定
    もしレスポンスがokでなかったらコンソールにUpdate error!と表示してerrorに進む
    コンソールにUpdate ok!と表示してレスポンスを次のthen()json形式で送る
  • 2つ目のthen()はデータ処理
    コンソールでthisIddata.ikkuの値を表示
    thisLiの一つ目のコンテンツのテキストをレスポンスのikkuの値に
    thisLiupdateAreaを削除
  • 最後のcatch()はエラー処理(引数はerror
    コンソールにエラー内容errorを表示

datasetでデータ属性の値を取得できる(ID文字列を入れている)。

※参考:HTMLOrForeignElement.dataset - Web API | MDN

更新するコンテンツのID文字列をAPIのURLのパスに追加してPOST通信。

先ほどのgetFetchData()と同じくレスポンスは個別のデータ内容のみ返ってくる。

{"ikku":"(一句)"}

ikkuキーの値でテキストを上書き更新し、「追加」「削除」ボタンは削除する。


クリックイベント

        document.addEventListener('click', (e) => {
            if (e.target.className !=='updateBtn') {
                return;
            } 
            const thisLi = e.target.closest('li');
            updateFetch(thisLi);
        }, false);
  • もしクリックしたターゲットがupdateBtnじゃなければ何もしない
  • 変数thisLiでターゲットの親要素のliタグを取得
  • updateFetch()を実行(引数はthisLi

closest()で親要素を取得できる

※参考:Element.closest() - Web API | MDN

データの削除:Delete

最後はCRUDのD、データの削除(Delete)

        // Delete
        const deleteFetch = (thisLi) => {
            const thisId = thisLi.dataset.id;
            const thisUrl = url + '/' + thisId + '.json'            
            fetch(thisUrl, {
                method: 'DELETE',
                headers: {
                    'Content-Type': 'application/json'
                },
            }).then((response) => {
                if(!response.ok) {
                    console.log('Delete error!');
                    throw new Error('error');
                }
                console.log('Delete ok!');
                return response.json();
            }).then((data)  => {
                if (data === null) {
                    console.log('Delete ID->' + thisId);
                }
                thisLi.remove();
            }).catch((error) => {
                console.log(error);
            });
        };
  • 変数deleteFetchの値は無名関数(引数はthisLi
  • 変数thisIdthisLiのデータ属性data-idの値を取得
  • 変数thisUrlurlthisIdのパスを合わせたURLを作成
  • 変数updateAreaでDOM.updateAreaを取得
  • 変数updateIkkuでDOM. updateIkkuのフォーム入力値を取得
  • 変数dataは連想配列でikkuキーにupdateIkkuを入れる
  • fetch()の第一引数はthisUrlで第二引数は連想配列、ここにオプションを設定
    methodDELETE通信
    headersContent-Typejson
    bodydataを送る(JSON.stringify()でJSON形式に変換)
  • 1つ目のthen()はレスポンス設定
    もしレスポンスがokでなかったらコンソールにDelete error!と表示してerrorに進む
    コンソールにDelete ok!と表示してレスポンスを次のthen()json形式で送る
  • 2つ目のthen()はデータ処理
    もしレスポンスがnullだったらコンソールに削除したIDthisIdを表示
    thisLiremove()を実行
  • 最後のcatch()はエラー処理(引数はerror
    コンソールにエラー内容errorを表示

削除の場合はレスポンスはnullしか返ってこない(逆にnullで更新すると削除常態にもできる)

※参考:ウェブでのデータの読み取りと書き込み  |  Firebase Realtime Database


クリックイベント

        document.addEventListener('click', (e) => {
            if (e.target.className !=='doDelete') {
                return;
            } 
            const thisLi = e.target.closest('li');
            deleteFetch(thisLi);
        }, false);
  • もしクリックしたターゲットがdoDeleteじゃなければ何もしない
  • 変数thisLiでターゲットの親要素のliタグを取得
  • deleteFetch()を実行(引数はthisLi

JS:DOM操作

DOM操作はJSON-Serverの内容がベースになっているので詳細はこちらを参照。

※参考:【JS】Fetch APIを使ってJSON ServerにCRUDする - クモのようにコツコツと

ボタンの追加

        // Append Button
        const appendBtn = (className, text) => {
            const btn = document.createElement('button');
            btn.className = className;
            btn.innerHTML = text;
            return btn;
        };

リストの追加

        // Append List
        const appendList = (id, thisData) => {
            const li = document.createElement('li');
            const thisId = id;
            console.log('Read Id->' + thisId);
            const thisIkku = thisData.ikku;
            console.log('Read Ikku->' + thisIkku);
            li.dataset.id = thisId;
            li.innerHTML = thisIkku;
            const updateBtn = appendBtn('doUpdate', '修正');
            li.appendChild(updateBtn);
            const deleteBtn = appendBtn('doDelete', '削除');
            li.appendChild(deleteBtn);
            ikkulist.appendChild(li);
        };

ここは前回と異なっていて引数がidthisDataの2つ。それぞれ個別にidliのデータ属性、thisDataikkuをテキストに入れている。


修正欄のinputタグを追加

        // Append Update Area
        const appendUpdateInput =  (thisIkku) => {
            const input = document.createElement('input');
            input.type = 'text';
            input.name = 'updateIkku';
            input.size = '30';
            input.maxlength = '30px';
            input.className = 'updateIkku';
            input.value = thisIkku;
            return input;
        };

修正欄の送信ボタンを追加

        const appendUpdateBtn = () => {
            const btn = document.createElement('input');
            btn.type = 'button';
            btn.value = '送信';
            btn.className = 'updateBtn';
            return btn;
        };

修正欄を追加

        const appendUpdateArea = (thisLi) => {
            const thisIkku = thisLi.firstChild.textContent;
            const appendDiv = document.createElement('div');
            appendDiv.className = 'updateArea';
            appendDiv.appendChild(appendUpdateInput(thisIkku));
            appendDiv.appendChild(appendUpdateBtn());
            thisLi.appendChild(appendDiv);
        };

修正欄追加のイベント設定

        document.addEventListener('click', (e) => {
            if (e.target.className !=='doUpdate') {
                return;
            }
            const thisLi = e.target.closest('li');
            if (thisLi.querySelector('.updateArea') === null) {
                appendUpdateArea(thisLi);
            }
        }, false);

ブラウザ動作確認

ブラウザの動きはこうなる。下記はfirebase deployでデプロイしたFirebase上での表示。

※参考:Realtime DatabaseにFetch APIでCRUD

ページ読み込み時(0件)

ページ読み込み時(投稿が0件の時) f:id:idr_zz:20210115053203j:plain

Dev-tools > Network > Headersを見るとGET通信が実行されている f:id:idr_zz:20210115054242j:plain (ステータスコード200は通信成功)

おなじくNetwork > Responseを見ると、投稿0件のレスポンスは「null」 f:id:idr_zz:20210115054712j:plain

データの追加:Create

「一句読む」に一句を入力して投稿 f:id:idr_zz:20210115053225j:plain

「過去の一句」に表示された! f:id:idr_zz:20210115053229j:plain

Headersを見るとPOST通信が実行されている f:id:idr_zz:20210115054827j:plain

Responseは「name」キーで値はID文字列 f:id:idr_zz:20210115055006j:plain

さらにこのID文字列をパスにしたURLでGET通信 f:id:idr_zz:20210115055139j:plain

レスポンスは「ikku」キーで一句を取得 f:id:idr_zz:20210115055151j:plain

データの取得:Read

2件追加して合計3件にした f:id:idr_zz:20210115053232j:plain

この状態でページをリロードするとGET通信が実行されている f:id:idr_zz:20210115055531j:plain

最初のGETのResponseは「null」だが今度は3件一気に取得 f:id:idr_zz:20210115055611j:plain

Previewを見るとID文字列をキーにした連想配列でカンマ区切りになっている f:id:idr_zz:20210115055624j:plain

データの更新:Update

「過去の一句」の中の2番目の一句を修正する f:id:idr_zz:20210115053235j:plain

一句が修正された! f:id:idr_zz:20210115053238j:plain

Headersを見ると修正する一句のIDをパスにしてPOST通信が実行されている f:id:idr_zz:20210115055841j:plain

Responseを見ると修正後の一句が返ってきている f:id:idr_zz:20210115055854j:plain

データの削除:Delete

「過去の一句」の中の3番目の一句を削除する(削除ボタンを押す) f:id:idr_zz:20210115053241j:plain

一句が削除された! f:id:idr_zz:20210115053244j:plain

Headersを見ると削除する一句のIDをパスにしてDELETE通信が実行されている f:id:idr_zz:20210115060008j:plain

Responseは「null」のみ f:id:idr_zz:20210115060247j:plain

その他の環境

その他、ローカル環境(firebase serve)、GitHubで生成されたGitHub Pagesのすべてで同じように動くことを確認できた。

firebase serveでローカル起動した表示(localhostは5000番) f:id:idr_zz:20210115060950j:plain


GitHub Pages上での表示 f:id:idr_zz:20210115061108j:plain

※参考:GitHub Pages
Realtime DatabaseにFetch APIでCRUD

ローカル環境、Firebase 、GitHub Pagesのどの環境でCRUD操作しても同じ内容が同期されて表示される!


プレビュー(Firebase)
※参考:Realtime DatabaseにFetch APIでCRUD

ソース(GitHub)
※参考:firebase-hosting-test/fetch.html at main · ryo-i/firebase-hosting-test · GitHub
※参考:firebase-hosting-test/fetch.js at main · ryo-i/firebase-hosting-test · GitHub

最後に

ということでFirebaseのRealtime DatabaseにFetch APIを使ってCRUD操作できました!Realtime Database上にはFetch APIを使った方法は見当たらず、半信半疑の状態でやってみたらできたので感動しました♪

REST APIが用意されているということは少なくともGET通信ができるのは間違いないので、ワンチャン狙いでPOST、UPDATE、DELETEも叩いてみたらAPIがちゃんと用意されていたようです。

これまで、MySQL、PostgreSQL、MongoDBなどにFetch APIを使ってCRUD操作する際にはサーバ側のAPI設定も必要でExpress作っていました。それがFirebaseは最初からAPIが用意されているということでブラウザのFetch APIを設定するだけで完結します。

これはちょっと驚くべき楽さで、ブラウザ側のフロントエンド開発に集中することができるわけです。しかも!Firebase固有のset()などのメソッドを使わず一般的なFetch APIのみなので、今後DBを別の種類に変更する際もメンテナンスが少なく済みそうです。

ただ、現状だとあらゆる環境から誰でもCRUDできてしまうため、この先必要になってくるのはユーザー認証設定のノウハウかな、と思います。Firebaseはユーザー認証も「Authentication」というプロダクトを用意してくれているので、調べてみようかと思います。

それではまた!


※参考:ネイティブJSでいろいろやってみたシリーズまとめ
qiita.com

*1:ログインしていないとFirebaseのログイン画面になる