【ポケモン】JavaScriptを使った非同期通信について

# はじめに
こちらの記事ではWEB開発における非同期通信について理解しやすいように手を動かして作れるアプリを作りました。
非同期通信について学んでみたい人はぜひ作ってみてください。

# 対象者
非同期通信とはなにかわからない、または名前だけ知っている、漠然としたイメージだけあるといった方
WEB開発に関する知識はある程度持っている方

# 非同期通信とは
非同期通信とは通信技術の一つで送信者のデータ送信タイミングと受信者のデータ受信タイミングを合わせずに通信を行う通信方式です。
イメージが湧きにくいと思うので以下の画像を見てください。

![ajax.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3638023/e6da0d41-8a86-7ee6-a3f9-2fc4f5e8e072.gif)

この処理ではボタンをクリックした後、画面遷移を行わずにデータベースへアクセスしランダムなポケモン情報を画面に表示しています。
非同期通信では処理は以下のように行われています。
1. 送信ボタンをクリックするとJavaScriptにてhttpリクエストが送信され、サーバーサイドでDBへの検索処理が実行される。
2. 待機(この間、JavaScriptは処理を行っていない)
3. 検索処理が完了して結果が返却されたことをキッカケにしてJavaScriptで画面要素の更新処理が変更される。

このように手順1と3が独立して動いているため間にサーバーサイドの処理が入り、処理に時間がかかったとしても後続の処理に影響を与えずに実行することができています。

この技術は主にこのような**画面要素を一部だけ更新する**ような処理を実行するために使用されます。
また、「いいね」ボタンの機能として画面遷移をせずにデータベースのデータを更新するような機能などでも使われています。

他にも利用方法は多く、よく使われる技術なのでここから実際に動かしてみて動きを確認していきましょう。

# 実装
ここからは先程のサンプルの通りポケモン情報を取得することができる機能実装していきながら非同期通信の流れを確認していきましょう。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Random Pokemon Information</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
</head>
<body>
    <h1>ランダムなポケモンの情報</h1>
    <button id="fetchButton">ポケモンを取得</button>
    <script>
        $('#fetchButton').click(function() {
            console.log('ボタンをクリックしました');
        });
    </script>
</body>
</html>
```

まずはhtmlとJavaScriptの基本的なコードを作っていきます。
これで画像のようにボタンをクリックするとコンソールに文字が出力されることを確認してください。

![console1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3638023/2ab7cc1a-0213-2998-0f96-48a12bb2f137.png)

このように処理の最小単位で一度確認を挟むことで「少なくともボタンのクリックまでは正常にできている」と確認できるので後々バグが発生した時にこれ以降の部分に原因を特定することができますね。

そうしたらこのクリックイベントで非同期通信を行っていきます。
コードを下記のように編集します。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Random Pokemon Information</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
</head>
<body>
<h1>ランダムなポケモンの情報</h1>
<button id="fetchButton">ポケモンを取得</button>
<script>
    $('#fetchButton').click(function() {
        const pokemonId = 1;
        $.ajax({
            url: `https://pokeapi.co/api/v2/pokemon/${pokemonId}/`,
            dataType: 'json',
            success: function(data) {
                console.log(data);
            },
            error: function() {
                console.error('エラーが発生しました');
            }
        });
    });
</script>
</body>
</html>
```

この```ajax()```の部分が非同期通信処理になります。
引数としてオブジェクトを渡しますが各項目について解説していきます。

### url
urlはアクセスするサーバーサイドの処理のURLを記述します。
ご自身でPHPなどの処理を記述した場合はこちらにそのURLを書くことで意図したプログラムを実行することができます。
今回はPokeAPIという無料で公開されているAPIにリクエストを送信していきます。

https://pokeapi.co/

使い方はこちらに記載されていて誰でも登録不要で無料でポケモンの各種情報を取得することができるようになっています。
今回の```https://pokeapi.co/api/v2/pokemon/${pokemonId}/``` では指定したポケモンIDに該当するポケモンの情報を取得することができます。
まずはid 1番のポケモンを取得してみましょう。

### dataType
dataTypeは、サーバーからのレスポンスとして期待されるデータの形式を指定するパラメータです。これにより、jQueryはサーバーから受け取ったデータを適切に処理し、JavaScriptで扱いやすい形に変換します。一般的に使用されるdataTypeには以下のようなものがあります。

#### json
JavaScript Object Notationの略で、属性-値のペアをテキスト形式で表したものです。Web開発においてデータ交換の標準的なフォーマットとして広く利用されています。dataTypeを'json'に設定すると、サーバーから返されたJSON形式の文字列は自動的にJavaScriptオブジェクトに変換されます。こちらが最も多く使われる形式かと思います。

