ファイルをダウンロード保存する方法
ここではウェブサイトでファイルをダウンロード保存させる方法について説明します。
a タグの download 属性でダウンロード
従来は Content-Disposition で「ファイルに保存」としていた
これまで、サーバーからのデータを「ダウンロードしてファイルに保存」するには、サーバーからクライアントへの HTTP レスポンスを送信するときに次のような HTTP ヘッダーを送る必要がありました。
Content-Disposition: attachment; filename="foo.png"
ブラウザ側では、Content-Disposition というヘッダーをみることで、そのコンテンツをそのままインラインで表示するのではないということを知ることができます。
言い換えると、Content-Disposition というヘッダーがある場合に、ブラウザはそのページにナビゲートするのではなく、これからサーバーから送られてくるデータは今のページのアタッチメント (添付データ) である、と認識することができたのです。
つまり、これはデータをファイルに保存するということであり、ダウンロード保存する、ということに他なりません。
HTML5 では a タグに download 属性が導入されたことで、Content-Disposition はもう不要に!
「サーバーからの HTTP ヘッダーによって、ファイルのダウンロードが指定できる」ということは、言い換えれば 「ダウンロードを指定するには、サーバーの HTTP ヘッダーを操作しなければならない」ということです。
JavaScript 上で動くクライアントサイドのアプリケーションを開発している時に、ウェブサーバーのヘッダーを操作しないとできないことがある、というのはとても厄介ですね。
クライアント側 (ブラウザ) 内のアプリケーションの動作は、クライアントで実行される JavaScript (と HTTP/CSS) で決められる方が良いです。
HTML5 では a 要素 (a タグ、アンカータグ) に download 属性が追加されました。
download 属性が設定された a タグをクリックした場合、ブラウザはユーザーをそのコンテンツのページにナビゲートするのではなく、 コンテンツをファイルに保存します。
例えば、次のように a タグを書くとdownload 属性がセットされているので、ブラウザは画像ファイル foo.jpg を表示せずに、ダウンロードしてファイルに保存しようとします。
<a href="foo.jpg" download>Image 1</a>
ファイル名は href に指定した URL からとるか、download 属性の 値として設定できることになっています。
例えば、次のように a タグを書いた場合、ブラウザは画像ファイル foo.jpg を bar.jpg という名前で保存します。
<a href="foo.jpg" download="bar.jpg">Image 1</a>
a タグの download 属性については「HTML5 : a タグの download 属性」も参考にしてください。
HTM5 の download 属性を JavaScript から利用する
上では a タグの download 属性を使うと、ファイルのダウンロードができることを説明しました。
この操作を JavaScript から動的に行うにはどうすれば良いでしょうか。
a タグの download 属性を使って、JavaScript からファイルをダウンロードするには、次のようにします。
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
document.body.appendChild(a);
a.download = 'foo.jpg';
a.href = url;
a.click();
a.remove();
URL.revokeObjectURL(url);
ここで blob と書いた箇所には Blob オブジェクトが入ります。
Blob は File API で定義されています。Blob は生のバイナリデータを表しています。URL.createObjectURL() メソッドで Blob を表すデータ URL を作成しています。
データ URL を保持していると、それに関連付けされた実際のデータが解放されないので、不要になったら URL.revokeObjectURL() を呼び出して、 URL を破棄します。
ちなみに、ブラウザによっては早めに破棄が行われてしまうことで、ファイルへの保存が失敗するという事例があったようです。 その場合は次のようにタイマーで破棄のタイミングを遅らせれば良いらしいです。
setTimeout(() => {
URL.revokeObjectURL(url);
}, 1E4);
万が一にもそんなリスクがあるなら、いつもタイマー付きでも良いのですけど。
さて、ここまでで、JavaScript 上のデータをファイルのダウンロードとして保存する方法がわかりました。
次にいくつか具体例を見てみましょう。
テキストデータをダウンロードする
それでは、 JavaScript からデータをダウンロードしてファイルに保存する簡単な例をみていきましょう。
上で見たように Blob オブジェクトさえ手に入れば、簡単にダウンロード保存できます。ポイントはどうやって Blob オブジェクトを作るか、というところになります。
テキストの Blob を作りダウンロードする
この例では次のように、ボタンを押すと "Hello, blob!" という文字の書いたテキストファイルがダウンロードされます。
HTML は次の通りです。ボタンがひとつあるだけの画面です。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Download Test 1</title>
<script src="test1.js"></script>
</head>
<body>
<p><button id="button1">Download Text</button></p>
</body>
</html>
次の JavaScript ファイルを、test1.js として上の HTML から取り込みます。
window.addEventListener('load', () => {
const button1 = document.getElementById('button1');
button1.addEventListener('click', button1_clicked);
});
function button1_clicked(evt) {
evt.preventDefault();
const blob = new Blob(["Hello, blob!"], {type: 'text/plain'});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
document.body.appendChild(a);
a.download = 'foo.txt';
a.href = url;
a.click();
a.remove();
URL.revokeObjectURL(url);
}
この例では Blob に文字列を渡して、MIME タイプを text/plain とすることで、 プレーンテキストデータを保持する Blob を作成しています。
Canvas データを PNG 画像として保存
次は Canvas から Blob データを作成して、それを PNG 画像として保存してみましょう。
Canvas データを Blob にして、Blob のデータ URL を作る
HTML は次の通りです。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
<script src="file6.js"></script>
</head>
<body style="margin:1rem;">
<div style="text-align:center;">
<canvas id="canvas1" width="250" height="250"></canvas>
<p><button class="btn btn-primary" id="button1">Download Image</button></p>
</div>
</body>
</html>
少しだけ見栄えを整えるために、Bootstrap の CSS を取り込んでいます。ボタンに設定した btn btn-primary という CSS クラスは Bootstrap のボタン用のクラスです。
JavaScript は次の通りです。
window.addEventListener('load', () => {
const button1 = document.getElementById('button1');
const canvas1 = document.getElementById('canvas1');
button1.addEventListener('click', button1_clicked);
canvas1.addEventListener('click', canvas1_clicked);
draw_grid();
});
// グリッドを描く
function draw_grid() {
const c = document.getElementById('canvas1');
const ctx = c.getContext('2d');
ctx.beginPath();
for (let i = 0; i >= 25; i++) {
ctx.moveTo(i * 10, 0);
ctx.lineTo(i * 10, 250);
}
for (let i = 0; i >= 25; i++) {
ctx.moveTo(0, i * 10);
ctx.lineTo(250, i * 10);
}
ctx.strokeStyle = '#aaaaaa';
ctx.lineWidth = 1;
ctx.stroke();
}
// Canvas に描かれた内容を PNG 画像としてダウンロードする
function button1_clicked(evt) {
evt.preventDefault();
const c = document.getElementById('canvas1');
c.toBlob((blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
document.body.appendChild(a);
a.download = 'foo.png';
a.href = url;
a.click();
a.remove();
setTimeout(() => {
URL.revokeObjectURL(url);
}, 1E4);
}, 'image/png');
}
// Canvas をクリックした場所に円を描く
function canvas1_clicked(evt) {
evt.preventDefault();
const x = evt.offsetX;
const y = evt.offsetY;
const r = 50;
const c = document.getElementById('canvas1');
const ctx = c.getContext('2d');
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2, true);
ctx.strokeStyle = '#ff0000';
ctx.lineWidth = 5;
ctx.stroke();
}
この JavaScript では、何をしているのでしょうか。
まず、window グローバルオブジェクトの load イベントを利用して、イベントリスナーの設定と、グリッド線の描画をしています。
イベントリスナーは Canvas のクリックイベントとボタンのクリックイベントを設定しています。
Canvas のクリックイベントでは、Canvas に赤い円を描画しています。
ボタンのクリックイベントハンドラでは、Canvas の toBlob() メソッドを使って Blob を作成しています。
そして作成した Blob をダウンロードします。この時もダウンロードの方法はテキストの時と同じで、a 要素を作成して、download 属性を設定した上で、 click() を呼ぶことでダウンロードしています。
サーバーからダウンロードした PDF ファイルを保存
最後の例では、PDF ファイルをサーバー側で動的に作成して、その内容を PDF ファイルにダウンロードしてみましょう。
次のように文字を入力してボタンを押したら、その文字が書かれた PDF ファイルをサーバー側で作成します。 そして、クライアントにダウンロードしてファイルに保存します。
サーバー側の受け口は Node.js 上で Express を使いました。バックエンドには必ず Node.js と Express を使わないといけないというわけではありません。
今回 PDF を作成するために PDFKit を使用しています。
PDF の生成については次の記事を参考にしてください。
サーバー側の準備 Node.js+Express+PDFKit
Node.js に関する予備知識が必要です。ここでは簡単に手順だけ説明します。「Node.js 入門」なども参考にしてください。
次のコマンドで Node のプロジェクトを作成して、PDFKit をインストールします。
% mkdir pdf_test % cd pdf_test % npm init % npm install express body-parser pdfkit % mkdir -p fonts/M_PLUS_1p % mkdir public_html
ファイルは次のようなディレクトリ構成にして配置しています。
/fonts /M_PLUS_1p MPLUS1p-Regular.ttf /node_modules /public_html index.html test.js index.js package-lock.json package.json
フォントは Google Fonts からダウンロードしました。
日本語が使えるようにここでは日本語フォントの M Plus 1p を使っています。ファイル形式は TrueType フォントを選んでください。
index.js は次のようにします。
const express = require('express');
const http = require('http');
const bodyParser = require('body-parser');
const app = express();
const path = require('path');
const PDFDocument = require('pdfkit');
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, './public_html')));
app.post('/test1', (req, res) => {
let message = req.body.message;
if (!message) {
message = 'Please enter something :)';
}
let pdf = new PDFDocument({
size: 'LEGAL',
info: {
Title: 'Title!',
Author: 'Author!',
}
});
pdf
.lineWidth(10)
.strokeColor('blue')
.polygon([100, 50], [50, 150], [150, 150])
.stroke();
pdf
.font('fonts/M_PLUS_1p/MPLUS1p-Regular.ttf')
.fontSize(28)
.text(message, 40, 180);
pdf.pipe(res).on('finish', () => {
console.log('The PDF file was sent.');
});
pdf.end();
});
http.createServer(app).listen(
3000,
() => console.log('Listening on port 3000...')
);
以上で、サーバー側の準備はできました。
fetch を使ってサーバーからデータを取得してファイルに保存
引き続き、ここで作成した public_html ディレクトリ内に index.html と test.js を作成しましょう。
index.html は次の通りです。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
<script src="test.js"></script>
</head>
<body style="margin: 1rem;">
<p><input
type="text" id="text1"
class="form-control" style="width:300px;"
value="Hello, PDF!"></p>
<p><button
class="btn btn-primary"
id="button1">Download PDF</button></p>
</body>
</html>
Bootstrap で UI を整えているので、Bootstrap の CSS の取り込みや CSS クラスの設定のため、少しコードが増えています。
要は入力フィールドとボタンが一つずつあるだけです。
test.js は次の通りです。
これまでと同様に window の load イベントで、 ボタンのイベントハンドラを設定するところから始まります。
window.addEventListener('load', () => {
const button1 = document.getElementById('button1');
button1.addEventListener('click', button1_clicked);
});
function button1_clicked(evt) {
evt.preventDefault();
const text1 = document.getElementById('text1').value;
const data = {
message: text1
};
fetch('/test1',
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then((res) => {
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}`);
}
return res.blob();
})
.then((blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
document.body.appendChild(a);
a.download = 'foo.pdf';
a.href = url;
a.click();
a.remove();
setTimeout(() => {
URL.revokeObjectURL(url);
}, 1E4);
})
.catch((reason) => {
console.log(reason);
});
}
ここでのポイントは fetch() メソッドの二個目の then() で Blob オブジェクトを受け取り、 それをダウンロード保存しているところです。
fetch() メソッドの使い方については「fetch() の使い方」をみてください。
以上のコードを用意したら、次のコマンドで Node + Express サーバーをスタートします。
% node index.js
これでポート 3000 番で HTTP リクエストを待ち受けている状態になります。
ブラウザを開き、http://localhost:3000/ を要求することで、動作確認できるはずです。
以上、ここでは a タグの download 属性を JavaScript から動的に利用することで、 様々なデータをダウンロードできることをみてきました。