MSBOOKS

プログラミングとか深層学習とか日常の出来事とか

【勉強メモ】JavaScriptを使用してASP.NET Core Web APIを呼び出すチュートリアルを試してみた

はじめに

前回の最後に行っていた、JavaScript を使用して ASP.NET Core Web API を呼び出すチュートリアルも勉強してみました。CSS、HTML、JavaScript に関する知識が必要らしいけど、ちょっと触ったことある程度だけど大丈夫かな?
docs.microsoft.com

JavaScriptでWeb APIを呼び出す

To Do アイテムを作成および管理するためのフォームが含まれる HTML ページを追加することが主にやることだそう。結構楽しそう。この前やったGETとかはイベント ハンドラによって、送信されるみたい。Fetch APIのfetch関数により、各HTTP要求を記述するそうだ。postmanで書いていたやつをプログラム的に書く方法だね。
”fetch 関数からは Promise オブジェクトが返され、このオブジェクトには Response オブジェクトとして表された HTTP 応答が含まれます。一般的なパターンでは、Response オブジェクトに対して json 関数を呼び出して、JSON 応答の本文を抽出します。”と書いてあって、まあつまり返り値でもらったものをJSONで解釈すれば、前回のような応答文が読み取れるって感じなのかな?

"1.静的ファイルを提供し、既定のファイルマッピングを有効にするように、アプリを構成します。 Startup.cs の Configure メソッドでは、次の強調表示されたコードが必要です。"って書いてあるけど、多分自分で作ったファイルを開いてもらうようにするための変更かな?
・Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace TodoApi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<TodoContext>(opt =>
               opt.UseInMemoryDatabase("TodoList"));
            services.AddControllers();

        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseDefaultFiles(); //追加
            app.UseStaticFiles(); //追加

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

2~6は言われたとおりに作成した。
・index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>To-do CRUD</title>
    <link rel="stylesheet" href="css/site.css" />
</head>
<body>
    <h1>To-do CRUD</h1>
    <h3>Add</h3>
    <form action="javascript:void(0);" method="POST" onsubmit="addItem()">
        <input type="text" id="add-name" placeholder="New to-do">
        <input type="submit" value="Add">
    </form>

    <div id="editForm">
        <h3>Edit</h3>
        <form action="javascript:void(0);" onsubmit="updateItem()">
            <input type="hidden" id="edit-id">
            <input type="checkbox" id="edit-isComplete">
            <input type="text" id="edit-name">
            <input type="submit" value="Save">
            <a onclick="closeInput()" aria-label="Close">&#10006;</a>
        </form>
    </div>

    <p id="counter"></p>

    <table>
        <tr>
            <th>Is Complete?</th>
            <th>Name</th>
            <th></th>
            <th></th>
        </tr>
        <tbody id="todos"></tbody>
    </table>

    <script src="js/site.js" asp-append-version="true"></script>
    <script type="text/javascript">
        getItems();
    </script>
</body>
</html>

・site.css

input[type='submit'], button, [aria-label] {
    cursor: pointer;
}

#editForm {
    display: none;
}

table {
    font-family: Arial, sans-serif;
    border: 1px solid;
    border-collapse: collapse;
}

th {
    background-color: #f8f8f8;
    padding: 5px;
}

td {
    border: 1px solid;
    padding: 5px;
}

この7のsite.jsが今回の主題であるfetch 関数のところだね。GETとかの命令がjavascriptで記述されているっぽい。
・site.js

const uri = 'api/TodoItems';
let todos = [];

function getItems() {
  fetch(uri)
    .then(response => response.json())
    .then(data => _displayItems(data))
    .catch(error => console.error('Unable to get items.', error));
}

function addItem() {
  const addNameTextbox = document.getElementById('add-name');

  const item = {
    isComplete: false,
    name: addNameTextbox.value.trim()
  };

  fetch(uri, {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(item)
  })
    .then(response => response.json())
    .then(() => {
      getItems();
      addNameTextbox.value = '';
    })
    .catch(error => console.error('Unable to add item.', error));
}

function deleteItem(id) {
  fetch(`${uri}/${id}`, {
    method: 'DELETE'
  })
  .then(() => getItems())
  .catch(error => console.error('Unable to delete item.', error));
}

function displayEditForm(id) {
  const item = todos.find(item => item.id === id);
  
  document.getElementById('edit-name').value = item.name;
  document.getElementById('edit-id').value = item.id;
  document.getElementById('edit-isComplete').checked = item.isComplete;
  document.getElementById('editForm').style.display = 'block';
}

