クモのようにコツコツと

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

【JS】Fetch APIを使ってJSON ServerにCRUDする

以前、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送信!
f:id:idr_zz:20200731200046j:plain ただし、ダミーの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

「過去の一句」にはあらかじめ一句が表示されている。 f:id:idr_zz:20200813175157j:plain

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';
  • 変数postIkkupostBtnikkulistは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
  • 変数dataikkuキーをpostIkkuvalue属性に

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で配列datai番目を取得
    appendList()関数を実行、引数はthisData

fetch()に第二引数のオプションがないのは以下の2つの理由

  • fetch()はGET送信が標準で、Read操作もGETのため
  • headersbodyに送信するリクエスト情報もない

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
    };
  • 変数thisIdthisLiのデータ属性id
  • 変数updateUrlurl/thisId
  • 変数updateAreathisLi.updateAreaセレクタ
  • 変数updateIkkuthisLi.updateIkku'セレクタの.value属性
  • 変数dataは連想配列でikkuキーの値はupdateIkku

今回はURLにid番号を追加してデータを特定している。その番号はデータ属性から得ている。

dataikkuキーに入れたい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の直下のテキストをdataikku
    thisLiupdateAreaを削除

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;
  • 変数thisIdthisLiのデータ属性id
  • 変数updateUrlurl/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()関数(引数は左からclassNametext

// 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は無名関数(オブジェクト)型を想定しており、キー名から値を取得している。

liappendChild()を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:データの生成

「一句詠む」の入力欄に新しい一句を追記し、「送信」ボタンを押すと… f:id:idr_zz:20200813175200j:plain

「過去の一句」に一句が追加される。 f:id:idr_zz:20200813175204j:plain

「db.json」にも追加された! id番号「2」が自動に振られている。

{
  "ikku": [
    {
      "ikku": "JSや ああJSや JSや",
      "id": 1
    },
    {
      "ikku": "Reactや ああReactや Reactや",
      "id": 2
    }
  ]
}

Read:データの取得

Read設定が効いているため、ブラウザをリロードしても「過去の一句」の表示は変わらない。 f:id:idr_zz:20200813175207j:plain

データがDBと紐づいて永続化されている!(Read設定前は消えてしまった)

Update:データの更新

「過去の一句」上で修正したい一句の「修正」ボタンを押すと修正欄が表示される(あらかじめ一句が入っている) f:id:idr_zz:20200813175211j:plain

これを打ち替えて修正すして、「送信」ボタンを押すと… f:id:idr_zz:20200813175214j:plain

「過去の一句」に修正が反映される!(修正欄は非表示になる) f:id:idr_zz:20200813175217j:plain

db.jsonにも修正が反映される

{
  "ikku": [
    {
      "ikku": "JSや ああJSや JSや",
      "id": 1
    },
    {
      "ikku": "Reactや 嗚呼Reactや Reactや",
      "id": 2
    }
  ]
}

Delete:データの削除

「過去の一句」で修正したい一句の「削除」ボタンを押すと… f:id:idr_zz:20200813175217j:plain

一句が削除される! f:id:idr_zz:20200813175220j:plain

db.jsonもデータが削除される!

{
  "ikku": [
    {
      "ikku": "JSや ああJSや JSや",
      "id": 1
    }
  ]
}

やった!CRUD操作成功〜♪

なお、コンソールには「OK」のメッセージしか表示されてないので通信エラーは起きていないことがわかる。

f:id:idr_zz:20200813205003j:plain

最後に

今回はとても手応えを感じる体験ができました!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