VueUse と Typescript で画像アップロードをリアクティブに表示してみた。

technologies

お疲れ様ですベンジャミン天早です。
今回は、Vue.jsのコンポーネントライブラリである VueUse と Typescript を組み合わせて
画像アップロード機能についてブログを書いていきたいと思います。

最後までお付き合い下さい。

作業環境

  • mac os 14.4.1
  • vue 3.5.13
  • TypeScript 5.7.2
  • node.js 18.20.5

前提作業

  • Node.js がインストールされていること
  • VScodeをセットアップしていること

※前提作業がまだの方は、下記記事を参考にしてみて下さい。🙇‍♂️

 Node.js ダウンロードサイト
https://nodejs.org/ja

 VScodeセットアップ方法について
https://www.kikagaku.co.jp/kikagaku-blog/visual-studio-code-windows/

目次

完成形

0.機能説明

今回はVue.jsで実装しました。
まず、機能の概要について簡単に説明します。

[App.vue]「ファイル選択」ボタンクリック時に画像選択ダイアログを表示する。(下記画像①)
[App.vue]ダイアログから画像を選択し保存する。(下記画像①)
[useFileReader.ts]選択した画像を読み込む。(下記画像②)
[useFileReader.ts]読み込んだ画像をApp.vueへ返却する。(下記画像③)
[useFileReader.ts]画像を表示する。(下記画像④)

1.Vue.jsのセットアップ

下記コマンドを実行しVue.jsのセットアップを実施していきます。

npm create vue@latest

上記コマンドを実行すると対話型のセットアップが開始されます。

1.1 プロジェクト / パッケージ名の入力

まず初めにプロジェクト名が問われるのでお好みのプロジェクト名を記載します。
ここでは、vue-picUp としておきます。

Vue.js - The Progressive JavaScript Framework

? Project name: › vue-picUp

入力が完了したらEnterキー を押します。
続けて下記のようにパッケージ名が問われますが何も入力せず、Enterキー を押します。

Package name: … vue-picup

1.2 各種機能、ツール追加について

次に各種機能、ツールを追加するかの質問が問われます
今回は、TypeScript を使用するので下記の様に設定します。

✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add an End-to-End Testing Solution? › No / Yes
✔ Add ESLint for code quality? › No / Yes

上記選択が完了すると、選んだ設定に基づいてプロジェクトの雛形が生成されます。

1.3 パッケージインストール

次に以下のようなコマンドが表示され実行することで、開発用サーバーを起動し、Vue.jsプロジェクトの開発を開始できます。

  cd vue-picUp
  npm install
  npm run dev

以下の様な画面が表示されれば成功です。

2.フォルダ構成 / 作業ファイル

続いてフォルダ構成について見ていきます。

・フォルダ構成

├── README.md
├── index.html
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
│   └── favicon.ico
├── src
│   ├── App.vue
│   ├── assets
│   │   ├── base.css
│   │   ├── logo.svg
│   │   └── main.css
│   ├── components
│   │   ├── HelloWorld.vue
│   │   ├── TheWelcome.vue
│   │   ├── WelcomeItem.vue
│   │   └── icons
│   └── main.ts
└── vite.config.ts

上記が今回のアプリのフォルダ構成になります。
赤字で示しているのが今回作業するApp.vueファイルになります。
別名ルートコンポーネントファイルと呼ばれ、アプリを開いた時、最初に表示される画面
です。

2.1 今回使わないファイルの削除

App.vueファイルに記述する前に今回使わない余計なファイルの削除と
main.tsファイルを訂正します。

・フォルダ構成

├── README.md
├── index.html
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
│   └── favicon.ico
├── src
│   ├── App.vue
│   ├── assets      ←削除
│   │   ├── base.css
│   │   ├── logo.svg
│   │   └── main.css
│   ├── components   ←削除
   │   ├── HelloWorld.vue
│   │   ├── TheWelcome.vue
│   │   ├── WelcomeItem.vue
   │   └── icons
│   └── main.ts
└── vite.config.ts

▼src/main.ts
↓削除
import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

3.画面表示確認

続いて、作業ファイルのApp.vueを訂正し、『TEST』という文字列が正しく表示されるか確認します。

▼src/App.vue  変更前

<script setup lang="ts"> ↓削除
import HelloWorld from './components/HelloWorld.vue'
import TheWelcome from './components/TheWelcome.vue'
</script>

<template>
  <header>  ↓削除
    <img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" />

    <div class="wrapper">  ↓削除
      <HelloWorld msg="You did it!" />
    </div>
  </header>

  <main> ↓削除
    <TheWelcome />
  </main>
