Firebaseの続きです。前回はRealtime Databaseにブラウザから直接CRUD操作しました。この時はset()
などRealtime Databaseに予め用意されたメソッドを使いました。その後、APIのURLがあることがわかったので、Fetch APIによるCRUD操作にトライ。それではいきましょう!
【目次】
- 前回のおさらい
- Realtime DatabaseにREST APIのURLがある!?
- Realtime DatabaseにFetch APIでCRUD操作できる?
- HTMLファイル作成
- JS:初期設定
- JS:CRUD操作
- JS:DOM操作
- ブラウザ動作確認
- 最後に
※参考:前回記事
【Firebase】Realtime Databaseにブラウザから直接CRUD操作する - クモのようにコツコツと]
※参考:ネイティブJSでいろいろやってみたシリーズまとめ
qiita.com
前回のおさらい
Realtime Databaseにブラウザから直接CRUD操作
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なのだが
このURLをブラウザで開くと、ログインしているとFirebaseのコンソールに転送される。*1
このURLの末尾に.json
を追加してみると…
おお、データだけ表示された!
※参考:https://kumokotsu-test-default-rtdb.firebaseio.com/member.json
さらに深い階層にすると下層データのみを取得できる!
※参考: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を取得 - 変数
fullUrl
でurl
に.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
の値は無名関数 - 変数
thisUrl
でfullUrl
を取得 - 変数
data
は連想配列でikku
キーにpostIkku
の入力値value
を入れる fetch()
の第一引数はthisUrl
で第二引数は連想配列、ここにオプションを設定
method
はPOST
通信
headers
はContent-Type
をjson
に
body
でdata
を送る(JSON.stringify()
でJSON形式に変換)- 1つ目の
then()
はレスポンス設定
もしレスポンスがok
でなかったらコンソールにCreate error!
と表示してerror
に進む
コンソールにCreate ok!
と表示してレスポンスを次のthen()
にjson
形式で送る - 2つ目の
then()
はデータ処理
変数thisId
にdata
のname
を取得(これが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
) - 変数
thisUrl
はurl
とスラッシュ'/'とthisId
と.json
- コンソールでIDを表示
- 変数
data
は連想配列でikku
キーにpostIkku
の入力値value
を入れる return
でfetch()
を返す。引数はthisUrl
- 1つ目の
then()
はレスポンス設定
もしレスポンスがok
でなかったらコンソールにRead error!
と表示してerror
に進む
コンソールにRead ok!
と表示してレスポンスを次のthen()
にjson
形式で送る - 2つ目の
then()
はデータ処理
postIkku
の値をvalue
を空に
appendList()
を実行(引数はthisId
、data
) - 最後の
catch()
はエラー処理(引数はerror
)
コンソールにエラー内容error
を表示
GET通信はfetch()
の初期設定のためオプション設定は不要。GET通信で一句の内容が返ってくる。
{"ikku":"(一句)"}
createFetch()
で得たid
、getFetchData()
で得た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
の値は無名関数 - 変数
thisUrl
はfullUrl
fetch()
を実行(引数はthisUrl
)- 1つ目の
then()
はレスポンス設定
もしレスポンスがok
でなかったらコンソールにRead error!
と表示してerror
に進む
コンソールにRead ok!
と表示してレスポンスを次のthen()
にjson
形式で送る - 2つ目の
then()
はデータ処理
Object.keys()
でdata
を取得
forEach()で連想配列を処理をループ(引数
key) で
appendList()を実行、引数は
key、
data[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
) - 変数
thisId
でthisLi
のデータ属性data-id
の値を取得 - 変数
thisUrl
でurl
とthisId
のパスを合わせたURLを作成 - 変数
updateArea
でDOM.updateArea
を取得 - 変数
updateIkku
でDOM. updateIkku
のフォーム入力値を取得 - 変数
data
は連想配列でikku
キーにupdateIkku
を入れる fetch()
の第一引数はthisUrl
で第二引数は連想配列、ここにオプションを設定
method
はPUT
通信
headers
はContent-Type
をjson
に
body
でdata
を送る(JSON.stringify()
でJSON形式に変換)- 1つ目の
then()
はレスポンス設定
もしレスポンスがok
でなかったらコンソールにUpdate error!
と表示してerror
に進む
コンソールにUpdate ok!
と表示してレスポンスを次のthen()
にjson
形式で送る - 2つ目の
then()
はデータ処理
コンソールでthisId
とdata.ikku
の値を表示
thisLi
の一つ目のコンテンツのテキストをレスポンスのikku
の値に
thisLi
のupdateArea
を削除 - 最後の
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
) - 変数
thisId
でthisLi
のデータ属性data-id
の値を取得 - 変数
thisUrl
でurl
とthisId
のパスを合わせたURLを作成 - 変数
updateArea
でDOM.updateArea
を取得 - 変数
updateIkku
でDOM. updateIkku
のフォーム入力値を取得 - 変数
data
は連想配列でikku
キーにupdateIkku
を入れる fetch()
の第一引数はthisUrl
で第二引数は連想配列、ここにオプションを設定
method
はDELETE
通信
headers
はContent-Type
をjson
に
body
でdata
を送る(JSON.stringify()
でJSON形式に変換)- 1つ目の
then()
はレスポンス設定
もしレスポンスがok
でなかったらコンソールにDelete error!
と表示してerror
に進む
コンソールにDelete ok!
と表示してレスポンスを次のthen()
にjson
形式で送る - 2つ目の
then()
はデータ処理
もしレスポンスがnull
だったらコンソールに削除したIDthisId
を表示
thisLi
でremove()
を実行 - 最後の
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); };
ここは前回と異なっていて引数がid
とthisData
の2つ。それぞれ個別にid
をli
のデータ属性、thisData
のikku
をテキストに入れている。
修正欄の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件の時)
Dev-tools > Network > Headersを見るとGET通信が実行されている (ステータスコード200は通信成功)
おなじくNetwork > Responseを見ると、投稿0件のレスポンスは「null」
データの追加:Create
「一句読む」に一句を入力して投稿
「過去の一句」に表示された!
Headersを見るとPOST通信が実行されている
Responseは「name」キーで値はID文字列
さらにこのID文字列をパスにしたURLでGET通信
レスポンスは「ikku」キーで一句を取得
データの取得:Read
2件追加して合計3件にした
この状態でページをリロードするとGET通信が実行されている
最初のGETのResponseは「null」だが今度は3件一気に取得
Previewを見るとID文字列をキーにした連想配列でカンマ区切りになっている
データの更新:Update
「過去の一句」の中の2番目の一句を修正する
一句が修正された!
Headersを見ると修正する一句のIDをパスにしてPOST通信が実行されている
Responseを見ると修正後の一句が返ってきている
データの削除:Delete
「過去の一句」の中の3番目の一句を削除する(削除ボタンを押す)
一句が削除された!
Headersを見ると削除する一句のIDをパスにしてDELETE通信が実行されている
Responseは「null」のみ
その他の環境
その他、ローカル環境(firebase serve
)、GitHubで生成されたGitHub Pagesのすべてで同じように動くことを確認できた。
firebase serve
でローカル起動した表示(localhostは5000番)
GitHub Pages上での表示
※参考: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のログイン画面になる