function updateItem() {
  const itemId = document.getElementById('edit-id').value;
  const item = {
    id: parseInt(itemId, 10),
    isComplete: document.getElementById('edit-isComplete').checked,
    name: document.getElementById('edit-name').value.trim()
  };

  fetch(`${uri}/${itemId}`, {
    method: 'PUT',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(item)
  })
  .then(() => getItems())
  .catch(error => console.error('Unable to update item.', error));

  closeInput();

  return false;
}

function closeInput() {
  document.getElementById('editForm').style.display = 'none';
}

function _displayCount(itemCount) {
  const name = (itemCount === 1) ? 'to-do' : 'to-dos';

  document.getElementById('counter').innerText = `${itemCount} ${name}`;
}

function _displayItems(data) {
  const tBody = document.getElementById('todos');
  tBody.innerHTML = '';

  _displayCount(data.length);

  const button = document.createElement('button');

  data.forEach(item => {
    let isCompleteCheckbox = document.createElement('input');
    isCompleteCheckbox.type = 'checkbox';
    isCompleteCheckbox.disabled = true;
    isCompleteCheckbox.checked = item.isComplete;

    let editButton = button.cloneNode(false);
    editButton.innerText = 'Edit';
    editButton.setAttribute('onclick', `displayEditForm(${item.id})`);

    let deleteButton = button.cloneNode(false);
    deleteButton.innerText = 'Delete';
    deleteButton.setAttribute('onclick', `deleteItem(${item.id})`);

    let tr = tBody.insertRow();
    
    let td1 = tr.insertCell(0);
    td1.appendChild(isCompleteCheckbox);

    let td2 = tr.insertCell(1);
    let textNode = document.createTextNode(item.name);
    td2.appendChild(textNode);

    let td3 = tr.insertCell(2);
    td3.appendChild(editButton);

    let td4 = tr.insertCell(3);
    td4.appendChild(deleteButton);
  });

  todos = data;
}

実行してみたところ以下のような画面が見れた。
f:id:msteacher:20210806201136p:plain

To Do アイテムのリストの取得

HTTP GET 要求の話で、前にやった↓のやつですね。

  • GET /api/TodoItems
function getItems() {
    fetch(uri)
        .then(response => response.json())
        .then(data => _displayItems(data))
        .catch(error => console.error('Unable to get items.', error));
}

Web API から正常に返ってくると、_displayItems 関数が呼び出されるそうです。
_displayItems関数は最後の方にあって、↓のやつですね。

function _displayItems(data) {
    const tBody = document.getElementById('todos');
    tBody.innerHTML = '';

    _displayCount(data.length);

    const button = document.createElement('button');

    data.forEach(item => {
        let isCompleteCheckbox = document.createElement('input');
        isCompleteCheckbox.type = 'checkbox';
        isCompleteCheckbox.disabled = true;
        isCompleteCheckbox.checked = item.isComplete;

        let editButton = button.cloneNode(false);
        editButton.innerText = 'Edit';
        editButton.setAttribute('onclick', `displayEditForm(${item.id})`);

        let deleteButton = button.cloneNode(false);
        deleteButton.innerText = 'Delete';
        deleteButton.setAttribute('onclick', `deleteItem(${item.id})`);

        let tr = tBody.insertRow();

        let td1 = tr.insertCell(0);
        td1.appendChild(isCompleteCheckbox);

        let td2 = tr.insertCell(1);
        let textNode = document.createTextNode(item.name);
        td2.appendChild(textNode);

        let td3 = tr.insertCell(2);
        td3.appendChild(editButton);

        let td4 = tr.insertCell(3);
        td4.appendChild(deleteButton);
    });

    todos = data;
}

