導入
今回はVue3でこれからアッロードする画像ファイルのプレビューをリアクティブに表示するためのcomposableを作ってみるという話題です。
※ 私が株式会社愛宕 Advent Calendar 2023に書く記事は主に社内向けに共有しておきたいけど勉強会をするまでもないちょっとしたTipsにしたいと思います。
アップロード前にブラウザで画像ファイルの内容を一時的に表示したいとします。ローカルファイルは直接<img>
タグで読み込めないので一旦FileReader
で読み込んでからData URLに変換して<img>
タグのsrc
に書き込むことになります。
この一連の流れを1つのcomposableとして定義して再利用できるようにしてみましょう。
解決
ひとまず、read
という関数を返すようにします。
export const useFileReader = () => {
const read = () => {} // 未実装
return {
read
}
}
この関数は次のように使うことを想定しています。
(ファイルダイアログの扱いは本題とは無関係です。VueUseのuseFileDialogを使って書いています。)
<script setup lang="ts">
import { useFileDialog } from "@vueuse/core"
import { useFileReader } from "./composables/useFileReader"
const src = ref<string>()
const { open, onChange } = useFileDialog({
accept: "image/*",
multiple: false
})
const reader = useFileReader()
// ファイルダイアログでファイルが選択されたらファイルの中身を読み込む
onChange((files) => {
if (files.length > 0) {
reader.read(files[0])
// ここで src.value = reader.read(files[0]) のようには書けない
// FileReaderというのは同期的に読み込んだ内容を得られる仕組みではないので
// 読み込み処理の呼び出しだけを書く
// 読み込んだファイルの内容はどこから受け取ればいいだろう・・・
}
})
</script>
<template>
<div>
<button type="button" @click="open">ファイル選択</button>
<img v-if="src" :src="src" alt=""/>
<div v-else>画像未選択</div>
</div>
</template>
このままだと、src
を更新する処理が含まれていません。FileReader
を使ったファイル読み込みはaddEventListener
で追加できるイベントを使った処理で行います。useFileReader()
の呼び出し時にaddEventListener
に渡すコールバックを指定しておきましょう。
ここではファイルが読み込まれたらsrc
の値をその読み込んだ内容で更新することにします。
- const reader = useFileReader()
+ const reader = useFileReader((result) => src.value = result)
useFileReader.ts
側の呼び出し方も書き換えます。
- export const useFileReader = () => {
+ export const useFileReader = (onLoad: (result: string) => void) => {
const read = () => {} // 未実装
return {
read
}
}
次にFileReader
を実際に使った実装を書いていきます。
気をつけないといけないのはイベントリスナーの解除を忘れずに書いておくことです。
export const useFileReader = (onLoad: (result: string) => void) => {
let reader: FileReader
// onLoadはaddEventListenerに渡すためにもらってきた関数だが、
// 念の為、読み込み結果がstringであることを確認してから呼び出すことにする
const handleLoad = () => typeof reader.result === "string" && onLoad(reader.result)
onMounted(() => {
// マウントされたらreaderを作成してイベントリスナーを割り当てる
reader = new FileReader
reader.addEventListener("load", handleLoad)
})
onUnmounted(() => {
// 忘れずにアンマウントでイベントリスナーを外しておかないとメモリリーク
// こういうことを利用者が気にかけなくてもいいのがcomposableのいいところかもしれない
reader?.removeEventListener("load", handleLoad)
})
// readAsDataURLを使えば読み込み結果をData URLに変換してくれる
const read = (blob: Blob) => reader.readAsDataURL(blob)
return {
read
}
}
まとめ
今回はアッロードする画像ファイルのプレビューをリアクティブに表示するためのcomposableを作ってみました。
使い方は初めのuseFileReader
で読み込み完了後の動作を指定し、公開したread
関数で指定されたファイルオブジェクトの中身を読み込むといったものでした。
あまり使い方が直感的ではないので、情報の渡し方を逆にしてuseFileReader
でファイルオブジェクトを指定し、公開する関数はonLoad
にして読み込み完了時の処理を後で書けるようにするのもいいかもしれません。