#### html
HTML形式のデータを期待する場合に指定します。この場合、サーバーから返されたHTMLはそのままの形式で受け取られます。

#### text
テキスト形式のデータを期待する場合に指定します。サーバーからのレスポンスが単純なテキストである場合に適しています。

#### script
サーバーからのレスポンスをJavaScriptとして実行します。このdataTypeは、サーバーから動的にスクリプトをロードして実行したい場合に使用されます。

#### xml
XML形式のデータを期待する場合に指定します。XMLはマークアップ言語で、データを階層的に表現するために用いられます。

:::note warn
非同期通信を行う際にdataTypeを適切に指定することは、レスポンスデータを正しく処理し、エラーを防ぐために非常に重要です。
:::
Web APIからデータを取得する場合、ほとんどのAPIはJSON形式でデータを返すため、'json'を指定することが一般的です。
今回の例であるPokeAPIもJSON形式でデータを返すため、dataType: 'json'を指定しています。

### data
今回の例では存在していませんがよく使うので解説します。
```javascript
data: {
    key1: "value1",
    key2: "value2"
}
```
dataはこのようにオブジェクト型で指定します。
リクエストを送信する対象のURLに対して GETの場合はクエリストリングとして。 POSTの場合はリクエストボディとして送信されます。
例えばLaravelで作られたプログラムにリクエストを送信する場合

```php
public function method(Request $request)
{
  ...
}
```

このようになることが多いかと思いますが```data```で送信したデータが```$request```として値を参照することができるので引数を渡す必要がある場合に使用します。
今回はURLの末尾に直接 pokemonIdを指定しているため不要です。
この辺りは使用するapiの仕様を確認して適切に設定してください。

### type
こちらも今回は使用されていませんが下記のように設定することでhttpリクエストをPOSTに変更することができます。
```javascript
type: "POST"
```
デフォルトではGETになっていますのでこちらも適宜変更してください。
今回のPokeAPIではGETでリクエストしていきます。

### success
これはリクエストが成功した時に実行されるコールバック関数を指定します。
```javascript
success: function(data, textStatus, jqXHR) {
    // 成功時の処理
}
```
このような形式で定義され、レスポンスデータ、ステータステキスト、jqXHRオブジェクトの3つの引数を受け取る事ができます。
今回の例ではdataだけを受け取っています。

### error
こちらはリクエストが失敗した時に実行されるコールバック関数を指定します。
```javascript
error: function(jqXHR, textStatus, errorThrown) {
    // エラー時の処理
}
```
こちらもjqXHRオブジェクト、ステータステキスト、そして投げられた例外の3つの引数を受け取ることができます。
今回はconsoleにエラー出力だけを行っています。


説明が長くなりましたが、これでリクエストが成功した場合レスポンスデータであるdataがコンソールに出力されるはずです。
実際に確認してみましょう。

![bulbasaur.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3638023/ef256ece-0a00-81da-58fa-7c57163cb9ab.png)

画像のように表示されていれば成功です。

```name: "bulbasaur" ``` と書かれていますがこれは英語名のフシギダネを表しています。

このようにしてJavaScriptからサーバーサイドへリクエストを送信し、レスポンスを受け取り次第consoleに出力することができました。

# fetch APIについて
今回非同期通信を実行するのに```ajax()```というメソッドを使用しましたが、これはjQueryのメソッドになります。
古くから使用されており現場ではまだまだjQueryやAjaxを使用することが多いので今回はこちらで解説を進めていきます。
ただしjQueryを使用しない場合などでも非同期通信自体は可能で他にも色々なメソッドが存在していますので調べてみてください。

