Ajaxというのを業務で経験したので勉強

# 目次

[1. Ajaxとは](#ajaxとは)
[2. 今回の勉強用アプリについて](#今回の勉強用アプリについて)
[3. Ajax使用箇所解説](#ajax使用箇所解説)
[4. 終わりに](#終わりに)

# Ajaxとは
Asynchronous JavaScript and XML の造語？

## Asynchronous
「非同期」という意味

<dl>
  <dt>同期</dt>
  <dd>・【リクエスト→処理A開始→処理A終了→処理B開始→処理B終了→レスポンス】のように<strong>順番</strong>にプログラムを処理する。</dd>
  <dd>・「ECサイトで商品を購入確定後、在庫状況を更新する」ような処理</dd>
  <dt>非同期</dt>
  <dd>・【リクエスト→処理A→処理B→レスポンス】の順番で動くアプリだけど、【処理A→処理C】も別で実行させる</dd>
  <dd>・「ECサイトで商品を購入確定後、在庫状況を更新するのと並行して、配送作業を行う」ような処理
</dl>

## JavaScript

ブラウザ上で動作するスクリプト
ブラウザのページ上に存在する画像の上にマウスポインタを置くと説明文を表示させたり、用意された入力フォームの入力チェックなどを、サーバ上で処理させるのではなくブラウザ上で処理することができるスクリプト

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="rNQVENG" data-user="ownrluyo-the-flexboxer" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/ownrluyo-the-flexboxer/pen/rNQVENG">
  Untitled</a> by bobボブ (<a href="https://codepen.io/ownrluyo-the-flexboxer">@ownrluyo-the-flexboxer</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## XML

文字列データをタグを用いて意味・構造を定義したもの

```xml:hoge.xml
<?xml version="1.0" encoding="utf-8"?>
<Person>
    <profile>
        <name>Bob</name>
        <like>Rayquaza</like>
    </profile>
    <profile>
        <name>Satoshi</name>
        <like>Pikachu</like>
    </profile>
</Person>
```

# 今回の勉強用アプリについて

- PC上のファイルをブラウザで選択およびサーバへ転送し、そのファイルをサーバへ保存
- サーバ上に保存されている、アップロードファイルをクライアントへ転送し、ダウンロード
- サーバでのファイルアップロード/ダウンロード処理実行結果をポップアップ画面に表示

## 動作環境

ブラウザ : Chrome
サーバ(PC) : SpringBoot(Apache Tomcat)

※ブラウザとサーバは同じ１台のPC(Mac)

## アプリ画面レイアウト

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3469387/9c7bfad8-59ca-8b38-6cee-7dfee888ac1f.png" width=50%>

## Ajax使用箇所

- ファイルデータを受け取りサーバへPOST通信にて転送
- サーバからクライアントへファイルデータを転送しファイルダウンロード
- サーバでの処理結果を確認しポップアップ画面へ結果表示

## コード

### html

```html:main.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <script type="text/javascript" th:src="@{jquery-3.7.1.min.js}"></script>
        <script type="text/javascript" th:src="@{ajax.js}"></script>
        <title>ファイルアップロード/ダウンロード</title>
    </head>
    <body>
        <div>ファイルのアップロード</div>
        <input type="file" id="input-file">
        <button type="button" onclick="upload()">実行</button>

        <div>特定ファイルのダウンロード</div>
        <button onclick="download()">実行</button>
    </body>
</html>
```

### Javascript

```js:ajax.js
// ファイルアップロード処理
function upload() {
    // 画面からファイルを受け取る
    let file = $("#input-file")[0].files[0];

    // ファイルが指定されているか確認
    if (file === undefined) {
        // 指定されていない場合はアラートを出して処理中断
        alert("ファイルをアップロードしてから実行してください");
        return false;
    }
    
    // サーバに画面から受け取ったファイルを渡す準備
    let formdata = new FormData();
    // 「file」というキーでファイルを参照するよう設定
    formdata.append("file", file);

    // サーバへ接続
    $.ajax({
        url:"/upload",
        type:"POST",
        data:formdata,
        contentType:false,
        processData:false,
        dataType:"html"
    })
    // 実行結果が成功となった場合
    .done(function(data, textStatus, jqXhR) {
        alert("ファイルアップロード成功しました");
    })
    // 実行結果が失敗となった場合
    .fail(function(jqXhR, textStatus, errorThrown) {
        alert("エラーが発生しました");
    })
    // 成功・失敗に関わらず実行したいものがある場合
    .always(function(data, textStatus, jqXhR) {
        alert("処理終了");
    });
}

// ファイルダウンロード処理
function download() {
    // サーバへ接続
    $.ajax({
        url:"/download",
    })
    // 実行結果が成功となった場合
    .done(function(data, textStatus, jqXhR) {
        // aタグを生成
        let a = document.createElement("a");
        // サーバから受け取ったファイルの中身を設定
        let blobObj = window.URL.createObjectURL(new Blob([data], {
            type: jqXhR.getResponseHeader("Content-Type")
        }));
        document.body.appendChild(a);
        a.style = "display: none";
        a.href = blobObj;
        // ファイル名を固定で設定
        a.download = "test.txt";
        // aタグを実行し、画面上でファイルダウンロード処理を実施
        a.click();
    })
    // 実行結果が失敗となった場合
    .fail(function(jqXhR, textStatus, errorThrown) {
        alert("エラーが発生しました");
    })
    // 成功・失敗に関わらず実行したいものがある場合
    .always(function(data, textStatus, jqXhR) {
        alert("処理終了");
    });
}
```

### Java

#### SpringApplication

```Java:QiitaApplication.java
package spring.qiita;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

// SpringBootアプリケーションのエントリーポイント
@SpringBootApplication
@ComponentScan(basePackageClasses = ApplicationController.class)
public class QiitaApplication {

	public static void main(String[] args) {
		SpringApplication.run(QiitaApplication.class, args);
	}

}
```

#### Controller

```Java:ApplicationController.java
package spring.qiita;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RequestMethod;

import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletRequest;

// SpringBootアプリケーションのコントローラ
@Controller
public class ApplicationController {

    // ブラウザからhttp://localhost:8080/mainへ接続されると
    // main.htmlを返す
    @RequestMapping("/main") 
    public String main() {
        return "main";
    }

    // http://localhost:<サーバが待ち受けているhttpポート番号>/upload がPOST通信で接続されると
    // ファイルアップロード処理を呼び出す
    @RequestMapping(value = "/upload", method = RequestMethod.POST)
    @ResponseBody
    public void upload(HttpServletRequest request) {
        FileUploadDownload service = new FileUploadDownload();
        service.uploadFile(request);
    }

    // http://localhost:<サーバが待ち受けているhttpポート番号>/download がGET通信で接続されると
    // ファイルダウンロード処理を呼び出す
    @RequestMapping(value = "/download", method = RequestMethod.GET)
    @ResponseBody
    public void download(HttpServletResponse response) {
        FileUploadDownload service = new FileUploadDownload();
        service.downloadFile(response);
    }
}
```

#### ファイルダウンロード/アップロード

```Java:FileUploadDownload.java
package spring.qiita;

import java.io.InputStream;
import java.io.OutputStream;

import org.apache.commons.io.IOUtils;

import java.io.FileInputStream;
import java.io.IOException;

import jakarta.servlet.http.Part;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class FileUploadDownload {
    // ファイル保存先
    private String filepath = "";

    // コンストラクタ
    public FileUploadDownload() {
        // ファイルの保存先の設定
        filepath  = "/Volumes/TRANSCEND/develop/qiita/output/ajax-test.txt";
    }

    // ファイルアップロードメソッド
    public void uploadFile(HttpServletRequest request) {
        try {
            // FormDataオブジェクトで追加した、ブラウザから指定されたファイルを受け取り
            Part part = request.getPart("file");
            // コンストラクタで設定したファイルへ中身を書き込む
            part.write(filepath);
        } catch(IOException e) {
            System.out.println("ファイル入出力エラー");
            System.out.println(e.getMessage());
        } catch(ServletException ex) {
            System.out.println("サーブレットエラー");
            System.out.println(ex.getMessage());
        }
    }

    // ファイルダウンロードメソッド
    public void downloadFile(HttpServletResponse response) {
        // サーバ上に存在するファイルの読み込み及びレスポンスへファイルの中身を返却する為の準備
        try (InputStream is = new FileInputStream(filepath);
            OutputStream os = response.getOutputStream();) {
                // 
                byte[] fileByte = IOUtils.toByteArray(is);
                // レスポンスのContentTypeヘッダーを設定（バイナリデータ)
                response.setContentType("application/octet-stream");
                // レスポンスへファイルの中身の書き込み
                os.write(fileByte);
                // クライアントへ送信
                os.flush();            
        } catch(IOException e) {
            System.out.println("ファイル入出力エラー発生");
            System.out.println(e.getMessage());
        }
    }
}
```

# Ajax使用箇所解説

```js:ajax.js
// ファイルアップロード
// 非同期通信実行処理
$.ajax({
    url:"/upload",
    type:"POST",
    data:formdata,
    contentType:false,
    processData:false,
    dataType:"html"
})
// 実行成功
.done(function(data, textStatus, jqXhR) {
    alert("ファイルアップロード成功しました");
})
// 実行失敗
.fail(function(jqXhR, textStatus, errorThrown) {
    alert("エラーが発生しました");
})
// 必ず実行
.always(function(data, textStatus, jqXhR) {
    alert("処理終了");
});

// ファイルダウンロード
// 非同期通信実行処理
$.ajax({
    url:"/download",
    dataType:false
})
// 実行成功
.done(function(data, textStatus, jqXhR) {
    let a = document.createElement("a");
    let blobObj = window.URL.createObjectURL(new Blob([data], {
        type: jqXhR.getResponseHeader("Content-Type")
    }));
    document.body.appendChild(a);
    a.style = "display: none";
    a.href = blobObj;
    a.download = "test.txt";
    a.click();
})
// 実行失敗
.fail(function(jqXhR, textStatus, errorThrown) {
    alert("エラーが発生しました");
})
// 必ず実行
.always(function(data, textStatus, jqXhR) {
    alert("処理終了");
});
```

- Ajaxでは大きく分けて「非同期通信実行**＜ajax＞**」「実行(コールバック)成功時処理**＜done＞**」「実行失敗時処理**＜fail＞**」の3つの構成を定義することになる(成功時のみ処理したいと、失敗部分の省略などは可能)

- 上記に加えて、Ajaxの実行結果に関わらず、必ず実行したいものがある場合の定義も可能**＜always＞**

## ajax - 非同期通信処理定義 -

```js: ファイルアップロード処理
$.ajax({
    url:"/upload",
    type:"POST",
    data:formdata,
    contentType:false,
    processData:false,
})
```

- **url**: 
    - リクエスト送信先のURL指定
    - デフォルトはカレントページ
    - "/upload" => "http://localhost:<サーバのhttp待ち受けポート番号>/upload"
- **type**: 
    - 接続方法の指定
    - デフォルトはGET
    - 今回のアプリでは、ファイルのアップロード処理時にサーバ側(Java)で、"method = RequestMethod.POST"でPOST通信を待ち受けるようにしているので、"POST"を指定
- **data**: 
    - サーバに送信するデータ
    - 今回のアプリではFormDataオブジェクトで設定したデータを指定
- **contentType**: 
    - 送信するデータの種類の指定
    - デフォルトは'application/x-www-form-urlencoded; charset=UTF-8'
    - 今回のアプリでFormDataを送信するが、このFormDataオブジェクトが適切なContentTypeを設定してくれるようなので、ajaxではfalseと設定
    [参考]https://www.koikikukan.com/archives/2014/09/30-013333.php
- **processData**: 
    - dataに指定したデータをクエリ文字列("application/x-www-form-urlencoded"形式)に変換するか設定
    - デフォルトはtrue
    - 今回のアプリではFormDataオブジェクトをdataに指定しているので、falseに設定

上記以外にも、サーバから受け取るデータの種類を設定する"dataType"や、タイムアウト値を設定する"timeout"、基本非同期通信をしているものを同期通信したいときに設定する"async"、ajax実行直前に何かしら処理を実行したい時に使用する"beforeSend"など、様々なパラメータが存在します。
[参考]
https://api.jquery.com/jQuery.ajax/#jQuery-ajax-settings

## done / fail - 非同期通信実行成功/失敗時処理定義 -

```js: ファイルダウンロード処理
.done(function(data, textStatus, jqXhR) {
    let a = document.createElement("a");
    let blobObj = window.URL.createObjectURL(new Blob([data], {
        type: jqXhR.getResponseHeader("Content-Type")
    }));
    document.body.appendChild(a);
    a.style = "display: none";
    a.href = blobObj;
    a.download = "test.txt";
    a.click();
})

.fail(function(jqXhR, textStatus, errorThrown) {
    alert("エラーが発生しました");
})
```

- **data**: 
    - サーバから送信されたデータ
    - 今回はバイナリ形式のファイルの中身
- **textStatus**:
    - サーバでの実行ステータス
    - "success", "notmodified", "nocontent", "error", "timeout", "abort", "parsererror"のいずれかが入っている
- **jqXhR (jQuery XMLHttpRequest)**:
    - XMLHttpRequestオブジェクトのスーパーセット
    - responseText, responseXML, getResponseHeader()メソッド、jQueryのバージョンが1.5.1の場合には追加でoverrideMimeType()メソッドが含まれている
    - 今回はサーバ側で設定されたContent-Typeを、getResponseHeader()メソッドにて取得
- **errorThrown**:  
    - HTTPの実行ステータス
    - "Not Found" や "Internal Server Error"など、エラーのテキスト文が入っている

# 終わりに

アプリエンジニアとしては、まだ駆け出し(２０２３年で２９歳ですが。。)で、
業務の中で初めて知る技術が多い状態なので、それをまとめる意味で投稿してみました。

今後業務を進めていきながら投稿にまとめた技術をブラッシュアップしていければなと考えています。

(投稿した後に、Ajaxの非同期らしさの説明や具体例もあった方がいいなと思ったので、追加でまとめようかと思います。。)