</template>

<style scoped>
header {    ↓削除
  line-height: 1.5;
}

.logo {↓削除
  display: block;
  margin: 0 auto 2rem;
}

↓削除
@media (min-width: 1024px) {
  header {
    display: flex;
    place-items: center;
    padding-right: calc(var(--section-gap) / 2);
  }

↓削除
  .logo {
    margin: 0 2rem 0 0;
  }

↓削除
  header .wrapper {
    display: flex;
    place-items: flex-start;
    flex-wrap: wrap;
  }
}
</style>


▼src/App.vue  変更後

<script setup lang="ts"></ script>

<template>↓追記
  <h1>TEST</h1>
</template>

<style scoped></style>

▼追記解説
templateタグにh1要素でTESTと記載しています。

下記のようにブラウザ上でTESTと表示されていれば成功です。

4.VueUseパッケージインストール

Vue.jsのセットアップが完了したら
下記コマンドを実行し @vueuse/core パッケージをプロジェクトにインストールします。

npm install @vueuse/core

5.ファイル選択ボタン作成

続いてApp.vueファイルを下記の様に修正します。

・src/App.vue  変更前

<template>
  <h1>TEST</h1>
</template>

・src/App.vue  変更後

<script setup></script>

<template>
    <div> ↓追記
        <button class="button is-dark" type="button">ファイル選択</button>
        <br>
        <img alt=""/>
    </div>
</template>


<style scoped>
button{ ↓追記
  margin-top: 20px;
  margin-left: 20px;
}
</style>

▼追記解説
templateタグにbutton要素を定義し、背景色を黒くし、ボタンの位置をブラウザの上端から20px、右端から20px離れた位置に配置しています。

※ button要素にBulumacssのクラス名を記載しボタンに色合いを付け足しています。
本記事では、Bulumacssの使用方法については割愛させて頂きます。
気になる方は、下記公式サイトなどをご確認下さい

▼公式サイト
https://bulma.io/

6.ファイル操作・ダイアログ表示

次に下記動画のように「ファイル選択」ボタンをクリックしてダイアログ画面を表示させるまでを
実装します。

App.vueファイルを下記のように追記します。

・src/App.vue  変更前

<script setup></script>

<template>
    <div>
        <button class="button is-dark" type="button">ファイル選択</button>
        <br>
        <img alt=""/>
    </div>
</template>


・src/App.vue 変更後


<script setup> ↓追記①
import { useFileDialog } from "@vueuse/core"

const { open } = useFileDialog({
  accept: "img/*",
  multiple: false
})
</script>

<template>
    <div>                     ↓追記②
        <button class="button is-dark"type="button" @click="open">ファイル選択</button>
        <br>
        <img alt=""/>
    </div>
</template>

追記したcodeについて解説します。

▼①追記解説
ここでは、VueUseが提供しているユーティリティ関数(useFileDialog)をファイルにインポートし
上記関数の主要機能であるopen()を使い、許可するファイルタイプをimgとし複数ファイル選択を拒否 しています。

▼②追記解説
buttonタグにv-onディレクティブのclickイベントでopen関数を呼び出しています。

7.TypeScriptファイル作成 – 下準備

次に画像を読み取り処理を行うためTypeScriptファイルを作成します。
src配下に下記ファイルを新規作成します。

src/composables/ useFileReader.ts

・フォルダ構成

├── README.md
├── index.html
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
│   └── favicon.ico
├── src
│   ├── App.vue
│   ├── composables ←追加
│   │   └──useFileReader.ts
│   └── main.ts
└── vite.config.ts

上記で作成したtsファイルを下記の様に記載します。

・src/composables/useFileReader.ts

↓追記
export const useFileReader = () => {
    const read = () => {}
    return {
        read
    }
}

▼追記解説
ひとまず、readという関数を返すようにします。続きは、後ほど記載します。
次に上記tsファイルで定義したuseFileReader関数をApp.vueファイルでインポートしreader変数に格納します。

・src/App.vue 変更前


<script setup>
import { useFileDialog } from "@vueuse/core"

const { open } = useFileDialog({
  accept: "img/*",
  multiple: false
})
</script>

<template>
    <div>
        <button class="button is-dark"type="button" @click="open">ファイル選択</button>
        <br>
        <img alt=""/>
    </div>
</template>


・src/App.vue 変更後


<script setup>  ↓追記
import { useFileDialog } from "@vueuse/core" 
import { useFileReader } from "@/composables/useFileReader"

const reader = useFileReader()
const { open } = useFileDialog({
  accept: "img/*",
  multiple: false
})
</script>

