Michi's Tech Blog

一人前のWebエンジニアを目指して

バニラJSでAjaxを実装してみよう!

こんにちは!
スマレジ テックファームのMichiです!

今回はタイトルの通り、バニラJS(生のJavaScript)でAjaxを実装してみます。

はじめに

作るもの

郵便番号を入力すると、対象の住所が表示される機能を作ります。Ajaxで実装しますので、[検索]ボタンを押しても画面全体は更新されず、住所の表示のみ変わるようにします。

郵便番号検索API

郵便番号検索には、日本郵便が公開している郵便番号検索APIを使用します。使い方はhttps://zipcloud.ibsnet.co.jp/api/searchの後にリクエストパラメータを追加し、GET送信するだけです。
試しに、https://zipcloud.ibsnet.co.jp/api/search?zipcode=1000001をブラウザで叩いてみましょう。すると、このようなJSONが返ってきます。

概要の説明は以上です。

バニラJSで実装してみよう!

完成形

まずはじめに、完成したコードをお見せします

index.html(<head>は省略)

  <body>
    <form>
      <label for="zip-code">郵便番号</label>
      <input id="zip-code" type="number" />
      <input id="btn" type="button" value="検索" />
    </form>
    <hr />
    <div id="address">
      <!-- ここに検索結果が表示される -->
    </div>
    <script src="script.js"></script>
  </body>

script.js

document.addEventListener('DOMContentLoaded', () => {
  /* [検索]ボタンクリック時に実行される処理 */
  document.getElementById('btn').addEventListener('click', () => {
    const address = document.getElementById('address')
    const xhr = new XMLHttpRequest()

    /* 非同期通信の処理を定義 */
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          const data = JSON.parse(xhr.responseText)
          if (data.results) {
            const result = data.results[0]
            address.textContent = result.address1 + result.address2 + result.address3
          } else {
            address.textContent = '該当の住所は存在しません。'
          }
        } else {
          address.textContent = 'サーバーエラーが発生しました。'
        }
      } else {
        address.textContent = '通信中…'
      }
    }

    /* サーバーとの非同期通信を開始 */
    xhr.open(
      'GET',
      `https://zipcloud.ibsnet.co.jp/api/search?zipcode=${encodeURIComponent(
        document.getElementById('zip-code').value,
      )}`,
      true,
    )
    xhr.send(null)
  })
})

解説

①[検索]ボタンクリック時の処理

document.addEventListener('DOMContentLoaded', () => {
  /* [検索]ボタンクリック時に実行される処理 */
  document.getElementById('btn').addEventListener('click', () => {
    const address = document.getElementById('address')
  // XMLHttpRequestオブジェクトの作成
    const xhr = new XMLHttpRequest()

検索ボタンをクリックしたとき、XMLHttpRequestオブジェクトを作成します。
XMLHttpRequestオブジェクトとは、非同期通信を管理するJavaScriptの組み込みオブジェクトです。「XMLHttpRequest」という名前であるにもかかわらず、通信に使用するデータ形式/プロトコルXML*1/HTTPだけでなく、JSONも利用できます。むしろ現在では、JSONを扱うケースがほとんどでしょう。
要するにXMLHttpRequestオブジェクトは、「クライアント・サーバー間の通信を担当するオブジェクト」だと覚えておきましょう。

②非同期通信の処理を定義する

    /* 非同期通信の処理を定義 */
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) { // 通信が完了したとき
        if (xhr.status === 200) { // 通信が成功したとき
          const data = JSON.parse(xhr.responseText)
          if (data.results) { // 住所が取得できた場合、表示する
            const result = data.results[0]
            address.textContent = result.address1 + result.address2 + result.address3
          } else { // 住所が取得できなかった場合には、エラーメッセージを表示
            address.textContent = '該当の住所は存在しません。'
          }
        } else { // 通信が失敗したとき
          address.textContent = 'サーバーエラーが発生しました。'
        }
      } else { // 通信が完了する前
        address.textContent = '通信中…'
      }
    }

XMLHttpRequestオブジェクトのonreadystatechangeプロパティで、非同期通信の開始から終了までに実行する処理を定義します。onreadystatechangeは、通信の状態が変化したタイミングで呼び出されるイベントハンドラーです。
処理の内容は、フロー図で表すと以下のようになります。

③サーバーとの非同期通信を開始

    /* サーバーとの非同期通信を開始 */
    // HTTPリクエストを初期化
    xhr.open(
      'GET',
      `https://zipcloud.ibsnet.co.jp/api/search?zipcode=${encodeURIComponent(
        document.getElementById('zip-code').value,
      )}`,
      true,
    )
    // HTTPリクエストを送信
    xhr.send()
  })
})

