【JavaScript】Fetch で Ajax を実装するサンプル（Promise と async / await）

# はじめに

ネイティブ JavaScript で Ajax を実装するには、[XMLHttpRequest](https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest) よりも、[Fetch](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API/Using_Fetch) の方が一般的なようです。
学習がてら、Fetch による Ajax 処理の基本的なところをまとめておきます。

[POSTメソッドの動作確認](#3-post-メソッドフレームワークを使用した例)は、[Rails 6](https://railsguides.jp/6_0_release_notes.html) を使用していますが、どのフレームワークでも基本は変わらないと思います。

# 1. GET メソッド

GET メソッドは、HTML ファイルと Javascript ファイルだけで簡単に動作確認ができます。
サンプルコードはコピペで動くようにしています。

## 1-1. fetch を実装する（then - catch - finally の形式）

気象庁の天気予報データ（JSON形式）を取得するだけであれば、次のとおりです。
fetch メソッドの第１引数には `URL`、第２引数には `オプション` を指定しますが、第２引数は省略することができます（デフォルトで GET メソッドとなります）。

```sample1.js
window.addEventListener('load', function() {
  fetch('https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json')
    .then(function(response) {
      return response.json();
    })
    .then(function(data) {
      return console.log(data);
    });
});
```

アロー演算子を使って書くと、次のように、よりシンプルになります。

```sample2.js
window.addEventListener('load', function() {
  fetch('https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json')
    .then(response => response.json())
    .then(data => console.log(data));
});
```

コンソール画面を見ると、JSON データが取得できているのがわかります。
![スクリーンショット 2021-12-13 20.54.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/8069555d-47c9-ea5a-2251-3fa6d262e478.png)

### ソースコードの説明

タイトルに「then - catch - finally」と書いていますが、この例では、catch メソッド、finally メソッドは使用していません。

fetch メソッドは返り値として、[Promise](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise) というものを返します（Promise は、非同期処理の完了を待って結果を返すオブジェクトです）。
正確性は置いておいて、コードの内容を順に見ると、おおよそ次のようなイメージです。

**① `fetch(url)`**
第１引数に URL を指定。
第２引数を省略すると、GET メソッドで HTTP リクエストが送信されます。

**② `.then(function(response) { return response.json() })`**
thenメソッドにより、①の処理が終了するまで待機。
引数の `response` に、①の fetch メソッドから返されたデータ（Promise）が入る。
`response.json()` でレスポンスデータを JSON 形式で読み取って返却する。

**③ `.then(function(data) { return console.log(data) });`**
then メソッドにより、②の処理が終了するまで待機。
引数の `data` に、②から返されたデータ（Promise）が入る。
`console.log(data)` でコンソール画面に表示する。

## 1-2. async / await で fetch を実装する

[async / await](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function) で書くと次のようになります。

```sample3.js
window.addEventListener('load', function() {
  (async () => {
    const response = await fetch('https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json');
    const data = await response.json();
    console.log(data);
  })();
});
```

＜参考サイト＞
・[How to Use Fetch with async/await](https://dmitripavlutin.com/javascript-fetch-async-await/)

### ソースコードの説明

コードの内容を順に見ると、おおよそ次のようなイメージです。

**① `const response = await fetch(url);`**
await により、fetch メソッドによる処理が終わるまで次の処理に進まない。

**② `const data = await response.json();`**
await により、`response.json()` の処理が終わるまで次の処理に進まない。

**③ `console.log(data);`**
①、②の処理が終わるのを待って、`console.log(data)` でコンソール画面に表示する。

## 1-3. 例外処理を追加する

[fetch メソッドが成功したかどうかを明確に判定](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API/Using_Fetch#checking_that_the_fetch_was_successful)するには [Response.ok プロパティ](https://developer.mozilla.org/en-US/docs/Web/API/Response/ok)を使用します。
Response.ok プロパティは、レスポンスが成功したか否か（200–299の範囲のHTTPステータスか否か）を示す論理値（true / false）を返します。

### 1-3-1. 例外処理（then - catch - finally の形式）

例外処理（エラー処理）を行うには、`then - catch - finally` を使用して、次のように記述します。

```sample4.js
window.addEventListener('load', function() {
  fetch('https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json')
    .then(response => {
      if (!response.ok) {
        throw new Error('Network response was not ok');  // fetchが成功したかどうかの判定
      }
      return response.json()
    })
    .then(data => console.log(data))
    .catch(error => {
      alert(error);  // 例外（エラー）が発生した場合に実行
    })
    .finally(() => {
      console.log('finally');  // 処理結果の成否に関わらず実行
    })
});
```

### 1-3-2. async / await の例外処理

async/await で例外処理（エラー処理）を行うには、`try - catch - finally` を使用して、次のように記述します。

```sample5.js
window.addEventListener('load', function() {
  (async () => {
    try {
      const response = await fetch('https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json');
      if (!response.ok) {
        throw new Error('Network response was not ok');  // fetchが成功したかどうかの判定
      }
      const data = await response.json();
      console.log(data);
    } catch(e) {
      alert(e);  // 例外（エラー）が発生した場合に実行
    } finally {
      console.log('finally');  // 処理結果の成否に関わらず実行
    }
  })();
});
```

# 2. POSTメソッド（基本のサンプル）

次に、POST メソッドについてです。
まずは、基本的な骨組みだけのサンプルです。

## 2-1. 単純なサンプル（then - catch - finally の形式）

POST メソッドの Ajax を骨組みだけで書くと次のような感じです。
GET メソッドとの違いは、fetch の第２引数にオプションとして、`HTTPメソッド`、`リクエストヘッダ`、`リクエストボディ` などの情報を入れるところです。

```sample6.js
const options = {
  method: 'POST',  // HTTPメソッドを指定
  headers: {  // リクエストヘッダを追加
    'Content-Type': 'application/json',  // リクエストデータをJSON形式と指定
    'Accept': 'application/json'  // レスポンスデータをJSON形式と指定
  },
  body: JSON.stringify(data) // リクエストボディ（送信データ）を指定
}
fetch(url, options)  // サーバにリクエストを送信
  .then(response => response.json())  // レスポンスデータをJSON形式に
  .then(data => console.log(data));  // 取得したデータをコンソール画面に表示
```

上記の例は、クライアントから JSON 形式のデータを送信して、サーバからも JSON 形式のデータを受け取る場合になります。
送受信するデータの形式に応じて、リクエストヘッダに追加する情報なども異なってきます。

ここで使用している `method`、`headers`、`body` 以外のオプションについては「[リクエストにオプションを適用する](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API/Using_Fetch#supplying_request_options)」を参照してください。

## 2-2. async / await を使用した単純なサンプル

async / await を使って書くと次のような感じです。

```sample7.js
(async () => {
  const options = {
    method: 'POST',  // HTTPメソッドを指定
    headers: {  // リクエストヘッダを追加
      'Content-Type': 'application/json',  // リクエストデータをJSON形式と指定
      'Accept': 'application/json'  // レスポンスデータをJSON形式と指定
    },
    body: JSON.stringify(data) // リクエストボディ（送信データ）を指定
  }
  const response = await fetch(url, options);  // サーバにリクエストを送信
  const data = await response.json();  // レスポンスデータをJSON形式に
  console.log(data);  // 取得したデータをコンソール画面に表示
})()
```

＜参考記事＞
・[Proper Way to Make API Fetch 'POST' with Async/Await](https://stackoverflow.com/questions/50046841/proper-way-to-make-api-fetch-post-with-async-await)

これだけでは実際的ではないので、以下、具体的に動作するサンプルコードを書いておきます。

# 3. POST メソッド（フレームワークを使用した例）

POST メソッドの動作確認には、Rails6 を使用しました。

＜実行環境＞
Rails 6.1.4
ruby 3.0.3

## 3-1. Rails6 の準備

Rails アプリケーション作成方法は「[RubyとRailsを最新版にして新規アプリケーションを作成する](https://qiita.com/t-yama-3/items/ca14ea17b00a003bae55)」を参照してください。
Rails6 では、Rails5 から変更になっている部分があったので、その点を簡単に触れておきます。

### 3-1-1 . Rails6 で JavaScript を使用する

Rails6 では JavaScript ファイルの取扱いが変更になっていましたので備忘として書いておきます。
本記事の趣旨とは離れるので折りたたんでおきます。

<details><summary><font color="blue">詳細を表示</font></summary><div>

`rails new` を実行すると、`app` ディレクトリ直下に `javascript` というフォルダが作成されています。
このフォルダの直下に、`main.js` ファイルを作成します。

```app/javascript/main.js
// 動作確認用のコード
window.addEventListener('load', function() {
  console.log('Hello!');
});
```

このままでは、JavaScript ファイルを読み込みません。
次のように、javascript フォルダの `packs` フォルダ内にある `application.js` ファイルに、`import "main.js"` と追加します。

```app/javascript/packs/application.js
import Rails from "@rails/ujs"
import Turbolinks from "turbolinks"
import * as ActiveStorage from "@rails/activestorage"
import "channels"
import "main.js"  // ここに追加

Rails.start()
Turbolinks.start()
ActiveStorage.start()

```

一応、以上で JavaScript ファイルを読み込ませることができます。

詳細は、以下の記事を参考にしてください（Rails6 のバージョンにより若干書き方が異なりますので、うまく読み替えてください）。

＜参考記事＞
・[Ruby on RailsにおけるJavascriptファイルの取り扱い(Rails6)](https://nobunobu1717.site/?p=1292)
・[How to require custom JS files in Rails 6](https://stackoverflow.com/questions/56242664/how-to-require-custom-js-files-in-rails-6)
</div></details>

### 3-1-2. Rails6 で Turbolinks を無効にする

Turbolinks は、画面遷移を高速化するライブラリで、自動で非同期通信（Ajax）まで行ってくれます。
ここでは、動作確認の邪魔になってしまうので、この Turbolinks を無効化しておきます。
この内容も、本記事の趣旨とは離れるので折りたたんでおきます。

<details><summary><font color="blue">詳細を表示</font></summary><div>

**① gemを削除する**
Gemfile から `turbolinks` の gem を探して、削除（コメントアウト）します。
手元の環境では、`gem 'turbolinks', '~> 5'` の部分をコメントアウトしています（以下）。

```:Gemfile（抜粋）
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
# gem 'turbolinks', '~> 5'  # ここを削除（コメントアウト）
```

コメントアウトしたら、ターミナルで、 `bundle install` （または、`bundle update`）で gem を更新して turbolinks を削除します。

```
$ bundle install
```

**② JavaScript の設定ファイルから関係部分を削除する**
app/javascript/packs フォルダにある`application.js` ファイルから、Turbolinks を読み込んでいる部分を削除（コメントアウト）します。以下の２行分の削除が必要です。

```app/javascript/packs/application.js
import Rails from "@rails/ujs"
// import Turbolinks from "turbolinks"  // ここを削除（コメントアウト）
import * as ActiveStorage from "@rails/activestorage"
import "channels"
import "main.js"

Rails.start()
// Turbolinks.start()  // ここを削除（コメントアウト）
ActiveStorage.start()
```

**③ View の設定ファイルから関係部分を削除する**
app/views/layouts フォルダにある `application.html.erb` ファイルから、Turbolinks を読み込んでいる部分を削除（コメントアウト）します。
`<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>` の１行です。

```erb:app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>AjaxSample</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%# <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>
```

**④ 個別の View ファイルでの記載**
あとは、個別の View ファイルの `form_with` タグのオプションで、`local: true` と記載すれば OK です。

```erb:sample.html.erb
<%= form_with(model: @sample, local: true) do |form| %>
  <%# 省略 %>
<% end %>
```

＜参考記事＞
[Rails Turbolinksをページ毎に無効化する方法](https://qiita.com/keitah/items/05f02efe4e11ab2473e8)
</div></details>

## 3-2. 関連ファイルの記載

動作確認に使用する、ビュー（HTML）、コントローラーのサンプルコードも載せておきます。
本記事の趣旨とは離れるので、これも折りたたんでおきます。

<details><summary><font color="blue">詳細を表示</font></summary><div>

**① View ファイル**

```erb:app/views/samples/index.html.erb
<h2>Sample</h2>
<%= form_with(model: @sample, id: "sample_input", local: true) do |form| %>
  <%= form.text_field :memo, class: "sample_form-text" %>
  <%= form.submit "登録", class: "sample_form-btn" %>
<% end %>
<div class="samples">
  <% @samples.each do |sample| %>
    <div>
      <span><%= sample.memo %></span>
    </div>
  <% end %>
</div>
```

**② Controller ファイル**

```app/controllers/samples_controller.rb
class SamplesController < ApplicationController
  # トップページ
  def index
    @samples = Sample.all  # Sampleテーブルのデータ全てを取得したインスタンス
    @sample = Sample.new  # Sampleテーブルの新しいレコード格納用インスタンス
  end

  # データの登録
  def create
    @sample = Sample.new(sample_params)
    if @sample.save
      respond_to do |format|
        format.html { redirect_to root_path }  # 通常のリクエストの場合
        format.json { render json: { memo: @sample.memo, id: @sample.id } }  # Ajaxによるリクエストの場合
      end
    end 
  end

  # パラメータの受取り
  private
  def sample_params
    params.require(:sample).permit(:memo)
  end
end
```

**③ ルーティング**

```config/routes.rb
Rails.application.routes.draw do
  root to: 'samples#index'  # ルートパスに"/samples"を指定
  resources :samples  # samples関連のルーティングを設定
end
```
</div></details>

## 3-3. 実装する画面

サンプルとして実装するのは、下図のように文字列を入力して送信すると、テキストボックスの下に文字列が表示されるだけの簡単な内容です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/44c26535-1978-0dfa-b40d-6e3f9893b556.png" width="502">
送信ボタンを押すと、ページがリロードせずに文字列が表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/c424af68-c488-5e8b-5759-f47c452733ee.png" width="503">

## 3-4. POSTメソッドのサンプルコード（then - catch - finally の形式）

CSRF 対策がされている場合は、リクエスト時に CSRF トークンを送信しないと、認証エラーが生じてしまいます。
下記は、リクエストヘッダから CSRF トークンを送信する例です。

```app/javascript/main.js
window.addEventListener('load', function() {
  let token = document.getElementsByName('csrf-token')[0].content; //HTMLのヘッダからセキュリティトークンの取得
  let url = document.getElementById('sample_input').getAttribute('action');  // formのaction属性から必要なURLを取得
  document.getElementById('sample_input').addEventListener('submit', function(e) {
    e.preventDefault();  // デフォルトのイベントを無効にする
    let inputText = document.getElementsByClassName('sample_form-text')[0].value;  // 入力値を取得
    let data = { sample: {memo: inputText} }; // オブジェクト形式で送信データを格納
    const options = {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',  // リクエストデータがJSON形式と指定
        'Accept': 'application/json',  // レスポンスデータをJSON形式と指定
        'X-CSRF-Token': token  // CSRFトークンをヘッダに追加
      },
      body: JSON.stringify(data)
    };
    fetch(url, options)
      .then(function(response) {
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        return response.json();
      })
      .then(function(data) {
        let divElm = document.createElement('div');  // divタグを生成
        divElm.appendChild(document.createTextNode(data.memo));  // divタグにテキストノードを追加
        document.getElementsByClassName('samples')[0].appendChild(divElm);  // 作成したHTMLをドキュメントに追加
        document.getElementsByClassName('sample_form-text')[0].value = '';  // テキストエリアを空白に戻す
      })
      .catch(function(error) {
        alert(error);
      })
      .finally(function() {
        document.getElementsByClassName('sample_form-btn')[0].disabled = false;  // submitボタンのdisableを解除
        document.getElementsByClassName('sample_form-btn')[0].removeAttribute('data-disable-with');  // submitボタンのdisableを解除
      })
  }, false);
});
```

## 3-5. async / await を使用したPOSTメソッドのサンプルコード

async / await で書くと次のようになります。

```app/javascript/main.js
window.addEventListener('load', function() {
  let token = document.getElementsByName('csrf-token')[0].content; //HTMLのヘッダからセキュリティトークンの取得
  let url = document.getElementById('sample_input').getAttribute('action');  // formのaction属性から必要なURLを取得
  document.getElementById('sample_input').addEventListener('submit', function(e) {
    e.preventDefault();  // デフォルトのイベントを無効にする
    let inputText = document.getElementsByClassName('sample_form-text')[0].value;  // 入力値を取得
    let sampleData = { sample: {memo: inputText} }; // オブジェクト形式で送信データを格納
    (async () => {
      try {
        const options = {
          method: 'POST',  // HTTPメソッドを指定
          headers: {
            'Content-Type': 'application/json',  // リクエストデータがJSON形式と指定
            'Accept': 'application/json',  // レスポンスデータをJSON形式と指定
            'X-CSRF-Token': token  // CSRFトークンをヘッダに追加
          },
          body: JSON.stringify(sampleData)
        };
        const response = await fetch(url, options);  // サーバにリクエストを送信
        if (!response.ok) {
          throw new Error('Network response was not ok');  // fetchが成功したかどうかの判定
        }
        let data = await response.json();
        let divElm = document.createElement('div');  // divタグを生成
        divElm.appendChild(document.createTextNode(data.memo));  // divタグにテキストノードを追加
        document.getElementsByClassName('samples')[0].appendChild(divElm);  // 作成したHTMLをドキュメントに追加
        document.getElementsByClassName('sample_form-text')[0].value = '';  // テキストエリアを空白に戻す
      } catch (e) {
        alert(e);
      } finally {
        document.getElementsByClassName('sample_form-btn')[0].disabled = false;  // submitボタンのdisableを解除
        document.getElementsByClassName('sample_form-btn')[0].removeAttribute('data-disable-with');  // submitボタンのdisableを解除
      }
    })()
  }, false);
});
```

## 3-6. FormData を使用したPOSTメソッドのサンプルコード

Rails の場合、[FormData オブジェクト](https://developer.mozilla.org/ja/docs/Web/API/FormData/Using_FormData_Objects)を使用すると、CSRF トークンをリクエストボディに含めて送信できるので、記載がシンプルになります。
CSRF トークンをリクエストボディから送れるか否かはフレームワークによって異なると思われます。

```app/javascript/main.js
window.addEventListener('load', function() {
  let url = document.getElementById('sample_input').getAttribute('action');  // formのaction属性から必要なURLを取得
  document.getElementById('sample_input').addEventListener('submit', function(e) {
    e.preventDefault();  // デフォルトのイベントを無効にする
    const form = new FormData(document.getElementById('sample_input'));  // FormDataを取得
    (async () => {
      try {
        const options = {
          method: 'POST',  // HTTPメソッドを指定
          headers: {
            'Accept': 'application/json'  // レスポンスデータをJSON形式と指定
          },
          body: form
        };
        const response = await fetch(url, options);  // サーバにリクエストを送信
        if (!response.ok) {
          throw new Error('Network response was not ok');  // fetchが成功したかどうかの判定
        }
        let data = await response.json();
        let divElm = document.createElement('div');  // divタグを生成
        divElm.appendChild(document.createTextNode(data.memo));  // divタグにテキストノードを追加
        document.getElementsByClassName('samples')[0].appendChild(divElm);  // 作成したHTMLをドキュメントに追加
        document.getElementsByClassName('sample_form-text')[0].value = '';  // テキストエリアを空白に戻す
      } catch (e) {
        alert(e);
      } finally {
        document.getElementsByClassName('sample_form-btn')[0].disabled = false;  // submitボタンのdisableを解除
        document.getElementsByClassName('sample_form-btn')[0].removeAttribute('data-disable-with');  // submitボタンのdisableを解除
      }
    })()
  }, false);
});
```

# さいごに

Fetch メソッドは、まだ、あまり試していませんが、XMLHttpRequest オブジェクトと比べると随分と書きやすい印象です。
本記事は学習の記録に過ぎませんが、何らかの参考となれば幸いです。