XMLHttpRequestを利用したPOSTによるデータ受信をPromiseで実現

#XMLHttpRequestを利用したPOSTによるデータ受信をES6(ECMAScript)のPromiseで実現する
JS実装で、必ずといっていいほど利用する`XMLHttpRequest`だけど、ちゃんとコードを清書する機会がなく`Promise`を使う機会があまりなかったので、ここでまとめることにした。
ここで記載するシナリオはPOSTでJSONデータを利用するシナリオ(シナリオ1)と、POSTでCSVを受信するシナリオ(シナリオ2)の2パターンを記載。

##サンプルのディレクトリ構成
PHPが動作できるWebサーバ環境で、ドキュメントルート直下に下記のディレクトリ構成を作成する。便宜上2つのシナリオについて別々とした。
`test.csv`の内容についてはここでは割愛。

`./scenario1.htm`
`./scenario2.htm`
`./json.php`
`./csv.php`
`./css/modal.css`
`./js/app1.js`
`./js/app2.js`
`./js/bundle1.js`
`./js/bundle2.js`
`./js/libs/MyRequest.js`
`./js/libs/MyLoader.js`
`./csv/test.csv`

##サンプルのES6のビルド方法(bundle1.jsとbundle2.jsの作成方法)について
[こちら](http://qiita.com/hiroykam/items/135c51310087182b03cb)にまとめた。

##基本的なライブラリを用意する
まずは、それぞれのシナリオで利用する共通ライブラリを作成。`XMLHttpRequest`を使ったライブラリと、せっかくなので、通信中にローダを表示するためのライブラリも用意。

###Promiseを利用したXMLHttpRequest
`XMLHttpRequest`を使ったライブラリをまずは用意する。ここでは`POST`を利用する前提にしたが、他のパターンが簡易的に追加できる。もちろん`Promise`を利用。

```js:./js/libs/MyRequest.js
"use strict";

export default class MyRequest {

    constructor(url, method, responseType) {
        this.method       = method || "POST";
        this.url          = url;
        this.responseType = responseType || MyRequest.RESPONSE_TYPE_JSON;
        this.onload       = () => {};
        this.onerror      = () => {};
        this.onabort      = () => {};
        this.xhr          = null;
    }

    static post(url, responseType) {
        return new MyRequest(url, "POST", responseType);
    }

    then(onload) {
        this.onload  = onload;
        return this;
    }

    catch(onerror) {
        this.onerror  = onerror;
        return this;
    }

    stop() {
        if (this.xhr) {
            this.xhr.abort();
        }
    }

    send(data) {
        let xhr = new XMLHttpRequest();

        xhr.onload = () => {
            if (xhr.status !== 200) {
                this.onerror.call(this, xhr.responseText);
                return;
            }
            switch (this.responseType) {
                case MyRequest.RESPONSE_TYPE_JSON:
                    let json;
                    try {
                        json = JSON.parse(xhr.responseText);
                    } catch (e) {
                        json = {};
                    }
                    this.onload.call(this, json);
                    break;
                case MyRequest.RESPONSE_TYPE_BLOB:
                    this.onload.call(this, xhr.response);
                    break;
            }
        };

        xhr.onerror = () => {
            switch (this.responseType) {
                case MyRequest.RESPONSE_TYPE_JSON:
                    let json;
                    try {
                        json = JSON.parse(xhr.responseText);
                    } catch (e) {
                        json = {};
                    }
                    this.onload.call(this, json);
                    break;
                case MyRequest.RESPONSE_TYPE_BLOB:
                    this.onerror.call(this, xhr.response);
                    break;
            }
        };

        xhr.open(this.method, this.url, true);
        xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
        if (this.method === "POST") {
            xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        }
        xhr.send(data || null);

        this.xhr = xhr;
        return this;
    }
}

MyRequest.RESPONSE_TYPE_JSON = 0x01;
MyRequest.RESPONSE_TYPE_BLOB = 0x02;
```

###ローダーを表示するライブラリ
簡単なモーダルのローダーを表示するライブラリ。
なお、ローダーのcssは[こちら](https://www.w3schools.com/howto/tryit.asp?filename=tryhow_css_loader4)を参考にした。

```js:./js/libs/MyLoader.js
"use strict"

const modal = document.createElement("div");
modal.className = "modal";

export default class MyLoader {

    constructor() {
        this.loading = document.createElement("div");
        this.loading.className = "loader";
    }

    show() {
        modal.appendChild(this.loading);
        document.body.appendChild(modal);
    }

    hide() {
        document.body.removeChild(modal);
        modal.removeChild(this.loading);
    }
}
```

```css:./css/modal.css
.modal {
    width: 100%;
    height: 300px;
}

.loader {
    margin: 150px auto;
    border: 16px solid #f3f3f3;
    border-radius: 50%;
    border-top: 16px solid blue;
    border-right: 16px solid green;
    border-bottom: 16px solid red;
    border-left: 16px solid pink;
    width: 120px;
    height: 120px;
    -webkit-animation: spin 2s linear infinite;
    animation: spin 2s linear infinite;
}

@-webkit-keyframes spin {
    0% { -webkit-transform: rotate(0deg); }
    100% { -webkit-transform: rotate(360deg); }
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}
```

##シナリオ1
もっともユースケースの多い、サーバ側にPOSTして`JSON`データを受信し、その内容を描画する。

###コード
```html:scenario1.htm
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Scenario1</title>
    <link rel="stylesheet" type="text/css" href="/css/modal.css">
</head>
<body>
<div style="margin: 20px" class="js-json">
    <div style="color: red" class="js-json-message"></div>
    <input type="button" post_url="/json.php" class="js-json-post" value="JSON">
</div>
<footer>
    <script type="text/javascript" src="/js/bundle1.js"></script>
</footer>
</body>
</html>
```

```php:json.php
<?php

header('Content-Type: application/json');
echo json_encode([
    "status" => "SUCCESS",
    "value" => 100
]);
```

```js:js/app1.js
"use strict"

import MyRequest from "./libs/MyRequest";
import MyLoader from "./libs/MyLoader";

class MyApp {
    static exec() {
        const obj = new MyApp();
        const target = document.querySelector(".js-json");
        target && obj.setupJson(target);
    }

    setupJson(target) {
        const url = [
            location.protocol + '//',
            location.host
        ];

        const buttons = target.querySelectorAll(".js-json-post");
        [].forEach.call(buttons, (button) => {
            button.addEventListener("click", (evt) => {
                evt.preventDefault();

                let post_url = url.join('') + button.getAttribute("post_url");
                const loading = new MyLoader();
                loading.show();
                MyRequest.post(post_url)
                    .then((json) => {
                        const msg = target.querySelector(".js-json-message");
                        msg.innerHTML = json.value;
                        loading.hide();
                    })
                    .catch(() => {
                        loading.hide();
                        alert('通信に失敗しました');
                    })
                    .send();
            });
        });
    }
}

MyApp.exec();
```

###デモ結果
####1. `http://localhost/scenario1.php`にアクセスし、ボタンクリックでローダが起動
<img width="585" alt="スクリーンショット 2017-06-17 20.42.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/135559/19c98fbc-aaef-827e-3233-1e820c58e021.png">

####3. 受信したJSONデータを描画
<img width="587" alt="スクリーンショット 2017-06-17 20.45.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/135559/6e357aaa-06e8-ad48-ee89-6ba5e1f59ac2.png">

##シナリオ2
あまりユースケースはないが、サーバにPOSTし、CSVデータを受信するようなケースを想定。JSONのときは`XMLHttpRequest`のプロパティ`responseText`で受信できるが、このケースでは`response`で受信する。
さらに`Blob`オブジェクトを生成し、`a`タグを作成して、クリックイベントを発生させることでダウンロードすることができる。
デモについては、シナリオ1と同様で、またcsvファイルをダウンロードするだけなので、省略。

###コード
```html:./scenario2.htm
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Scenario2</title>
    <link rel="stylesheet" type="text/css" href="/css/modal.css">
</head>
<body>
<div style="margin: 20px" class="js-csv">
    <input type="button" post_url="/csv.php" class="js-csv-post" value="csv">
</div>
<footer>
    <script type="text/javascript" src="/js/bundle2.js"></script>
</footer>
</body>
</html>
```

```php:./csv.php
<?php

$file = "./csv/test.csv";

header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=test.csv');
header('Content-Transfer-Encoding: binary');
header('Content-Length: '. filesize($file));
echo file_get_contents($file);
```

```js:./js/app2.js
"use strict"

import MyRequest from "./libs/MyRequest";
import MyLoader from "./libs/MyLoader";

class MyApp {
    static exec() {
        const obj = new MyApp();
        const target = document.querySelector(".js-csv");
        target && obj.setupCsv(target);
    }

    setupCsv(target) {
        const url = [
            location.protocol + '//',
            location.host
        ];
        const buttons = target.querySelectorAll(".js-csv-post");

        [].forEach.call(buttons, (button) => {
            button.addEventListener("click", (evt) => {
                evt.preventDefault();

                let post_url = url.join('') + button.getAttribute("post_url");
                const loading = new MyLoader();
                loading.show();
                MyRequest.post(post_url, MyRequest.RESPONSE_TYPE_BLOB)
                    .then((response) => {
                        const blob = new Blob([response]);
                        const URL  = window.URL || window.webkitURL;
                        const bu = URL.createObjectURL(blob);
                        const a  = document.createElement('a');
                        a.download = "test.csv";
                        a.href = bu;
                        a.click();
                        loading.hide();
                    })
                    .catch(() => {
                        loading.hide();
                        alert('通信に失敗しました');
                    })
                    .send();
            });
        });
    }
}

MyApp.exec();
```