0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

株式会社愛宕Advent Calendar 2023

Day 11

Vue3でこれからアッロードする画像ファイルのプレビューをリアクティブに表示するためのcomposableを作ってみる

Posted at

導入

今回はVue3でこれからアッロードする画像ファイルのプレビューをリアクティブに表示するためのcomposableを作ってみるという話題です。

※ 私が株式会社愛宕 Advent Calendar 2023に書く記事は主に社内向けに共有しておきたいけど勉強会をするまでもないちょっとしたTipsにしたいと思います。

アップロード前にブラウザで画像ファイルの内容を一時的に表示したいとします。ローカルファイルは直接<img>タグで読み込めないので一旦FileReaderで読み込んでからData URLに変換して<img>タグのsrcに書き込むことになります。

この一連の流れを1つのcomposableとして定義して再利用できるようにしてみましょう。

解決

ひとまず、readという関数を返すようにします。

composables/useFileReader.ts
export const useFileReader = () => {
  const read = () => {} // 未実装
  return {
    read
  }
}

この関数は次のように使うことを想定しています。
(ファイルダイアログの扱いは本題とは無関係です。VueUseのuseFileDialogを使って書いています。)

app.vue
<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の値をその読み込んだ内容で更新することにします。

app.vue の一部修正
- const reader = useFileReader()
+ const reader = useFileReader((result) => src.value = result)

useFileReader.ts側の呼び出し方も書き換えます。

composables/useFileReader.ts
- export const useFileReader = () => {
+ export const useFileReader = (onLoad: (result: string) => void) => {
  const read = () => {} // 未実装
  return {
    read
  }
}

次にFileReaderを実際に使った実装を書いていきます。
気をつけないといけないのはイベントリスナーの解除を忘れずに書いておくことです。

composables/useFileReader.ts
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にして読み込み完了時の処理を後で書けるようにするのもいいかもしれません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?