Vue + Axios + Express で非同期通信後にファイルダウンロードさせる

こんにちは2021年。一発目は Node.js と Web の話。

ウェブページからサーバに対して非同期の POST 通信を行い、そのレスポンスとしてファイルを受け取り、ダウンロードする処理を作った。「CSV 出力」ボタンを押すと、サーバで CSV ファイルが作られて、ファイル保存のダイアログが表示される、みたいなアレだ。

フロントエンドは Vue としたが、何の SPA フレームワークでも大してやり方は変わらない。axios を使う例で紹介しているが、ポイントさえ押さえておけば他のライブラリで非同期通信する場合でも同じ。

バックエンドも Express を例にしているが、ポイントさえ押さえれば他のミドルウェアでも応用が効く。


それでは早速コードを。

<template>
  <div class="my-page">
    <a id="download-link" v-bind:href="downloadUrl" v-bind:download="fileName">Link</a>  <!-- ファイルダウンロードのダイアログ表示のために配置しているが、画面上は非表示 -->
    <button v-on:click="downloadFile">ダウンロードする</v-btn>  <!-- ← 画面表示するボタン -->
  </div>
</template>
<style lang="scss" scoped>
  #download-link {
    display: none;
  }
</style>
<script>
import axios from 'axios';

export default {
  name: 'MyPage',
  data: () => ({
    // ダウンロード URL (Blob)
    downloadUrl: null,
    // ファイル名
    fileName: ''
  }),
  async downloadFile() {
    try {
      // ファイルを取得するための POST リクエスト。レスポンスタイプを指定する
      const response = await axios.post('/api/download-file', requestBody, { responseType: 'blob' });
      // 取得したファイルをダウンロードできるようにする
      const fileURL = window.URL.createObjectURL(new Blob([response.data]));
      this.downloadUrl = fileURL;
      this.fileName = response.headers['content-disposition'].replace((/attachment; filename="(.*)"/u), '$1');
      // クリックイベントを発火させてファイルをダウンロードさせる
      setTimeout(() => {
        window.document.getElementById('download-link').click();
      }, 10);
    }
    catch(error) {
      // リクエスト時に Blob 形式を指定しているので、レスポンスの Blob を変換して内容を取り出す
      try {
        const blob = error.response.data;
        const text = await blob.text();
        const json = JSON.parse(text);
        console.error('Error Object : ', json);
      }
      catch(innerError) {
        console.error('Inner Error : ', innerError);
        console.error('Error : ', error);
      }
    }
  }
}
</script>

フロントエンドはこんな感じ。

続いて Exprees 側。TypeScript で書いている。Vue CLI で作ったプロジェクトの場合、vue-cli-plugin-express を使って Express サーバを立てると簡単だ。

import express from 'express';

const router = express.Router();
router.post('/api/download-file', (req, res) => {
  // CSV ファイルの例。ココでは適当に。
  const csv = 'Header 1,Header 2,Header 3'
     + '\n' + 'Row-1-Col-1,Row-1-Col-2,Row-1-Col-3'
     + '\n' + 'Row-2-Col-1,Row-2-Col-2,Row-2-Col-3'
     + '\n' + 'Row-3-Col-1,Row-3-Col-2,Row-3-Col-3';
  
  res.setHeader('Content-Type', 'text/csv; charset=UTF-8');
  res.attachment(`example.csv`);  // Content-Disposition でファイル名を指定する (ダウンロード時のファイル名として利用する)
  res.send(String.fromCharCode(0xFEFF) + csv);  // UTF-16 U+FEFF は UTF-8 の BOM である EF BB BF に変換され、BOM 付き UTF-8 でレスポンスできる (BOM により Excel で直接開いても文字化けしなくなる)
});
export default router;

ココでは CSV ファイルを用意していて、UTF-8 の BOM を付けてレスポンスしている。コレは主に Excel で開いた時に文字化けしないようにするためだ。

Angular アプリで、フロントエンドオンリーで CSV ファイルをダウンロードさせる時に、次のように書いたことがあった。

@Component({ ... })
export class CsvFileToTableComponent {
  public onDownloadExampleFile(): void {
    // tslint:disable-next-line:no-magic-numbers
    const bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
    const content = '見出しA,見出しB,見出しC\nデータA1,データB1,データC1\nデータA2,データB2,データC2';
    const blob = new Blob([bom, content], { type: 'text/csv' });
    if(window.navigator.msSaveBlob) {
      window.navigator.msSaveBlob(blob, 'example.csv');
    }
    else {
      this.exampleFileUrl = this.domSanitizer.bypassSecurityTrustUrl(window.URL.createObjectURL(blob));
    }
  }
}

UTF-8 の BOM を作るために new Uint8Array([0xEF, 0xBB, 0xBF]) と書いていたが、String.fromCharCode(0xFEFF) で簡単に書けるらしいことが分かった。

コレでイイカンジ。