
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、この間 wordpressみたいなドラッグ・アンド・ドロップをVueで実装という記事を公開しました。この記事の目的はユーザビリティだったのですが、ドラッグ&ドロップ機能の先では画像のアップロードを想定していました。
最近のウェブサイトでは画像をサーバーへ保存するというのは珍しいことではないでしょうが、実はアップロードする側の状況が変化してきたことである問題が発生するようになってきました。
それが・・・
スマホ・カメラの高解像度化のせいで画像アップロードに時間がかかってしまう
というものです。
つまり、サイズが大きなファイルをアップロードするのに時間がかかってしまうということですね。
特にまだ日本のウェブサイトは、以下のように確認ページを挟む場合も多く存在していますので、その場合(バリデーションを厳密にするなら)画像サイズが大きい場合は1番と2番で画像をアップロードする必要がでてきます。
- 名前などを入力するページ (←ここで画像アップロード)
- 入力したものを確認するページ (←ここでも画像アップロード)
- 完了
これでは待ち時間が長くなってしまいますし、データ送信するごとにスマホの通信速度制限を気にしないといけなくなってしまいます。
そこで!
今回は選択された画像ファイルをブラウザでサイズ変更してからアップロードする方法をLaravel + Vueで実装してみます。
皆さんのお役にたてると嬉しいです♪
(最後に今回実際に開発したコードをダウンロードできます)
開発環境: Laravel 5.8、Vue 2.6(JavaScriptの記述法はES6)
目次 [非表示]
HTML部分をつくる
まずはベースとなるHTML部分を作っていきます。
といっても、Vue
とaxios
をcdn
で読み込み、それぞれイベントがついた「ファイル選択ボックス」と「送信ボタン」を配置したシンプルなものです。
※ なお、複数選択に対応しています。
また、Vueの変数の中身は以下のとおりです。
- maxWidth ・・・ リサイズする画像の最大幅
- smallImages ・・・ リサイズされた画像の配列
実際のコードはこうなります。
<html>
<body>
<div id="app">
<input type="file" accept="image/*" multiple @change="onImageChange">
<br>
<button type="button" @click="onSubmit">送信</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js"></script>
<script>
new Vue({
el: '#app',
data: {
maxWidth: 300,
smallImages: []
},
methods: {
onSubmit() {
// ここでAjax送信
},
onImageChange() {
// ここで画像をリサイズ
}
}
});
</script>
</body>
</html>
JavaScript部分をつくる
ではJavaScript部分を見ていきましょう。
実装方法の手順
ブラウザ上で画像をリサイズするには以下の手順が必要になります。
- HTML5のcanvas要素をつくる(表示はしません)
- canvasのサイズを変更
- 選択画像をcanvasに描画
- 描画した画像データを取得
画像をリサイズする部分
では、今回記事のメインになるJavaScriptで選択画像をリサイズする部分onImageChange()
をつくっていきます。
まずは実際のコードから。
onImageChange(e) {
this.smallImages = [];
const files = e.target.files;
for(let file of files) {
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
let img = new Image();
img.onload = () => {
let width = img.width;
let height = img.height;
if(width > this.maxWidth) {
height = Math.round(height * this.maxWidth / width);
width = this.maxWidth;
}
let canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
let ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
ctx.canvas.toBlob((blob) => {
const imageFile = new File([blob], file.name, {
type: file.type,
lastModified: Date.now()
});
this.smallImages.push(imageFile);
}, file.type, 1);
};
img.src = e.target.result;
};
}
}
少しコードが複雑ですがひとつずつ説明していきます。
まず、画像が選択されたらループの中で全ファイルをFileReader
を通して読み出します。
そしてその読み出しが完了したらImage
オブジェクトを作成しonload
の中で、その画像と同じサイズのcanvasに描画していくのですが、この時、もし画像の幅をチェックして最大値(上の例では300px)よりも大きければcanvasのサイズを変更してから画像を描画することでリサイズを実現しています。
また、画像の描画が完了したらtoBlob()
を使って、File
オブジェクトを作成しsmallImages
配列にひとつずつ格納していきます。
※ ちなみにIE 11ではtoBlob()
はサポートされていないので、JavaScript Canvas to Blobとうライブラリを使うといいでしょう。
【追記:2019.4.24】IE 11の場合 File API
でもエラーが発生してしまうことをご指摘いただきました。もしIE 11で利用したい場合は以下のように直接blog
にデータを追加してみてください。(ただし、ES6で紹介していますのでコード自体をbabel
などで変換する必要があります)
ctx.canvas.toBlob((blob) => {
blob.lastModifiedDate = new Date();
blob.name = file.name;
this.smallImages.push(blob);
if(this.smallImages.length == files.length) {
this.isResizing = false; // リサイズ完了!
}
}, file.type, 1);
なお、コードの中にでてくるfile.name
はファイル名、file.type
はそのファイルのmime-type
(image/png
など)が入っています。
データ送信する部分
リサイズされた画像データを取得できれば、後はAjaxで送信するだけです。
画像データはFormData
を使うといいでしょう。
実際のコードです。
methods: {
onSubmit() {
let formData = new FormData;
this.smallImages.forEach((smallImage) => {
formData.append('images[]', smallImage);
});
axios.post('/resize_ajax', formData)
.then((response) => {
// Ajax通信が成功した時
});
},
// 省略
注意すべき点
実は上のコードには入っていませんが、気をつけておくべき点があります。
それは、処理のタイミングです。
つまり、FileReader()
やImage()
は、非同期で実行されるので「まだ画像のリサイズが終わってないのに送信ボタンが押せてしまう」という状況ができてしまうわけです。ブラウザを実行するデバイスがロースペックの場合は特に時間がかかるので、気をつける必要があるでしょう。
そのため、true / false の変数をひとつ作り、リサイズが完了するまでは送信ボタンをクリックできなくするという方法を私はよく使います。
では実際のコードを見てみましょう。
(HTML部分)
<input type="file" accept="image/*" :disabled="isResizing" multiple @change="onImageChange">
(JavaScript部分)
data: {
// 省略
isResizing: false
},
onImageChange(e) {
this.isResizing = true;
// 省略
for(let file of files) {
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
let img = new Image();
img.onload = () => {
// 省略
ctx.canvas.toBlob((blob) => {
// 省略
if(this.smallImages.length == files.length) {
this.isResizing = false; // リサイズ完了!
}
}, file.type, 1);
};
img.src = e.target.result;
};
}
}
こうすることで、画像のリサイズを実行している間はinput
のdisabled
が有効になってボタンがクリックできなくなります。(お好みでv-if
を使って「リサイズ中…」などのテキストと表示を切り替えてもいいでしょう。)
Laravel部分
では最後にデータ送信する先のLaravel部分のコードを見てみましょう。
public function resize_ajax(Request $request) {
if($request->has('images')) {
foreach ($request->images as $photo) {
$photo->store('images'); // storage/app/imagesフォルダに保存
}
}
}
中身はシンプルで、images
というデータ(中身は画像のデータですが、配列なので、hasFile()
は使っていません)が存在しているかをチェックし、もし見つかった場合はループを使ってひとつずつデータを保存しているだけです。
実際のコードをダウンロード
今回実際に開発したソースコードを以下からダウンロードすることができます。
※ ただし、Laravel部分はご自身で実装してください。
大きな画像をJavaScriptでリサイズしてからAjax送信おわりに
ということで、今回はスマホやPCなどのクライアント側で選択した画像をリサイズしてから送信するテクニックをご紹介しました。
この方法を使うと、以下3つのメリットがゲットできることになります。
- 画像のアップロードが高速になる
- スマホなどの通信制限に優しくなる
- サーバー側の負荷が減る
逆にデメリットとなると、画像を選択してから送信可能になるまで少しだけ時間がかかるってことだけですが、最近のデバイスは高機能なのでこちらもあまり気にする必要はないかもしれません。
ぜひ皆さんもこのテクニックで省力化したウェブ開発をしてみてくださいね。
※ なお、VueでAjax送信するためのJSライブラリ「v-api-form」にも今回のリサイズ機能を追加しておきましたので、よろしければ使ってみてくださいね。
ではでは〜!