はじめに
画像アップロード機能を実装していたときにスコープに苦しめられた話です。
FileReaderやImageオブジェクトを使って苦手な非同期処理をしていたので、ハマっている原因が整理できずに泥沼化してました。。。
できごと
やりたいこと
-
<input type=’file’>
で画像選択時にリアルタイムで選択した画像が表示される - 最終的に選択した画像のbase64の値をサーバーに送信したいので、ファイルが選択されたらcanvas→blob→base64の流れでデータを変換する
参考にした記事
'input type=file'から'canvas'への転写
ほぼこちらのソースコードをコピーで実装
ソースコード
upload-component.vue
<template>
<div>
<div>
<input
type="file"
@change="uploadFile"
>
<!-- サーバーにリクエストする値 -->
<input
type="hidden"
name="icon_base64"
:value="iconBase64"
>
<canvas
id="canvas"
/>
</div>
</div>
</template>
<script>
export default {
data () {
return {
iconBase64: '',
}
},
methods: {
uploadFile(event) {
const file = event.currentTarget.files[0];
const readerForFile = new FileReader();
readerForFile.onload = function() {
// ファイルが読み込まれたあとの処理
const image = new Image();
image.onload = async function() {
// imageを転写するcanvasの取得、加工
const canvas = document.getElementById('canvas');
canvas.width = image.width;
canvas.height = image.height;
const context = canvas.getContext("2d");
// canvas(CanvasRenderingContext2D)に画像を転写
context.drawImage(image, 0, 0);
// canvas -> blobの変換
const blob = await this.canvasToBlob(canvas);
// blob -> base64の変換してプロパティに代入
this.iconBase64 = await this.readAsDataURL(blob);
}
// 画像がimageタグに読み込み->image.onloadイベント発火
image.src = readerForFile.result;
}
// Fileオブジェクトを読み込む->readerForFile.onloadイベント発火
readerForFile.readAsDataURL(file);
},
canvasToBlob(canvas) {
return new Promise((resolve) => {
canvas.toBlob(resolve, 'image/png');
});
},
readAsDataURL(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => {
resolve(reader.result);
};
reader.readAsDataURL(blob);
});
},
},
}
</script>
結果
以下エラーが発生
TypeError: canvasToBlob is not a function...
エラー箇所はココ。
// canvas -> blobの変換
const blob = await this.canvasToBlob(canvas);
でも、canvasToBlobは定義しているよ?ということでもう一度コードを読み返す。
結論
はい、そうです。thisのスコープでした。。。
今回以下のようにFileReaderやImageのonloadで非同期処理をしていて
readerForFile.onload = function() {
...
image.onload = async function() {
...
その中で
// canvas -> blobの変換
const blob = await this.canvasToBlob(canvas);
これをやっていると。
実際にcanvasToBlobを定義しているのはVueComponent内(※)で定義しているので、そりゃthis.canvasToBlob
でアクセスできないよねって話です。
※イメージ湧かない場合はvueのexport default内でconsole.log(this)やるとわかります。
対策
onload内でthis(=VueComponent)にアクセスしたい。でも、this.canvasToBlob
でアクセスできない。
ということでthisを変数化してしまえばOK
参考記事
Vue.jsのdataオブジェクトを参照する際に Uncaught (in promise) TypeError: Cannot set property 'foo' of undefined
ソースコード(修正後)
methods: {
uploadFile(event) {
const file = event.currentTarget.files[0];
const vm = this;
const readerForFile = new FileReader();
readerForFile.onload = function() {
// ファイルが読み込まれたあとの処理
const image = new Image();
image.onload = async function() {
...
...
// canvas -> blobの変換
const blob = await vm.canvasToBlob(canvas);
// blob -> base64の変換してプロパティに代入
this.iconBase64 = await vm.readAsDataURL(blob);
}
// 画像がimageタグに読み込み->image.onloadイベント発火
image.src = readerForFile.result;
}
// Fileオブジェクトを読み込む->readerForFile.onloadイベント発火
readerForFile.readAsDataURL(file);
},
canvasToBlob(canvas) {
...
},
...
},
const vm = this;
でVueComponentを示すthisを変数に格納してonload内でもcanvasToBlob
が呼べるようになりました!
おわりに
「こんだけ?」といわれるかもですが、すいません、すごくハマったのは事実なんです。。。
改めてJavaScriptのスコープの概念の理解不足が露呈した瞬間でした。。。
久しぶりにJavaScriptを触って、改めてJavaScriptに対してのアレルギーを感じることを実感したので、コツコツ言語仕様を理解しようかと思います。
最後までお読みいただきありがとうございます。