<template>
    <div>
        <button class="button is-dark"type="button" @click="open">ファイル選択</button>
        <br>
        <img alt=""/>
    </div>
</template>

▼追記解説
useFileReader.tsで定義したuseFileReader()関数をreaderと命名し
App.vueファイルで利用できるよう定義しています。

8.画像ファイル読み込み処理実装 (App.vue)

次にファイルダイアログでファイルが選択されたらファイルの中身を読み込む処理を実装します。

・src/App.vue 変更前


<script setup>
import { useFileDialog } from "@vueuse/core" 
import { useFileReader } from "@/composables/useFileReader"

const reader = useFileReader()

const { open } = useFileDialog({
  accept: "img/*",
  multiple: false
})
</script>

<template>
    <div>
        <button class="button is-dark"type="button" @click="open">ファイル選択</button>
        <br>
        <img alt=""/>
    </div>
</template>

・src/App.vue  変更後


<script setup>
import { useFileDialog } from "@vueuse/core"
import { useFileReader } from "@/composables/useFileReader"

const reader = useFileReader()
              ↓追記
const { open ,onchange } = useFileDialog({
  accept: "img/*",
  multiple: false
})
              ↓追記
onChange((files) => {
  if (files.length > 0) {
    reader.read(files[0])
    }
})
</script>

<template>
    <div>
        <button class="button is-dark"type="button" @click="open">ファイル選択</button>
        <br>
        <img alt=""/>
    </div>
</template>

▼追記解説
onChange関数では、少なくとも1つのファイルが選択された場合に、
reader.read(files[0]) を呼び出して、選択された最初のファイルを読み取ります。
この処理により、ユーザーがファイル選択ダイアログでファイルを選択すると、そのファイルが read 関数によって読み取られるようになります。

9.画像ファイル読み込み処理実装 (useFileReader.ts)

続いてuseFileReader.ts(tsファイル)を更新します。

・src/App.vue  変更前

<script setup>
import { useFileDialog } from "@vueuse/core"
import { useFileReader } from "@/composables/useFileReader"

const reader = useFileReader()

const { open ,onchange } = useFileDialog({
  accept: "img/*",
  multiple: false
})

onChange((files) => {
  if (files.length > 0) {
    reader.read(files[0])
    }
})
</script>

<template>
    <div>
        <button class="button is-dark"type="button" @click="open">ファイル選択</button>
        <br>
        <img alt=""/>
    </div>
</template>

・src/App.vue 変更後


<script setup>
import { ref } from "vue"
import { useFileDialog } from "@vueuse/core"
import { useFileReader } from "@/composables/useFileReader"
             ↓追記
const src = ref('')
                             ↓追記
const reader = useFileReader((result) => src.value = result)

const { open,onChange } = useFileDialog({
  accept: "img/*",
  multiple: false
})

onChange((files) => {
  if (files.length > 0) {
    reader.read(files[0])
    }
})

</script>

<template>
    <div>
        <button class="button is-dark"type="button" @click="open">ファイル選択</button>
        <br> ↓追記
        <img v-if="src" :src="src" alt=""/>
    </div>
</template>

▼追記解説
ここでは、srcという空の文字列を定義し、画像が存在する場合に表示する処理を新たに追記しています。

最後にuseFileReader.tsファイルを下記のように更新して完了になります。

・src/composables/useFileReader.ts 変更前

export const useFileReader = () => {
    const read = () => {}
    return {
        read
    }
}

・src/components/useFileReader.ts  変更後

↓追記
import { onMounted } from "vue"
                              ↓追記
export const useFileReader = (onLoad: (result: string) => void) => {
let reader: FileReader
↓追記
const handleLoad = () => typeof reader.result === "string" && onLoad(reader.result)
onMounted(() => {
      reader = new FileReader
      reader.addEventListener("load", handleLoad)
    })
  

              ↓追記
const read = (blob: Blob) => reader.readAsDataURL(blob)
    
    return {
      read
    }
  }


▼追記解説
ここでは、Vueのライフサイクルフック onMounted をtsファイルにインポートし、useFileReader関数の引数としてonLoad関数を受け取ります。
FileReader オブジェクト(reader)を定義し、画像ファイル読み込み完了時にonLoad関数を実行します。
マウント時にhandleLoadをイベントリスナーとして登録し、Blobオブジェクトを readAsDataURL で読み込んでreadを返します。

10.おわりに

いかがでしたでしょうか。
今回投稿したブログは、既に設計されたファイルに当てはめていくだけで良いので、考える手間を少なく実装することができます。ぜひ興味のある方は試してみて下さい。

Related posts