15
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Laravel × Next.js】Axiosを使ってCSVファイルをダウンロードする

Last updated at Posted at 2022-03-02

概要

フロントnext.js × API Laravel の構成で作成しているアプリケーションにおいて、
「フロント側でとあるボタンをクリックしたら、API側で生成したCSVファイルをダウンロードする」
という機能を業務で実装する機会があったので、実装方法や勉強になったことをまとめておきます。

全体の流れは以下の通りです。

1. フロント側からAxiosを使ってAPIへリクエストを飛ばす。
2. API側(Laravel)でCSVファイルを生成し、レスポンスとして返す。
3. フロント側でレスポンスを受け取り、CSVファイルをダウンロードする。

API側の実装

API側から実装していきます。
※本来はサービスクラスなどへ処理を切り分けるべきではありますが、ここではコントローラーに全ての処理を書くものとします。

ルーティング

フロント側(Next.js)からAxiosで送られてくるリクエストに対するルーティングを定義します。

// routes/api.php

Route::get('/download/csv', [DownloadCsvController::class, 'downloadCsv']);

ここで定義した通り、DownloadCsvControllerdownloadCsvメソッドにCSVファイルを生成してレスポンスとして返す処理をかいていきます。

コントローラー

コードの概要

先に概要だけざっくり説明すると、以下の通りです。

  • LaravelのstreamDownloadメソッドを使って、CSVファイルをレスポンスとして返す。

  • streamDownloadメソッドは引数を3つ取るので、各引数を用意する。
     ・第1引数:エクスポートするファイルを生成するコールバック関数
     ・第2引数:ファイル名
     ・第3引数:レスポンスヘッダーの配列

streamDownloadメソッドについてはReadoubleの以下の箇所を参照ください。

コードの全文

詳しい解説は後にして、コードの全文を載せます。

// DownloadCsvController.php

/**
 * CSVダウンロード
 * @return StreamedResponse
 */
public function downloadCsv(): StreamedResponse
{
  // CSVファイル作成コールバック
  $downloadCsvCallback = function () {
    // CSVファイル作成
    $csv = fopen('php://output', 'w');

    // CSVの1行目
    $colums = [
      'id' => 'ユーザーID',
      'name' => 'ユーザー名',
      'email' => 'メールアドレス'
    ];
    // 文字化け対策
    mb_convert_variables('SJIS-win', 'UTF-8', $colums);
    // CSVの1行目を記入
    fputcsv($csv, $colums);

    // CSVの2行目以降
    $users = User::all();
    foreach ($users as $user) {
      $userData = [
        'id' => $user->id,
        'name' => $user->name,
        'email' => $user->email
      ];
      // 文字化け対策
      mb_convert_variables('SJIS-win', 'UTF-8', $userData );
      // CSVファイルの2行目以降にユーザー情報を記入
      fputcsv($csv, $userData);
    }

    // CSVファイルを閉じる
    fclose($csv);
  }

  // ファイル名
  $fileName = 'ユーザー情報.csv';

  // レスポンスヘッダー情報
  $responseHeader = [
    'Content-type' => 'text/csv',
    'Access-Control-Expose-Headers' => 'Content-Disposition'
  ],

  return response()->streamDownload($downloadCsvCallback, $fileName, $responseHeader);
}

ここから上記のコードのポイントを解説していきます。

steramDownloadメソッド

  • 引数と返り値について

先述の通り、 steramDownloadメソッドは以下の3つの引数を取り、返り値は Symfony\Component\HttpFoundation\StreamedResponseとなります。
 ・第1引数:エクスポートするファイルを生成するコールバック関数
 ・第2引数:ファイル名
 ・第3引数:レスポンスヘッダーの配列

今回の構成では、フロント側(Next.js)からAPIリクエストを受け取ってStreamedResponseの形式でレスポンスを返すことになります。

  • レスポンスヘッダーについて

HTTPレスポンスの際に、ブラウザで表示するのではなくファイルをブラウザでダウンロードさせたい場合は、レスポンスヘッダーにContent-Dispositionを持たせる必要があります。
Content-Disposition - HTTP | MDN

// レスポンスヘッダーに以下の記述が必要
Content-Disposition: attachment

// ファイル名を指定する場合
Content-Disposition: attachment; filename="filename.jpg"

// ファイル名をエンコードする場合
Content-Disposition: attachment; filename*=UTF-8''URLエンコーディングされたファイル名

そこでstreamDownloadメソッドは、このContent-Dispositionをよしなにレスポンスヘッダーに加えてくれます。

以下の画像は、第2引数に'ファイル.csv'と指定してレスポンスを返した際のレスポンスヘッダーを、検証ツールで確認したものです。
レスポンスヘッダー.jpg
ファイル名に日本語を指定すると、URLエンコーディングされた状態でレスポンスヘッダーに付与されています。

CSVファイル作成コールバック

  • CSVファイルの作成

コールバック関数内で行う処理の大まかな流れは以下の通りで、
CSVファイルを作成→中身を書き込む→ファイルを閉じる
というものです。

// ファイルを作成する
fopen('php://output', 'w');  

// CSVの1行目のカラムを記入する
fputcsv($csv, $colums);

// CSVの2行目以降に企業情報を記入する
fputcsv($csv, $userData); 

// ファイルを閉じる
fclose($csv);  
  • 文字化け対策について

デフォルトの文字コードは、
・Excel:SJIS
・PHP:UTF-8php.iniで設定されている)
という違いがあります。

// php.ini
default_charset = "UTF-8"

