iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

Ajaxを使ってタスク管理アプリを作る

初めに


はじめまして!

株式会社iimonでフロントエンドエンジニアをしている林です。

普段は、弊社プロダクトの拡張アプリの開発をしています。

本日はAjaxについて解説していきたいと思います。

Ajaxとは?


1. 概要


Ajaxとは、Asynchronous JavaScript + XMLの略であり、JavaScriptXMLを使って非同期にサーバ間との通信を行う技術の事です。

従来のWebページではデータを追加取得して表示するには再レンダリングする必要がありましたが、Ajaxを使用することにより再レンダリングする必要がなく、サーバーから新しいデータを取得して既存のページに追加することが可能です。

名前の由来になっている通り、2005年頃に登場した初期のAjaxではXML形式のデータのやり取りが主流でしたが、その後、JSONの普及に伴い、現在ではJSON形式が主流になっています。

  • 非同期処理とは
    • ざっくり言うと複数の処理を同時に実行できる処理のことで、処理が完了するまで待たずに次の処理を実行できるため、データベースアクセスなど時間のかかる処理を行う際に別の処理をしながら、処理結果を待てます。
  • XMLとは
    • Extensible Markup Languageの略で、テキストベースのデータ形式の一種です。異なるプログラミング言語やプラットフォームで開発されたシステム間でデータを共有するために使われる言語です。
  • JSONとは

2. Ajaxの使用例


  • Googleマップ
    • ユーザーが地図をドラッグしたり、ズームしたりすると、新しい地図の部分をAjaxを使って、サーバーから取得し追加表示しています。
  • Twitter
    • 新しいツイートをAjaxを使ってサーバーから取得し追加表示しています。
  • YouTube
    • 動画検索結果ページをスクロールすると、Ajaxを使用して新しい動画が自動的に読み込まれ、ページを再読み込みすることなく表示されます。

上記以外にも様々なところでAjaxは使われております。

3. 今回実装するアプリ

それでは下記のような構成の簡単なタスク管理ツールを作成して貰おうと思います。


図の通り、ブラウザからwebサーバー(webServer.js)にリクエストを送り、リクエストファイルがあれば、それを返して、それが無ければアプリ(taskWebApp.js)に転送します。アプリはリクエストpathが正しければJSONでtaskを作成して返します。

バックエンドはnode.jsで簡単に作ってますが、あくまでAjaxを体験して頂きたくて作成する為、DB部分の実装とserver部分の解説は割愛させて頂きます。

feachのajax処理によりUIが再レンダリングされずに更新するのを確認頂ければと思います。

ディレクトリの作成と移動

mkdir ajax-tasks && cd $_

必要ファイルの作成

touch {tasks.html,tasks.js,taskWebApp.js,webServer.js}

package.jsonの作成

  • 色々聞かれるが全てenterでok
npm init

http-serverのインストール

npm install http-server

[tasks.html]

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>tasks</title>
  </head>
  <body>
    <h1>タスク管理アプリ</h1>
    <input id="task-title-input" type="text" />
    <button id="task-add-button" disabled>追加</button>
    <h2>タスク一覧</h2>
    <table>
      <thead>
        <tr>
          <th>タイトル</th>
          <th>作成日時</th>
        </tr>
      </thead>
      <tbody id="tasks-table-body"></tbody>
    </table>
  </body>
  <script src="tasks.js"></script>
</html>

[tasks.js]

const tasksTableBody = document.getElementById('tasks-table-body')
const taskTitleInput = document.getElementById('task-title-input')
const taskAddBtn = document.getElementById('task-add-button')

  const loadTasks = async () => {
    tasksTableBody.innerHTML = ""

    // ajax
    const response = await fetch('/api/tasks')
    const responseBody = await response.json()
    const tasks = responseBody.tasks

    tasks.forEach((task) => {
      const titleTd = `<td>${task.title}</td>`;
      const createdAtTd = `<td>${task.createdAt}</td>`;
      const trElement = `<tr>${titleTd}${createdAtTd}</tr>`;
      tasksTableBody.insertAdjacentHTML('beforeend', trElement);
    })
  }

  const createTask = async () => {
  const title = taskTitleInput.value

  const dt = new Date();
  const year = String(dt.getFullYear()).padStart( 2, '0')
  const month = String(dt.getMonth() + 1 ).padStart( 2, '0')
  const date = String(dt.getDate()).padStart( 2, '0')
  const hours = String(dt.getHours()).padStart( 2, '0')
  const minutes = String(dt.getMinutes()).padStart( 2, '0')
  const getSeconds = String(dt.getSeconds()).padStart( 2, '0')
  const createdAt = `${year}/${month}/${date}/${hours}:${minutes}:${getSeconds}`

  const requestBody = {
    title: title,
    createdAt: createdAt
  }

  // ajax
  await fetch('/api/tasks', {
    method: 'POST',
    body: JSON.stringify(requestBody)
  })

  await loadTasks()
  taskTitleInput.value = ''
}

  const main = async () => {
  taskTitleInput.addEventListener('input', (event) => {
    const inputValue = event.target.value
    const isInvalidInput = inputValue.length < 1 || 30 < inputValue.length
    taskAddBtn.disabled = isInvalidInput
  })
  taskAddBtn.addEventListener('click', createTask)
  await loadTasks()
}

