以前、Fetch APIを使ってJSON PlaceHolderにPOST送信してみました。今回は前々回に触れたJSON ServerをCRUD操作をしてみたく思います。JSON ServerにはDB機能だけでなくAPI機能も搭載されているので、そこに連携してみます。それではいきましょう!
【目次】
※参考:前回記事
【JS】Fetch APIでPOST送信してみる(Form送信との比較も) - クモのようにコツコツと
※参考:ネイティブJSでいろいろやってみたシリーズ
qiita.com
前回のおさらい
Fetch APIを使ってJSON PlaceHolderにPOST送信!
ただし、ダミーのPOST用URLだったため、レスポンスは得られなかった。
※参考:【JS】Fetch APIでPOST送信してみる(Form送信との比較も) - クモのようにコツコツと
今回はこのコードを改造して、Fetch APIでJSON ServerにCRUD処理できるようにしたい。
参考ページと今回の目標
下記の記事を参考に進める。
※参考:①コンソールへの表示
JSON Server使いこなし - モックサーバーの起動とリソース処理 | CodeGrid
※参考:②Vue.jsとの連携
JSON Serverで作るダミーAPI - to-R Media
※参考:③Reactとの連携
【初心者向け】非同期にCRUDするReactアプリケーションを作る - Qiita
JSON Serverの動作確認のためにJSフレームワーク(Vue.jsやReact)を使うのはちと重たい。
今回は①と②・③の間として「JSON ServerとネイティブJS(DOM操作)の連携」くらいを着地点にしたい。
作ってみた物
作ってみた物
※参考:ソースコード
GitHub - ryo-i/FetchCRUD
※参考:プレビュー(GitHub Pages)
Fetch APIでCRUD
「過去の一句」にはあらかじめ一句が表示されている。
JSON Serverのファイル「db.json」にも同じ一句がある。
{ "ikku": [ { "ikku": "JSや ああJSや JSや", "id": 1 } ] }
ikku
キーの値は配列- 配列の値は連想配列で、
ikku
キーは一句、id
は番号
この一句を読み込んで画面上に表示しているわけだ!
ただし、GitHub Pages上では静的なファイルしか動かないため、送信ボタンを押してもdb.jsonには反映されない。
ローカル環境でJSON Serverを起動すると、CRUD処理が反映される!
HTMLコード
以下、今回書いたコードについて解説。まずはHTMLから。
index.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>
画面に表示されている部分のみ抽出。それ以外はGitHubを参照。
※参考:FetchCRUD/index.html at master · ryo-i/FetchCRUD · GitHub
HTMLタグは実は大した内容ではない。
「一句詠む」の部分は実体のinputタグがあるが、その下の「過去の一句」は.ikkuList
のulタグ外枠しかない。ulタグの中のDOM操作はJSコードの方で書いている。
なお、CSSは今回最低限の設定しかしてないので割愛。詳細はGitHubを参照。
※参考:FetchCRUD/fetch_crud.css at master · ryo-i/FetchCRUD · GitHub
JSコード(グローバル変数)
ここからはJSファイルのコード。
※参考:FetchCRUD/fetch_crud.js at master · ryo-i/FetchCRUD · GitHub
まずは冒頭でグローバル変数を設定している。
const postIkku = document.querySelector('.postIkku'); const postBtn = document.querySelector('.postBtn'); const ikkulist = document.querySelector('.ikkuList'); const url = 'http://localhost:3000/ikku';
- 変数
postIkku
、postBtn
、ikkulist
はindex.htmlに実際にあるDOMを取得 - 変数
url
はJSON Server起動時のURL、ローカルホスト:3000直下のikku
オブジェクト
JSコード(CRUD操作)
Create:データの生成
ここからはCRUDの「C」でCreate(データの生成)
// Create const createFetch = () => { const data = { ikku: postIkku.value }; fetch(url, { 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) => { appendList(data); }).catch((error) => { console.log(error); }); }; postBtn.addEventListener('click', createFetch, false);
ブロックごとに見ていく
createFetch()
の中身
const data = { ikku: postIkku.value
- 変数
data
でikku
キーをpostIkku
のvalue
属性に
postIkku
はindex.htmlのinputタグ。ここに入力されて一句の値を取得してdata
キーに入れる。
次はfetch()
メソッド
fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) })
fetch()
の第一引数はurl
- 第二引数はオプション
method
キーの値はPOST
headers
にコンテンツがjson形式であることを送る
body
でJSON形式に変換したdata
を送る
Createは以前、下記の記事と同じPOST送信。この時はformDataを送った。
※参考:【JS】Fetch APIでPOST送信してみる(Form送信との比較も) - クモのようにコツコツと
今回はJSON形式なのでheaders情報にその設定を送信する。
※参考:Scala Json Requests - 2.0.x
data
の連想配列はJSON.stringify()
でJSON形式に変換。
※参考:JSON.stringify() - JavaScript | MDN
次のthen()
メソッド
.then((response) => { if(!response.ok) { console.log('Create error!'); throw new Error('error'); } console.log('Create ok!'); return response.json(); })
then()
の引数は無名関数で、無名関数の引数はresponse
response
がokじゃなければコンソールに「Create error!」と表示し、処理を抜ける- コンソールに「Create ok!」と表示
response
をjson形式で返す
※2020/09/13追記
エラー時に処理を抜けるようthrow new Error('error')
を追記した。これがないとその下の成功時の処理が実行される。(以下のCRUD処理も同様)
次のthen()
メソッド
.then((data) => { appendList(data); })
then()
の引数は無名関数で、無名関数の引数はdata
appendList()
関数を実行、引数はdata
appendList()
関数については後述する
最後、chach()
メソッド
.catch((error) => { console.log(error); });
chach()
の引数は無名関数で、無名関数の引数はerror
- コンソールにエラー内容を表示
コンソールにはエラー内容は表示されていないので通信エラーは起きていない!
イベント処理
postBtn.addEventListener('click', createFetch, false);
postBtn
をクリックしたときにcreateFetch
関数を実行
Read:データの取得
次はCRUDの「R」でRead(データの取得)
// Read const readFetch = () => { fetch(url).then((response) => { if(!response.ok) { console.log('Read error!'); throw new Error('error'); } console.log('Read ok!'); return response.json(); }).then((data) => { for (let i = 0; i < data.length; i++) { const thisData = data[i]; appendList(thisData); } }).catch((error) => { console.log(error); }); }; readFetch();
またブロックごとに見ていく。先ほどのCreateと重複しない処理を中心に。
readFetch()
関数の中身
fetch(url).then((response) => { if(!response.ok) { console.log('Read error!'); } console.log('Read ok!'); return response.json(); }).then((data) => { for (let i = 0; i < data.length; i++) { const thisData = data[i]; appendList(thisData); } }).catch((error) => { console.log(error); });
fetch()
の引数は第一引数のurl
のみ- 2つ目の
then()
ではfor文を配列data
の数量文繰り返す
変数thisData
で配列data
のi
番目を取得
appendList()
関数を実行、引数はthisData
fetch()
に第二引数のオプションがないのは以下の2つの理由
fetch()
はGET送信が標準で、Read操作もGETのためheaders
やbody
に送信するリクエスト情報もない
appendList()
関数については後述する。
最後にreadFetch()
を実行。
readFetch();
ページをロードする時に実行するのでイベント設定は不要。
Update:データの更新
次はCRUDの「U」でUpdate(データの更新)
// Update const updateFetch = (thisLi) => { const thisId = thisLi.dataset.id; const updateUrl = url + '/' + thisId; const updateArea = thisLi.querySelector('.updateArea'); const updateIkku = thisLi.querySelector('.updateIkku').value; const data = { ikku: updateIkku }; fetch(updateUrl, { 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) => { thisLi.firstChild.textContent = data.ikku; thisLi.removeChild(updateArea); }).catch((error) => { console.log(error); }); }; document.addEventListener('click', (e) => { if (e.target.className !=='updateBtn') { return; } const thisLi = e.target.closest('li'); updateFetch(thisLi); }, false);
ブロックごとに見ていく。
updateFetch()
関数には引数thisLi
が入る
// Update const updateFetch = (thisLi) => { // 中略 };
引数thisLi
に含まれる内容はポイントになってくる。詳細は後述。
updateFetch()
関数の中身、最初に変数設定
const thisId = thisLi.dataset.id; const updateUrl = url + '/' + thisId; const updateArea = thisLi.querySelector('.updateArea'); const updateIkku = thisLi.querySelector('.updateIkku').value; const data = { ikku: updateIkku };
- 変数
thisId
はthisLi
のデータ属性id
- 変数
updateUrl
はurl
と/
とthisId
- 変数
updateArea
はthisLi
の.updateArea
セレクタ - 変数
updateIkku
はthisLi
の.updateIkku'
セレクタの.value
属性 - 変数
data
は連想配列でikku
キーの値はupdateIkku
今回はURLにid
番号を追加してデータを特定している。その番号はデータ属性から得ている。
data
のikku
キーに入れたいupdateIkku
は後述するDOM操作で後から追加されるinputタグの入力値のため、ページロード時にはタグが存在しない。
そのため、updateFetch()
関数を実行するタイミングでタグとその入力値を取得している。querySelector()
をdocument
ではなくthisLi
に繋げると、その要素の子孫要素の中だけを見に行く。
次、fetch()
部分
fetch(updateUrl, { 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) => { thisLi.firstChild.textContent = data.ikku; thisLi.removeChild(updateArea); }).catch((error) => { console.log(error); });
fetch()
の第一引数はupdateUrl
。第二引数はオプションで無名関数
method
オプションはPUT
- 2つ目の
then()
でthisLi
の直下のテキストをdata
のikku
に
thisLi
のupdateArea
を削除
HTMLのformタグだとGETとPOSTしかできない。
※参考:なぜ html の form は PUT / DELETE をサポートしないのか? - Block Rockin’ Codes
しかしJSのFetchはPUT送信もできるようだった!
then()
の部分、「Update」は処理は少なかったため、直接ここに処理を書いた。
「過去の一句」のthisLi
のテキストを変更しようとしたら孫要素の内容まで含まれてしまった。firstChild
を指定すると直下の要素のみを変更できた!
※参考:JavaScript - 一部を除いて要素を取得したい|teratail
その後に削除しているupdateArea
はDOM操作で追加されたタグで、「修正」ボタンを押した時に現れる修正欄。修正が完了したら用はないので削除している。
updateFetch()
のイベント設定
document.addEventListener('click', (e) => { if (e.target.className !=='updateBtn') { return; } const thisLi = e.target.closest('li'); updateFetch(thisLi); }, false);
document
上をクリックした時に無名関数を実行(引数はイベントのe
)- もし
updateBtn
以外だった関数を抜ける - 変数
thisLi
はイベントターゲットの親要素のli
タグ updateFetch()
関数を実行、引数はthisLi
DOM操作で後から追加したupdateBtn
にイベントを操作する方法、こちらの方法が参考になった。document全体にクリックイベントを設定するが、if文で要素を特定して一致する場合のみ実行。
※参考:【JavaScript/jQuery】後から生成されたDOMからイベントを発火させる - B-Teck!
またこのif文をかますことで関数のネストが深くなるのを防ぐにはこちらの方法が参考になった。if文は冒頭に書いて!
で不一致の場合にreturn
で関数から抜けさせる。
※参考:JavaScript - ifのネストが深いので改善するべきか|teratail
closest()
は一つ上の親だけでなく祖先要素まで見に行ってくれる便利なメソッド。
※参考:Element.closest() - Web API | MDN
Delete:データの削除
最後、CRUDの「D」はDelete(データの削除)
// Delete const deleteFetch = (thisLi) => { const thisId = thisLi.dataset.id; const updateUrl = url + '/' + thisId; fetch(updateUrl, { method: 'DELETE', }).then((response) => { if(!response.ok) { console.log('Delete error!'); throw new Error('error'); } console.log('Delete ok!'); }).then(() => { thisLi.remove() }).catch((error) => { console.log(error); }); }; document.addEventListener('click', (e) => { if (e.target.className !=='doDelete') { return; } const thisLi = e.target.closest('li'); deleteFetch(thisLi); }, false);
ブロックごとに見ていく。
deleteFetch()
にもthisLi
の引数がある。
// Delete const deleteFetch = (thisLi) => { // 中略 };
deleteFetch()
の中身、冒頭は引数設定
const thisId = thisLi.dataset.id; const updateUrl = url + '/' + thisId;
- 変数
thisId
はthisLi
のデータ属性id
- 変数
updateUrl
はurl
と/
とthisId
先ほどのUpdateと同じくデータ属性からid番号を取得してURLに付け足している
fetch(updateUrl, { method: 'DELETE', }).then((response) => { if(!response.ok) { console.log('Delete error!'); throw new Error('error'); } console.log('Delete ok!'); }).then(() => { thisLi.remove() }).catch((error) => { console.log(error); });
fetch()
の第一引数はupdateUrl
第二引数はオプション、連想配列のmethod
キーの値はDELETE
- 二つ目の
then()
でthisLi
を削除
先ほどのPUT操作と同様、Fetch APIはDELETEもいけた!今回も他にオプションはなし。
then()
の処理、今回も少ないので直接書いた。thisLi
を削除するのみ。
doDelete()
関数のイベント
document.addEventListener('click', (e) => { if (e.target.className !=='doDelete') { return; } const thisLi = e.target.closest('li'); deleteFetch(thisLi); }, false);
document
上をクリックした時に無名関数を実行(引数はイベントのe
)- もし
doDelete
以外だった関数を抜ける - 変数
thisLi
はイベントターゲットの親要素のli
タグ deleteFetch()
関数を実行、引数はthisLi
先ほどのUpdateとほとんど同じ構成。deleteFetch()
関数は後述
JSコード(DOM操作)
先ほど、後述と書いたいくつかのDOM操作系の関数。処理が多かったので外に切り出した。API通信のメインではないのでざっと触れる
ボタンの追加
appendBtn()
関数(引数は左からclassName
、text
)
// Append Button const appendBtn = (className, text) => { const btn = document.createElement('button'); btn.className = className; btn.innerHTML = text; return btn; };
この関数でbuttonタグを作って引数className
からクラス名、引数text
からテキストを設定している。
リストの追加
appendList()
関数(引数はthisData
)
// Append List const appendList = (thisData) => { const li = document.createElement('li'); li.dataset.id = thisData.id; li.innerHTML = thisData.ikku; const updateBtn = appendBtn('doUpdate', '修正'); li.appendChild(updateBtn); const deleteBtn = appendBtn('doDelete', '削除'); li.appendChild(deleteBtn); ikkulist.appendChild(li); };
liタグを作成し、引数thisData
からデータ属性id
やテキストを設定している。thisData
は無名関数(オブジェクト)型を想定しており、キー名から値を取得している。
li
でappendChild()
を2回実行し、appendBtn()
関数で作った修正ボタン、削除ボタンを子要素に追加している。
最後にikkulist
の子要素にli
を追加している。
修正欄のinputタグを追加
ここから先は「修正」ボタンを押した時に現れる修正欄
appendUpdateInput()
関数(引数はthisIkku
)
// 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; };
inputgタグを作成。text
タイプ、neme
やclass名はupdateIkku
など属性を設定。入力値は引数thisIkku
これによって修正欄に最初から元の一句が入った状態になる。
修正欄の送信ボタンを追加
appendUpdateBtn()
関数(引数はなし)
const appendUpdateBtn = () => { const btn = document.createElement('input'); btn.type = 'button'; btn.value = '送信'; btn.className = 'updateBtn'; return btn; };
buttonタイプのinputタグを追加。値は送信に。class名はupdateBtn
に。
修正欄を追加
appendUpdateArea()
関数(引数はthisLi
)
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); };
修正欄自体はブロックで一塊にしたいのでappendDiv
でdivタグを作成。
その中にappendUpdateInput()
とappendUpdateBtn()
でinputタグと送信ボタンを作るって追加。
変数thisIkku
の引数thisLi
のテキストはappendUpdateInput()
の引数に入れる。最後にthisLi
にdivタグを追加。
修正欄追加のイベント設定
appendUpdateArea()
のイベント設定
document.addEventListener('click', (e) => { if (e.target.className !=='doUpdate') { return; } const thisLi = e.target.closest('li'); if (thisLi.querySelector('.updateArea') === null) { appendUpdateArea(thisLi); } }, false);
これもdocumentにクリックイベントを設定し、doUpdate
のときだけ実行。
変数thisLi
はターゲットの親要素のliタグ
もしupdateArea
が存在しなければappendUpdateArea()
関数でthisLi
を追加。
updateArea
の存在チェックをしないと修正ボタンを押すたびに修正欄が追加されてしまった。
ブラウザとDBの動作
上記のコードがどう操作するか確認。なお、先ほども書いたようにローカル環境でのJSON Serverの動作になる。
Create:データの生成
「一句詠む」の入力欄に新しい一句を追記し、「送信」ボタンを押すと…
「過去の一句」に一句が追加される。
「db.json」にも追加された! id
番号「2」が自動に振られている。
{ "ikku": [ { "ikku": "JSや ああJSや JSや", "id": 1 }, { "ikku": "Reactや ああReactや Reactや", "id": 2 } ] }
Read:データの取得
Read設定が効いているため、ブラウザをリロードしても「過去の一句」の表示は変わらない。
データがDBと紐づいて永続化されている!(Read設定前は消えてしまった)
Update:データの更新
「過去の一句」上で修正したい一句の「修正」ボタンを押すと修正欄が表示される(あらかじめ一句が入っている)
これを打ち替えて修正すして、「送信」ボタンを押すと…
「過去の一句」に修正が反映される!(修正欄は非表示になる)
db.jsonにも修正が反映される
{ "ikku": [ { "ikku": "JSや ああJSや JSや", "id": 1 }, { "ikku": "Reactや 嗚呼Reactや Reactや", "id": 2 } ] }
Delete:データの削除
「過去の一句」で修正したい一句の「削除」ボタンを押すと…
一句が削除される!
db.jsonもデータが削除される!
{ "ikku": [ { "ikku": "JSや ああJSや JSや", "id": 1 } ] }
やった!CRUD操作成功〜♪
なお、コンソールには「OK」のメッセージしか表示されてないので通信エラーは起きていないことがわかる。
最後に
今回はとても手応えを感じる体験ができました!Fetch APIのCRUD処理、HTMLのformタグと違ってPUTやDELETEの送信もできたのは感動!!
JSON ServerにはあらかじめこうしたFetch APIからのリクエストに対してレスポンスを返すAPI機能が備わっているそうです。
JSON Serverでは、lowdbというlodash APIベースのデータベースライブラリを使用することで、リソースの更新処理にも対応できるようになってます。
具体的には、POST/PUT/DELETEメソッドを使用した場合、リソースに対する追加/更新/削除が行われます。
※参考:JSON Server使いこなし - モックサーバーの起動とリソース処理 | CodeGrid
「lodash」は自分はあまり馴染みがないのだけど、便利な関数がたくさん用意されているライブラリのようです。
※参考:【JavaScript】lodashの使い方 - Qiita
前回まで触れたようなMySQLやMongoDBなどの純粋なデータベースに対してFetch APIでリクエストする場合は、間でREST APIを実現するバックエンド側の処理を書く必要がありそうです。
以前、触っていたNode.jsのExpressでそうした処理ができそうなので、次はこれに取り組んでみたく思います。
それではまた!
※参考:ネイティブJSでいろいろやってみたシリーズ
qiita.com