forEachとアロー関数が出てきてもう何がなんだかわからなくなってきました。forEachは配列の各要素に関して、個別に操作を行うことができる関数で、アロー関数(item=>はfunction(item){を短縮したものらしいです。data配列の中身を一個ずつアクセスしていて、それをitemとして取り出し、その中にはこの前やったid,name,isCompleteが入っているので、item.idのようにすることでアクセスできるようです。checkboxとedit、deleteを作って、1つの行を作ってindex.htmlのに追加しているっぽいです。最後にグローバル変数のtodosに追加しているって感じですね。Web API 要求が失敗した場合は、ブラウザのコンソールにエラーが表示されるっぽいですね。

To Do アイテムの追加

表示方法はわかったけど、まずはデータを入れないとイメージできないので、追加方法を見てみます。

  • POST /api/TodoItems

の部分ですね。
実行結果は↓
f:id:msteacher:20210806222040p:plain

function addItem() {
    const addNameTextbox = document.getElementById('add-name');

    const item = {
        isComplete: false,
        name: addNameTextbox.value.trim()
    };

    fetch(uri, {
        method: 'POST',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(item)
    })
        .then(response => response.json())
        .then(() => {
            getItems();
            addNameTextbox.value = '';
        })
        .catch(error => console.error('Unable to add item.', error));
}

”item 変数は、To Do アイテムのオブジェクト リテラル表現を構築するために宣言されています。”…?リテラルとは?調べたところ、"リテラルとは、プログラムに直接記述するデータ値のことです。コードに書いた値が、そのまま実行時にもその値のまま意味をもちます。"だそうで、おそらく今回jsonにして送るときに送りやすい形にしておくってイメージでしょうか。
fetchの中には

  • method— POSTを指定(postmanでは最初に選択してたやつ)
  • headers—Accept および Content-Type の HTTP 要求ヘッダーがapplication/json に設定され、それぞれ、受信および送信されるものとしてjsonを使っているってことですね。確かにpostmanでもjsonを指定した気がする。
  • body— 要求本文の JSON 表現が指定されていて、item をJSON.stringify関数(JavaScript のオブジェクトをJSON文字列に変換する関数)に渡すことによってJSON形式になって送信されるっぽいですね。

これによってHTTP POST 要求が api/TodoItems ルートに送信され、返ってきた値に対して、さっき見たgetItems()で画面表示がされているみたいです。

To Doアイテムの更新

この話ですね。更新できるってやつです。

  • PUT /api/TodoItems/{id}

{id}に、更新するアイテムのidが指定されます。api/TodoItems/1 のようになるそうです。

function updateItem() {
    const itemId = document.getElementById('edit-id').value;
    const item = {
        id: parseInt(itemId, 10),
        isComplete: document.getElementById('edit-isComplete').checked,
        name: document.getElementById('edit-name').value.trim()
    };

    fetch(`${uri}/${itemId}`, {
        method: 'PUT',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(item)
    })
        .then(() => getItems())
        .catch(error => console.error('Unable to update item.', error));

    closeInput();

    return false;
}

さっきのGETとほぼおんなじですがmethodは、PUTになっていますね。また、fetchのところみると、`${uri}/${itemId}`になっていてちゃんとidが送られています。itemIdはどっからきてるのかなーって見てみたら、あるtodoのEditボタンが押されたときに、displayEditForm関数でそのtodoのidがindex.htmlののところへ代入されているんですね。
editボタンを押すと、displayEditForm関数で新しい修正用フォームが以下のように表示されます。
f:id:msteacher:20210806223445p:plain
試しにnameをNakasu Kasumiに変更してみました。
f:id:msteacher:20210806223510p:plain

To Do アイテムの削除

DELETEのやつですね。指定したのを消せるやつ。

  • DELETE /api/TodoItems/{id}

さっきのPUTとほぼ同じで、method オプションを DELETE に設定して、最後にidをつけるだけですね。

function deleteItem(id) {
    fetch(`${uri}/${id}`, {
        method: 'DELETE'
    })
        .then(() => getItems())
        .catch(error => console.error('Unable to delete item.', error));
}

Deleteのときは特にjsonの送信がないので、簡単ですね。idはdeleteボタンのdeleteButton.setAttribute('onclick', `deleteItem(${item.id})`)で指定していました。削除したら、再度GETして表示データを更新しています。
実行結果↓
f:id:msteacher:20210806224133p:plain

IsCompleteは動いてる?

これで基本の関数の説明は終わりましたが、あれIsCompleteってやってないけど、ただの飾りなんじゃね?って思ってしまいました。そしたらeditでこのチェックボックスが入れられる事に気づき、ちゃんと保存されているかを確かめるために↓のように新たにtodoを追加してIsCompleteにチェックを入れてみました。
f:id:msteacher:20210806224734p:plain

データベースに保存されていれば、この画面を開き直してもIsCompleteがチェックされた状態ででてくるはずです。ブラウザを開き直したところ
f:id:msteacher:20210806224856p:plain

ちゃんと保存されていました。疑ってごめんなさい。

おわりに

.net Coreをjavascriptでweb上で操作できるようになりました。より実際のアプリに近いものを作ることができたので勉強になりました。
ただちょっとチュートリアルが日本語が微妙に直訳感があって読みにくかったです…。でもソースコードは世界共通ってこともあって、コードを読めば何をしているかなんとなくわかるのは改めてすごいと思いましたね。
また何か勉強したら共有したいと思います。それではまた。