Michi's Tech Blog

一人前のWebエンジニアを目指して

Django × Vue.jsでCSVダウンロード機能を実装する

こんにちは!
スマレジ テックファームのMichiです!

今回は実務でつまずいたところの話です。

記事を書こうと思った経緯

今の現場ではバックエンドがDjango(Python)、フロントエンドはVue.js(JavaScript)を使用しています。

先日、CSV出力機能の実装タスクを振られました。内容としては、「データベースから情報を取得し、CSVに整形して出力する」といった、ごくありふれたものです。

CSV出力機能自体は以前、MPAでの実装経験があったので楽勝と思い引き受けましたが、いざコーディングをはじめるとある壁にぶつかりました。

「あれ、そういえばSPAでCSV出力機能ってどうやって作るんだ??」

そして調べてみると意外と面倒くさかったので、忘備録として今回の記事に残しておきます。

実装

バックエンド(Django)

まずはバックエンドの実装から。

バックエンドはMPAで実装するときと変わりません。 レスポンスのContent-Typetext/csvに設定し、Content-DispositionCSVファイル名を設定します。

View.py

import csv
from datetime import datetime
from urllib.parse import quote

from django.http import HttpResponse


def download_csv(request):
    # CSVの元データ
    data = [
        ['First name', 'Last name', 'age'],
        ['Adam', 'Smith', 29],
        ['John', 'Doe'34], 
        ['Jane', 'Lee', 19],
    ]

    # ファイル名は「サンプル_20230629123040」のようにする
    current_datetime = datetime.now().strftime('%Y%m%d%H%M%S')
    filename = f"サンプル_{current_datetime}.csv"

    # ファイル名をURLエンコードする
    encoded_filename = quote(filename)
    
    # HTTPレスポンスを設定する
    response = HttpResponse(content_type='text/csv; charset=UTF-8')
    response['Content-Disposition'] = f'attachment; filename={encoded_filename}'

    # CSVへの書き込み
    writer = csv.writer(response)
    for row in data:
        writer.writerow(row)

    return response

ひとつ注意しなければならないのは、encoded_file_name = quote(filename)の部分です。

ここでファイル名をURLエンコードしてあげないと、Vue.jsから呼び出したときに文字化けが発生します。
MPAでブラウザから直接ファイルのダウンロードを行う場合はなくてもOK)

この時点で、ブラウザからエンドポイントを叩くとCSVファイルがダウンロードできます。

フロントエンド(Vue.js)

今回はSPAですので、Vue.jsで動的にデータを取得する方法を考えます。

実際にコーディングすると次のようになります。

csvDownLoad.vue

<template>
  <button @click="downloadCSV">CSVダウンロード</button>
</template>

<script>
import axios from "axios";

export default {
  setup() {
    const downloadCSV = () => {
      axios({
        url: "http://localhost:8000/download_csv",
        method: "GET",
        responseType: "blob", // レスポンスをblobとして扱う
      })
        .then((response) => {
          // ヘッダーのContent-Dispositionからファイル名を抽出
          const contentDisposition = response.headers["content-disposition"];
          const filenameMatch = contentDisposition.match(/filename=(.+)/);

          // URLエンコードされているファイル名をデコードする
          const filename = decodeURIComponent(filenameMatch[1]);

          // レスポンスデータをBlobオブジェクトとして扱い、そのオブジェクトのURLを生成
          const url = window.URL.createObjectURL(new Blob([response.data]));

          // ダウンロードリンクを作成し、生成したBlobオブジェクトのURLをhref属性に設定
          const link = document.createElement("a");
          link.href = url;

          // ダウンロードするファイル名をDjangoから取得したものに設定
          link.setAttribute("download", filename);

          // 作成したリンクをDOMに追加し、そのリンクをクリック
          document.body.appendChild(link);
          link.click();
        })
        .catch((error) => {
          console.log(error);
        });
    };
    return { downloadCSV };
  },
};
</script>

CSVデータが格納されているURLとそのリンクを持つ<a>要素を作成し、疑似的にクリックさせることでCSVファイルをダウンロードします。

なお、このコードはContent-Dispositionが常にattachment; filename=filename.csvの形式であることを前提としています。実際の開発では、ヘッダーの形式が異なる場合やヘッダーが存在しない場合などに対応するための、追加のエラーハンドリングが必要になる可能性があります。

まとめ

JavaScriptで疑似的なリンクを作り出してクリックさせる方法を知った時は、「なるほどな~」と感心してしまいました。

同時に、

「こんなん初見で思いつくわけないやろ!」

とも思いました(笑)

とはいえ、一度やり方さえ覚えてしまえばパターンは決まっているので、次からは問題なくできそうです。

ここまでお読みいただきありがとうございました。