準備は整ったので、実際にサーバーへリクエストを投げます。 まずはopenメソッドでリクエストを初期化します。
openメソッドの引数には

  1. HTTPメソッド(GET、POSTなど)
  2. アクセス先のURL
  3. 非同期通信かどうか(デフォルトはtrueなので、明示的に宣言しなくてもOK)

を取ります。
第2引数のURLには、マルチバイト文字や予約文字が含まれている場合に備えて、encodeURIComponentメソッドでエンコード処理をしておきましょう。

リクエストを準備できたら、最後にsendメソッドでリクエストを送信します。sendメソッドの引数には、POST通信時のみリクエスト本体を指定できます。今回はGETなので、引数の指定はしません(値が設定されていない場合、既定値の null が使用される)。

完成

これにて完成です!実際に動かしてみましょう。

フォームに郵便番号を入力し、[検索]ボタンをクリックすると、「通信中…」の文字が表示されます。

その後、再び表示が変化し、該当の住所が表示されます。

画面は一度も更新されることなく、住所欄の表示のみが変化していますね!
他にも、「実在しない住所を入力したパターン」や、「ソースのURLを間違ったものにして、サーバーエラーを発生させる」などして、期待通りの挙動になっているか試してみましょう。

Vue.jsで実装してみる

本題とは逸れますが、Vue.jsとaxiosを使って全く同じ機能を実装すると、こうなります。

<template>
  <form>
    <label for="zip-code">郵便番号</label>
    <input id="zip-code" type="number" v-model="zipCode" />
    <input id="btn" type="button" value="検索" @click="fetchAddress" />
  </form>
  <hr />
  <div id="address">{{ address }}</div>
</template>

<script>
import { ref } from 'vue'
import axios from 'axios'

export default {
  setup() {
    const zipCode = ref()
    const address = ref()

    /* 非同期通信の処理を定義 */
    const fetchAddress = () => {
      // 通信が完了する前
      address.value = '通信中…'
      axios
        // HTTPリクエストを送信
        .get('https://zipcloud.ibsnet.co.jp/api/search', { params: { zipcode: zipCode.value } })
        // 通信が成功したとき
        .then((response) => {
          if (response.data.results) {
            // 住所が取得できた場合、表示する
            const result = response.data.results[0]
            address.value = result.address1 + result.address2 + result.address3
          } else {
            // 住所が取得できなかった場合には、エラーメッセージを表示
            address.value = '該当の住所は存在しません。'
          }
        })
        // 通信が失敗したとき
        .catch(() => (address.value = 'サーバーエラーが発生しました。'))
    }

    return {
      zipCode,
      address,
      fetchAddress,
    }
  },
}
</script>

やってみて、「あれ?意外とバニラJSと行数変わらなくね?」と思いました。
これは単純に操作するDOMや発火イベントが少ないからでしょう。操作するDOMやイベントが増えると、バニラJSではそのたびにDOMを取得するスクリプトを記述する必要があります。一方、Vue.jsでは手動でDOMを取得する必要はなく、テンプレートの中に埋め込まれた{{ address }} が値の変更を検知して、よしなに表示を切り替えてくれます。

このあたりの話は下記の記事が詳しいので、一読されることをおすすめします。

runteq.jp

まとめ

AjaxをあえてバニラJSで実装することで、フレームワークの裏側の処理が理解できた気がします。 こんな面倒くさいDOM操作をすべて手動でやってたのかと思うと、昔のプログラマーはすごいな思いました。
僕はもう二度とやりたくないです。

*1:「eXtensible Markup Language」の略。データのやり取りや設定ファイル書くときなどに使われる。