main()

[webServer.js]

const http = require('http')
const fs = require('fs')
const PORT = 3000

const fileExists = (path) => {
  return fs.existsSync(`.${path}`) && !fs.statSync(`.${path}`).isDirectory()
}

// HTTPサーバーを作成
http
  .createServer((request, response) => {
    const method = request.method
    const path = request.url
    const requestFile = path.endsWith('/') ? path + 'tasks.html' : path

    if (method === 'POST' || !fileExists(requestFile)) {

      const requestOptions = {
        method: method,
        path: path,
        headers: request.headers
      }

      // taskWebAppへのリクエスト作成
      const taskWebAppRequest = http.request(
        'http://localhost:8080',
        requestOptions
      )

      // クライアントから送信されたリクエストのボディを、taskWebAppRequest に書き込み。
      request.on('data', (data) => {
        taskWebAppRequest.write(data)
      })

      // responseが来た際のハンドリング
      taskWebAppRequest.on('response', (taskWebAppResponse) => {
        Object.entries(taskWebAppResponse.headers).forEach((header) => {
          response.setHeader(header[0], header[1])
        })
        response.writeHead(taskWebAppResponse.statusCode)
        taskWebAppResponse.on('data', (data) => {
          response.write(data)
        })
        taskWebAppResponse.on('end', () => {
          response.end()
        })
      })

      // リクエスト終了後のハンドリング
      request.on('end', () => {
        taskWebAppRequest.end()
      })
      return
    }

    // リクエストのファイルをレスポンス
    const fileContent = fs.readFileSync(`.${requestFile}`)
    response.writeHead(200)
    response.write(fileContent)
    response.end()
  })
  .listen(PORT, '127.0.0.1')

  console.log(`Server started on port ${PORT}`)

[taskWebApp.js]

const http = require('http')
const PORT = 8080
const tasks = [
  {
    title: 'サンプルタスク',
    createdAt: '2023/03/17/14:10:10'
  }
]

// HTTPサーバーを作成
http
  .createServer((request, response) => {
    const method = request.method
    const path = request.url

    // リクエストによってハンドリング
    if (path === '/api/tasks' && method === 'GET') {
      response.writeHead(200)
      const responseBodyJson = {
        tasks: tasks
      }
      const responseBody = JSON.stringify(responseBodyJson)
      response.write(responseBody)
      response.end()
      return
    } else if (path === '/api/tasks' && method === 'POST') {

      let requestBody = ''
      request.on('data', (data) => {
        requestBody += data
      })
      request.on('end', () => {
        const requestBodyJson = JSON.parse(requestBody)
        const title = requestBodyJson.title
        const createdAt = requestBodyJson.createdAt

        const task = {
          title: title,
          createdAt: createdAt
        }

        tasks.push(task)
        response.writeHead(201)
        response.end()
      })
      return
    }
    response.writeHead(404)
    response.end()
    return
  })
  .listen(PORT, '127.0.0.1')

console.log(`Server started on port ${PORT}`)
  • ターミナルを2つ用意し、次のコマンドをそれぞれ実行し、サーバーを立ち上げ
    • node webServer.js
    • node taskWebApp.js

  • localhost:3000にアクセスすると、入力したタスクがajaxで下のリストに追加されるタスクアプリが動くと思います。

まとめ

  • AjaxはJSを始めた時に理解するのに詰まりやすいポイントです。少しでも理解するお役に立てたのであれば幸いです。

参考資料

https://qiita.com/hisamura333/items/e3ea6ae549eb09b7efb9

https://www.youtube.com/watch?v=V89nh3UCBbM

https://hnavi.co.jp/knowledge/blog/ajax/#title1

https://zenn.dev/wkb/books/node-tutorial/viewer/todo_03

https://qiita.com/ritukiii/items/7f28554369d63eb373c3

https://qiita.com/ritukiii/items/8173ff98f31c2f76b39a

https://qiita.com/ritukiii/items/5deba734249bf3543b85