背景
- Laravel + Vue.jsアプリケーション
-
JWT認証のSPA
- つまり、すべてのリクエストの
Authorization
ヘッダにJWTを載せている
- つまり、すべてのリクエストの
axios.defaults.headers.common['Authorization'] = `Bearer ${jwt}`;
- 集計したデータをCSVファイルとしてダウンロードさせたい
-
ただし、認可もつけたい
- 未ログイン状態ではダウンロードさせない
困ったこと
- RESTful APIを叩いているうちは何も問題はない
-
CSVダウンロードで困った
-
<a href="csvダウンロードリンク">...
ではダメAuthorization
ヘッダが載らないため、認可が通らない
-
やったこと
サーバ側でCSV生成
- こんな感じのCsvDownloaderクラスを作り、コントローラに注入して使った
- 参考
<?php
declare (strict_types = 1);
namespace App\Http\Controllers\Concerns;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
/**
* データをCSVファイルとしてダウンロードする
*/
class CsvDownloader
{
/**
* @param array|Collection $header 見出し
* @param array|Collection $body データ本体
* @param string $fileName
* @return Response
*/
public function downloadCsv(
$header,
$body,
string $fileName
): Response {
// 仮ファイル
$stream = fopen('php://temp', 'w');
// 見出し書き込み
fputcsv(
$stream,
collect($header)->toArray()
);
// データ本体書き込み
collect($body)->each(
function ($row) use (&$stream) {
fputcsv(
$stream,
$row
);
}
);
// ポインタの先頭へ
rewind($stream);
// 変換
// - 改行コード
$csv = str_replace(
PHP_EOL,
"\r\n",
stream_get_contents($stream)
);
// BOM
$csv= pack('C*',0xEF,0xBB,0xBF) . $csv;
$httpHeaders = [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
];
return response(
$csv,
Response::HTTP_OK,
$httpHeaders
);
}
- 何も考えずにBOMなしUTF-8で出力すると、Excelで開いた時に文字化けしてしまう
-
回避方法は下記のようなものがある:
- BOMつきUTF-8にする
- SJISにする
- (他にもUTF-16とか)
- フロントエンドとの兼ね合いで、SJISではなくBOMつきUTF-8にした (後述)
フロントエンド側でBlob生成
- CSVデータのダウンロードはAjaxで行い、フロントエンドでBlob化しダウンロードリンクを生成するようにした
- 参考
export default {
...
methods: {
// AjaxでサーバからCSVを落としてくる
downloadCsvReport() {
axios.get('/download/report')
.then(response => response.data)
.then(data => this.downloadCommon(data, 'filename.csv'));
},
// 落としてきたCSVをBlobにしてダウンロードリンクを生成する
downloadCommon(data, filename) {
const anchor = document.createElement('a');
document.body.appendChild(anchor);
// ここでもBOMをつける必要がある
// さもないとExcelで文字化けする
const bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
const blob = new Blob([bom, data], {type: 'text/csv'});
const objectUrl = window.URL.createObjectURL(blob);
anchor.href = objectUrl;
anchor.download = filename;
anchor.click();
window.URL.revokeObjectURL(objectUrl);
}
...
- buttonの
@click
か何かで実行すればCSVファイルをダウンロードできる - Chromeで動作確認
踏んだ文字化けのパターン
-
サーバ側でCSVのエンコーディングをSJISにしてしまった
- フロントエンド側でUTF-8としてデコードし、文字化けしてしまう
-
フロントエンド側でBlob生成時にBOMを付与し忘れた
- フロントエンドでHTTPレスポンスをデコードした時点でBOMは外れる
-
つまりサーバ側でBOMを付与する必要はない
- が、サーバ単体でまともなCSVファイルをレスポンスできないのは気持ち悪いので付与した