Django × Vue.jsでCSVダウンロード機能を実装する
こんにちは!
スマレジ テックファームのMichiです!
今回は実務でつまずいたところの話です。
記事を書こうと思った経緯
今の現場ではバックエンドがDjango(Python)、フロントエンドはVue.js(JavaScript)を使用しています。
先日、CSV出力機能の実装タスクを振られました。内容としては、「データベースから情報を取得し、CSVに整形して出力する」といった、ごくありふれたものです。
CSV出力機能自体は以前、MPAでの実装経験があったので楽勝と思い引き受けましたが、いざコーディングをはじめるとある壁にぶつかりました。
「あれ、そういえばSPAでCSV出力機能ってどうやって作るんだ??」
そして調べてみると意外と面倒くさかったので、忘備録として今回の記事に残しておきます。
実装
バックエンド(Django)
まずはバックエンドの実装から。
バックエンドはMPAで実装するときと変わりません。
レスポンスのContent-Type
をtext/csv
に設定し、Content-Disposition
にCSVファイル名を設定します。
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で疑似的なリンクを作り出してクリックさせる方法を知った時は、「なるほどな~」と感心してしまいました。
同時に、
「こんなん初見で思いつくわけないやろ!」
とも思いました(笑)
とはいえ、一度やり方さえ覚えてしまえばパターンは決まっているので、次からは問題なくできそうです。
ここまでお読みいただきありがとうございました。