ajax(XMLHttpRequest)でsjisのcsvダウンロード

# 概要
ajax(XMLHttpRequest)でtext/csv形式のapiを呼び出し
取得したcsvをブラウザでダウンロードする方法。
特にExcelで開いたら文字化けせずsjis形式で表示できることがポイント

ついでにプログラム内でcsvを生成して出力するケースを書いとく。

## apiから取得したcsvをダウンロード
ポイントは以下２点

- ポイント1
  - `xhr.responseType = "blob"`でレスポンスをバイナリ形式で受け取る
- ポイント2
  - aタグを生成して取得したバイナリデータを埋め込む

```typescript
/** Blob取得APIのレスポンス */
export default interface ApiBlobResponse {
  /** HTTPステータスコード */
  status: number;
  /** HTTPステータス名 */
  stausText: string;
  /** レスポンスデータ */
  response: Blob;
  /** レスポンスヘッダ */
  headers: {
    [name: string]: string;
  } & { "content-disposition"?: string; };
}

/**
 * CSVをダウンロード
 * @param result csv取得APIの結果（バイナリデータ）
 * @param fileName csvファイル名の指定
 */
export function downloadCsv(result: ApiBlobResponse | undefined): void {
    if (result === undefined || result.headers["content-disposition"] === undefined) {
        return;
    }
    const a = document.createElement("a");
    document.body.appendChild(a);
    a.href = window.URL.createObjectURL(result.response);  // ★ポイント2
    const matchedFileName: string[] | null = result.headers["content-disposition"].match(/.*filename=(.*\.csv)/);
    if (matchedFileName === null) {
        return;
    }
    a.download = matchedFileName[1];
    a.click();
}

/**
 * ajax (csv取得用)
 * @param url リクエストURL
 */
export function ajaxCsv<T>(url: string, method: string, data?: T): Promise<ApiBlobResponse> {
    return new Promise<ApiBlobResponse>
        ((resolve: (result: ApiBlobResponse) => void, reject: (result: ApiBlobResponse) => void) => {
            // XMLHttpRequest
            const xhr: XMLHttpRequest = new XMLHttpRequest();
            xhr.open(method, url);
            xhr.responseType = "blob";  // ★ポイント1

            // レスポンスの処理
            // 成功時
            xhr.onload = (() => {
                const response = getBlobResponseInfo(xhr);
                if (xhr.status === 200) {
                    resolve(response);
                } else {
                    reject(response);
                }
            });
            // 失敗時
            xhr.onerror = ((error: ProgressEvent) => {
                const result: ApiBlobResponse = {
                    headers: {},
                    response: new Blob(),
                    status: 500,
                    stausText: error.type,
                };
                reject(result);
            });

            // リクエスト実施
            if (method === "GET") {
                // GET
                xhr.send();
            } else {
                // POST --> POSTデータ(JSON)
                xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
                if (data === undefined || data === null) {
                    xhr.send();
                } else {
                    xhr.send(JSON.stringify(data));
                }
            }
        });
}

/**
 * Blob型のレスポンス詳細情報を取得する
 * @param xhr XMLHttpRequest
 * @return レスポンス詳細情報
 */
export function getBlobResponseInfo(xhr: XMLHttpRequest): ApiBlobResponse {
    const headers: Params = {};
    const responseHeader: string[] = xhr.getAllResponseHeaders().split("\n");
    responseHeader.forEach((line: string) => {
        const header: string[] = line.trim().split(":");
        if (header.length === 2) {
            headers[header[0].trim()] = header[1].trim();
        }
    });
    const apiResponseInfo: ApiBlobResponse = {
        headers,
        response: xhr.response,
        status: xhr.status,
        stausText: xhr.statusText,
    };
    return apiResponseInfo;
}
```

## apiから取得した文字列のcsvをダウンロード
最初文字列で取得したcsvをなんとかダウンロードしようと苦労してた頃の残骸。
だが、プログラム内でcsvを文字列生成した際などは使えそうなのでメモしておく。  

一応以下２つのパッケージをインストールしてる

- `npm install --save-dev encoding-japanese`
- `npm install --save-dev @types/encoding-japanese`

```typescript
import Encoding from "encoding-japanese";

export default interface ApiResponse {
  /** HTTPステータスコード */
  status: number;
  /** HTTPステータス名 */
  stausText: string;
  /** レスポンスデータ */
  responseText: string;
  /** レスポンスヘッダ */
  headers: {
    [name: string]: string;
  }
}

/**
 * CSVをダウンロード
 * @param result csv取得APIの結果
 * @param fileName csvファイル名の指定
 */
export function downloadCsv(result: string | null | undefined, fileName: string): void {
    if (result === undefined || result === null) {
        return;
    }
    const a = document.createElement("a");
    document.body.appendChild(a);
    // ★ポイント2 この辺以下
    const unicodeList = [];
    for (let i = 0; i < result.length; i += 1) {
        unicodeList.push(result.charCodeAt(i));
    }
    // 変換処理の実施
    const shiftJisCodeList = Encoding.convert(unicodeList, "SJIS", "UNICODE");
    const uInt8List = new Uint8Array(shiftJisCodeList);

    const blob = new Blob([uInt8List], { type: "text/csv" });
    a.href = window.URL.createObjectURL(blob);
    a.download = fileName;
    a.click();
}

/**
 * ajax (csv取得用)
 * @param url リクエストURL
 */
export function ajaxCsv<T>(url: string, method: string, data?: T): Promise<string | null> {
    return new Promise<string | null>
        ((resolve: (result: string | null) => void, reject: (result: ApiResponse) => void) => {
            // XMLHttpRequest
            const xhr: XMLHttpRequest = new XMLHttpRequest();
            xhr.open(method, url);
            xhr.overrideMimeType("text/csv;charset=Shift-JIS"); // ★ポイント1

            // レスポンスの処理
            // 成功時
            xhr.onload = (() => {
                const response: string | null = (xhr.responseText === "") ? null : xhr.responseText;
                if (xhr.status === 200) {
                    resolve(response);
                } else {
                    reject(getResponseInfo(xhr));
                }
            });
            // 失敗時
            xhr.onerror = ((error: ProgressEvent) => {
                // ProgressEvent からはほとんどエラー情報が取得できない
                const result: ApiResponse = {
                    headers: {},
                    responseText: error.type,
                    status: 500,
                    stausText: error.type,
                };
                reject(result);
            });

            // リクエスト実施
            if (method === "GET") {
                // GET
                xhr.send();
            } else {
                // POST --> POSTデータ(JSON)
                xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
                if (data === undefined || data === null) {
                    xhr.send();
                } else {
                    xhr.send(JSON.stringify(data));
                }
            }
        });
}
```

# まとめ
そもそもAPIレスポンスを受けて、別で用意した対象csvファイルのURLにアクセスさせる！
とかの方が最適なのかもしれないが、たまたまこういうcsvダウンロードを実現する必要があったので忘れないようにメモしとく