素のJavaScriptの場合は2015年あたりから台頭している```fetch()```を使うことが多いかと思いますので一応その場合の書き方も紹介しておきます。
```javascript
document.getElementById('fetchButton').addEventListener('click', function() {
    const pokemonId = 1;
    fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonId}/`)
        .then(response => {
            if (!response.ok) {
                throw new Error('ネットワークレスポンスが異常です');
            }
            return response.json();
        })
        .then(data => {
            console.log(data);
        })
        .catch(error => {
            console.error('エラーが発生しました:', error);
        });
});
```
fetch()の場合responseにはPromiseクラスのオブジェクトを返却するのでまずはresponseが成功しているかどうかをこちらで確認する必要があります。
その後、responseが成功していた場合の処理を更に繋げて記述していきます。
また、失敗していた場合はthrowを使って例外を投げているのでcatchを使って処理を続けていきます。

こちらの方が新しい技術で実装時もメリットが多いと思いますので技術選定が可能な場合は積極的に利用してもいいかと思います。
ただし、基本的には既存のコードを改修する場合は現行の使用技術を踏襲するのが無難です。


### Promiseについて
Promiseとは複数の非同期処理を同時に実行したり、処理順が逆になると困る処理を順次実行したりといったことをサポートするためのクラスです。
Promiseは「保留」「履行」「拒否」という3つのステータスのどれかを持っています。
```fetch()```の戻り値はPromiseと解説しましたが、この戻り値が履行ステータスに変わったタイミングでthen以下の処理が実行される仕組みになっています。
そして```then()```の戻り値もPromiseなのでthenが```return response.json();```にてjsonを返却するとthenの戻り値も履行ステータスとなり更に下の処理が実行されます。

このようにしてthenを数珠繫ぎにすることで正しい順番で処理が実行されていく仕組みになっています。
これは非同期通信のようにサーバーの処理速度が不明瞭な処理を複数同時に実行したりする場合により効果を発揮します。


### 画面表示
非同期通信自体の説明としては以上になります。
ここから先はJavaScriptの処理を使ってこのデータを画面に追加表示していきます。
非同期通信を使って得た情報を具体的に使用する方法になります

```html
    <h1>ランダムなポケモンの情報</h1>
    <button id="fetchButton">ポケモンを取得</button>
    <div id="pokemonInfo">
        <h2 id="pokemonName"></h2>
        <img id="pokemonImage" src="https://pokeapi.co/static/pokeapi_256.3fa72200.png" alt="ポケモンの画像" width="200">
        <p id="pokemonTypes"></p>
        <p id="pokemonWeight"></p>
    </div>