なのでPHPの文字コードがUTF-8のままCSVファイルを生成すると、Excelで開いたときに文字化けしてしまいます。

これを防ぐため、PHPのmb_convert_variables関数を使って文字コードをUTF-8からSJIS-winに変換します。
PHPドキュメント:mb_convert_variables

mb_convert_variables('SJIS-win', 'UTF-8', $colums);

SJIS-winはWindows向けに使われるShift-JISで、通常のSJISよりも対応している文字が多いようです。

レスポンスヘッダーについて

先述の通り、streamDownloadメソッドがレスポンスヘッダーにContent-Dispositionを自動で含めてくれます。

レスポンスヘッダーの要素はフロント側で AxiosResponse から受け取ることが出来るので、後ほどフロント側でレスポンスヘッダーからファイル名を取得することになります。

但し今回のような CORS の通信の場合、標準的なレスポンスヘッダー以外の場合はAPIから返すレスポンスヘッダーに

Access-Control-Expose-Headers: {レスポンスヘッダ名}

を追加しておかないと、AxiosResponse からレスポンス情報を受け取ることが出来ないので、Content-Dispositionについては上記の記述が必要です。
Access-Control-Expose-Headers - HTTP | MDN

参考記事:Axiosでレスポンスヘッダが取得できなかった (CORSなAPI)

よってstreamDownloadメソッドの第3引数には、以下のレスポンスヘッダー情報を渡します。

// レスポンスヘッダー情報
$responseHeader = [
  'Content-type' => 'text/csv',
  'Access-Control-Expose-Headers' => 'Content-Disposition' 
],

API(Laravel)側の実装は以上です。

ここまでの実装で、直接APIのURLをブラウザのURLに入れてアクセスすればCSVファイルをダウンロードできるはずです。
ただしフロント側にレスポンスとして返すだけではフロント側ではファイルダウンロードは行われないので、続けてフロント側も実装していきます。

フロント側の実装

フロント側の実装方法は2通り紹介させていただきたいと思います。

実装方法①

file-saver というライブラリを使ってファイル保存を行います。
ライブラリ:file-saver

このライブラリの saveAs メソッドに、

・第1引数: blob オブジェクト
・第2引数:ファイル名

を渡すだけで、指定したファイル名でファイルを保存することができます。

実装したコードは以下の通りです。

import { saveAs } from 'file-saver'

// 中略

axiosApi
  .get('/download/csv', {
    responseType: 'blob',
  })
  .then((res: AxiosResponse) => {
    const blob = new Blob([res.data], { type: res.data.type })
    const fileName = getFileName(res.headers['content-disposition'])
    saveAs(blob, fileName)
  })

const getFileName = (contentDisposition: string) => {
  return decodeURI(contentDisposition).substring(
    contentDisposition.indexOf("''") + 2,
    contentDisposition.length,
  )
}

ポイント

  • responseType: 'blob'

この指定をしないと文字化けしてしまいます。

  • ファイル名の取得

レスポンスヘッダーの情報は、AxiosResponseres から、

res.headers['content-disposition']

のように書くと取り出すことができます。

レスポンスヘッダーのContent-Dispositionをフロント側で受け取り、出力してみると、以下のようにエンコードされた状態です。

axiosApi
  .get(`/download/csv/${entryType}`, {
    responseType: 'blob',
  })
  .then((res: AxiosResponse) => {
    console.log(res.headers['content-disposition'])    // 出力する
    const blob = new Blob([res.data], { type: res.data.type })
    const fileName = getFileName(res.headers['content-disposition'])
    saveAs(blob, fileName)
  })

出力結果

attachment; filename=___20220301173602.csv; filename*=utf-8''%E9%A1%A7%E5%AE%A2%E6%83%85%E5%A0%B1_%E3%82%82%E3%81%AE%E3%81%A5%E3%81%8F%E3%82%8A%E8%A3%.csv

なので上記の状態からデコードを行い、さらにfilename*=utf-8''以降のファイル名の部分のみを切り取ります。

const getFileName = (contentDisposition: string) => {
  return decodeURI(contentDisposition).substring(
    contentDisposition.indexOf("''") + 2,
    contentDisposition.length,
  )
}

実装方法②

HTMLa タグにdownload 属性を設定すると、 a タグをクリックした場合、ブラウザはユーザーをそのURLへ遷移させるのではなくそのコンテンツを保存させます。
: アンカー要素 - HTML: HyperText Markup Language | MDN

この機能を使ってファイル保存を行います。

axiosApi
  .get('/download/csv', {
    responseType: 'blob',
  })
  .then((res: AxiosResponse) => {
    // Blobを参照するための一時的なURLを生成
    const url = window.URL.createObjectURL(new Blob([res.data]))
    // HTML要素のaタグを生成
    const link = document.createElement('a')
    link.href = url
    // aタグのdownload属性を設定
    link.setAttribute('download', `顧客情報_ものづくり補助金_全件_${getTimestamp()}.csv`)
    // 生成したaタグを設置し、クリックさせる
    document.body.appendChild(link)
    link.click()
    // URLを削除
    window.URL.revokeObjectURL(url)
  }

さいごに

自分で実装して検証ツールで確認しながら進めることで、レスポンスヘッダーの働きやレスポンスヘッダーが違えば挙動も変わることを実感できたので良かったと思います。

またLaravelの steramDownloadメソッドがレスポンスヘッダーをよしなに生成してくれていることに気付いたとき、フレームワークってすごい…と感動してしまいました!

参考記事

15
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?