```
まずは画面にこのように要素を用意しておきましょう。
こちらに取得したポケモンの情報を代入していきます。

```html
<script>
$('#fetchButton').click(function() {
    const pokemonId = 1;
    $.ajax({
        url: `https://pokeapi.co/api/v2/pokemon/${pokemonId}/`,
        dataType: 'json',
        success: function(data) {
            console.log(data);
            // ポケモンの名前
            $('#pokemonName').text(data.name);
            // ポケモンの画像
            $('#pokemonImage').attr('src', data.sprites.front_default);
            // ポケモンの種類
            const types = data.types.map(type => type.type.name).join(', ');
            $('#pokemonTypes').text(`タイプ: ${types}`);
            // ポケモンの重さ
            $('#pokemonWeight').text(`重さ: ${data.weight / 10} kg`);
        },
            error: function() {
            console.error('エラーが発生しました');
        }
    });
});
</script>
```

スクリプトタグの中をこのように編集してください。

![console2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3638023/917d9c39-9461-f85a-2b40-a44ca063dd0b.png)

consoleを見ての通り、ポケモンの各情報がdataに含まれていますのでこちらの各データを指定して画面上の要素に出力していきます。

この他にも多くのデータが含まれていますので必要に応じて表示してみてください。
データの構造については公式のドキュメントを参照してください。

https://pokeapi.co/docs/v2#pokemon

ここまでできましたら画面表示してみましょう。

![english_view.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3638023/62dac003-416e-179e-7c70-e87c3468f194.png)

無事に画面に情報を出すことができましたね。
この通りに表示されていない場合はコードを確認してみてください。

しかしこのままだと名前やタイプが英語表記になっていますね。
次はこれを日本語化していきましょう。

先程の情報には日本語名が含まれておらず別途取得する方法が用意されています。

```html
<script>
$('#fetchButton').click(function() {
    const pokemonId = 1;
    $.ajax({
        url: `https://pokeapi.co/api/v2/pokemon/${pokemonId}/`,
        dataType: 'json',
        success: function(data) {
            console.log(data);
            ////////////////////////////////////////////////
            // ポケモンの名前
            $.ajax({
                url:data.species.url,
                dataType: 'json',
                success: function(nameData){
                    console.log(nameData);
                    const pokemonName = nameData.names.find(name => name.language.name === 'ja').name
                    $('#pokemonName').text(pokemonName);
                }
            })
            ////////////////////////////////////////////////
            // ポケモンの画像
            $('#pokemonImage').attr('src', data.sprites.front_default);
            ///////////////////////////////////////////////
            // ポケモンの種類
            const typesPromises = data.types.map(type => $.ajax({
                url: type.type.url,
                dataType: 'json'
            }));

            Promise.all(typesPromises).then(typeDataObj => {
                console.log(typeDataObj);
                const typesCsv = typeDataObj.map(typeData => {
                    const typeName = typeData.names.find(name => name.language.name === 'ja');
                    return typeName ? typeName.name : '不明';
                }).join(', ');
                $('#pokemonTypes').text(`タイプ: ${typesCsv}`);
            });
            ///////////////////////////////////////////////
            // ポケモンの重さ
            $('#pokemonWeight').text(`重さ: ${data.weight / 10} kg`);
        },
            error: function() {
            console.error('エラーが発生しました');
        }
    });

    $.ajax({
        url: `https://pokeapi.co/api/v2/pokemon-species/${pokemonId}/`,
        dataType: 'json',
        success: function(speciesData) {
            const pokemonNameInJapanese = speciesData.names.find(name => name.language.name === 'ja').name;
            $('#pokemonName').text(pokemonNameInJapanese);
        }
    });
});
</script>
```

名前とタイプの取得部分をこのように変更しました。

![console3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3638023/5619b20b-02a2-536d-35dd-932a1cf5165e.png)

こちらの画像のように名前やタイプにはそれぞれをより詳細に取得することができるapiのURLが含まれています。
こちらに追加でリクエストを行いデータを取得していきます。

![console4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3638023/9ab75cee-2357-c891-15b5-dbeeb3de0f9d.png)

取得したデータには多くの情報が含まれていますが、namesの中から日本語のデータを検索して取得しています。

```typeData.names[9]```のように指定することもできますが、apiの追加変更があった場合でも対応しやすいので```ja```をキーにして選択するのがおすすめです。

またtypeの取得では 先程解説したPromiseクラスの```all()```が使われています。
これにより、```typesPromises```に格納された複数のajax処理を全て同時に実行開始し、全てのajax処理が履行ステータスになったことを確認してからタイプの画面表示処理が開始されています。
今回は「くさ」「どく」のそれぞれの日本語名を取得するapiを同時に実行して値が揃い次第表示するという流れになっています。
こうすることでどちらか片方のデータが不足している状態で処理が開始されることを防ぐことができますね。

これで日本語で全ての情報を取得することができるようになりましたね。

最後にサンプルのようにランダムなポケモン情報を取得するようにしてみましょう。
```const pokemonId = 1;```こちらで指定したIDのポケモンが表示されるので、ここにポケモンの数のランダムなIDを指定するように変更していきます。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Random Pokemon Information</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
</head>
<body>
    <h1>ランダムなポケモンの情報</h1>
    <button id="fetchButton">ポケモンを取得</button>
    <div id="pokemonInfo">
        <h2 id="pokemonName"></h2>
        <img id="pokemonImage" src="https://pokeapi.co/static/pokeapi_256.3fa72200.png" alt="ポケモンの画像" width="200">
        <p id="pokemonTypes"></p>
        <p id="pokemonWeight"></p>
    </div>

    <script>
        $('#fetchButton').click(function() {
            const pokemonId = Math.floor(Math.random() * 898) + 1;
            $.ajax({
                url: `https://pokeapi.co/api/v2/pokemon/${pokemonId}/`,
                dataType: 'json',
                success: function(data) {
                    // ポケモンの名前
                    $.ajax({
                        url:data.species.url,
                        dataType: 'json',
                        success: function(nameData){
                            const pokemonName = nameData.names.find(name => name.language.name === 'ja').name
                            $('#pokemonName').text(pokemonName);
                        }
                    })
                    // ポケモンの画像
                    $('#pokemonImage').attr('src', data.sprites.front_default);
                    // ポケモンの種類
                    const typesPromises = data.types.map(type => $.ajax({
                        url: type.type.url,
                        dataType: 'json'
                    }));

                    Promise.all(typesPromises).then(typeDataObj => {
                        const typesCsv = typeDataObj.map(typeData => {
                            const typeName = typeData.names.find(name => name.language.name === 'ja');
                            return typeName ? typeName.name : '不明';
                        }).join(', ');
                        $('#pokemonTypes').text(`タイプ: ${typesCsv}`);
                    });
                    // ポケモンの重さ
                    $('#pokemonWeight').text(`重さ: ${data.weight / 10} kg`);
                },
                error: function() {
                    console.error('エラーが発生しました');
                }
            });
        });

    </script>
</body>
</html>
```

これがサンプルの全文になります。
最後は```console.log()```などは消しておきましょう。

# まとめ
これで非同期通信の解説は終了です。

実際の開発では同プロジェクト内のPHPなどのサーバーサイド言語で実装されたプログラムに対して非同期通信のリクエストを送信して利用することが多いと思いますが、基本的には今回のような

1. 指定のURLにリクエストを送信
2. サーバーサイドの処理
3. 結果をjson形式で返却
4. JavaScriptがjsonをオブジェクトとして利用して画面要素の更新などを行う

という流れになります。

# 最後に
記事を読んでくださった方は、是非弊社開発課のXもフォローしてください。
毎日エンジニアに向けた情報発信を行っています。

https://twitter.com/tech_cin