56
46

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Vue #3Advent Calendar 2019

Day 5

Vue.js で画像アップロード機能をシンプルに作ってみよう!!

Last updated at Posted at 2019-12-04

ご挨拶

サァ Qiita の皆さんごきげんよう!!!

本日記事アップの木曜日といえば、石黒正数の『木曜日のフルット』ですが、アップといえば画像アップロードですね!!!皆さん画像のアップロードしてますか??????!!!!!!!

Vue.js を使って画像アップロードを実装する機会が何度かあったので、その知見を非常にミニマムにまとめて紹介します!時間がないからといってチープな記事を書く言い訳にはしない!!!書くなら本気の人間は誰のことだ????

オレダァ!!!!!!!!!!!!!!!!!!!!!!!!!!!!

下準備

ではまず面倒なのでプロジェクトは vue-cli でサクッと作ってしまいましょう。 vue-cli をインストールしておきましょう。

$ npm install -g @vue/cli
$ vue create sakura

cli で設定どうするか聞かれますが今回はデフォルトでいきましょう。

Vue CLI v4.1.1
? Please pick a preset:
❯ default (babel, eslint)
  Manually select features

プロジェクトが出来たら早速サーバーを起動してみましょう!!!

$ cd sakura
$ npm run serve
スクリーンショット 2019-12-04 23.10.03.png

お決まりのあれが出ましたね!!!でももうこいつとはおさらばです。容赦なく生成されれたコンポーネントを削除します。

$ rm src/components/HelloWorld.vue

このままだと HelloWorld.vue というコンポーネントがないみたいに怒られるので、仮に Upload.vue という名前のコンポーネントを作成しておきましょう!!!
それから、親コンポーネントたる App.vue も作成した Upload.vue を呼び出すようにしておきましょう。

src/components/Upload.vue
<template>
  <div>
    アップロード!!!!!!!!!!
  </div>
</template>

<script>
export default {
  name: 'Upload'
}
</script>
src/App.vue
<template>
  <div id="app">
    <Upload />
  </div>
</template>

<script>
import Upload from './components/Upload.vue'

export default {
  name: 'app',
  components: {
    Upload
  }
}
</script>

さてここまででで画面の様子を見てみましょうか。

スクリーンショット 2019-12-04 23.23.17.png

渋い!!!!!!
非常に渋いサイトが出来たので、適当に git commit して、本題である画像アップロード機能を実装してみましょう!!

親コンポーネントの準備

今回の構想では、 Upload.vue コンポーネントで生成した画像情報を base64エンコードして文字列として親コンポーネントである App.vue で受け取って、画像として再度表示してみたいと思います!!!!!

ということで、親コンポーネントの実装を先にやっちゃいましょう!!!!GOGO!!

diff --git a/src/App.vue b/src/App.vue
index ca84756..e7a00be 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,6 +1,7 @@
 <template>
   <div id="app">
-    <Upload />
+    <upload v-model="picture" />
+    <img :src="picture" />
   </div>
 </template>

@@ -11,6 +12,11 @@ export default {
   name: 'app',
   components: {
     Upload
+  },
+  data() {
+    return {
+      picture: null
+    }
   }
 }
 </script>

ついでにチョットだけスタイルの準備も進めておきましょう。単に気分を上げるためです。

diff --git a/src/App.vue b/src/App.vue
index e7a00be..577250f 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -20,3 +20,21 @@ export default {
   }
 }
 </script>
+
+<style>
+* {
+  box-sizing: border-box;
+}
+
+html {
+  font-size: 62.5%;
+}
+
+body {
+  color: #2c2d30;
+  font-size: 1.6rem;
+  font-family: "Hiragino Kaku Gothic Pro", "ヒラギノ角ゴ Pro W3", "Meiryo",
+    "メイリオ", "Osaka", "MS PGothic", arial, helvetica, clean, sans-serif;
+  line-height: 1.5;
+}
+</style>

これで親コンポーネントで画像を受け取って表示する準備ができましたね!!!オラワクワクすっぞ!!!

子コンポーネントの準備

続いて Upload.vue の最低限の実装を行っていきましょう。

diff --git a/src/components/Upload.vue b/src/components/Upload.vue
index 77a5175..659d3b5 100644
--- a/src/components/Upload.vue
+++ b/src/components/Upload.vue
@@ -1,11 +1,120 @@
 <template>
   <div>
-    アップロード!!!!!!!!!!
+    <label v-if="!value" class="upload-content-space user-photo default">
+      <input ref="file" class="file-button" type="file" @change="upload" />
+      アップロードする
+    </label>
+
+    <div v-if="value" class="uploaded">
+      <label class="upload-content-space user-photo">
+        <input ref="file" class="file-button" type="file" @change="upload" />
+        <img class="user-photo-image" :src="value" />
+      </label>
+    </div>
   </div>
 </template>

 <script>
 export default {
-  name: 'Upload'
+  name: 'Upload',
+  props: {
+    value: {
+      type: String,
+      default: null
+    }
+  },
+  data() {
+    return {
+      file: null
+    }
+  },
+  methods: {
+    async upload(event) {
+      const files = event.target.files || event.dataTransfer.files
+      const file = files[0]
+
+      if (this.checkFile(file)) {
+        const picture = await this.getBase64(file)
+        this.$emit('input', picture)
+      }
+    },
+    getBase64(file) {
+      return new Promise((resolve, reject) => {
+        const reader = new FileReader()
+        reader.readAsDataURL(file)
+        reader.onload = () => resolve(reader.result)
+        reader.onerror = error => reject(error)
+      })
+    },
+    checkFile(file) {
+      let result = true
+      const SIZE_LIMIT = 5000000 // 5MB
+      // キャンセルしたら処理中断
+      if (!file) {
+        result = false
+      }
+      // jpeg か png 関連ファイル以外は受付けない
+      if (file.type !== 'image/jpeg' && file.type !== 'image/png') {
+        result = false
+      }
+      // 上限サイズより大きければ受付けない
+      if (file.size > SIZE_LIMIT) {
+        result = false
+      }
+      return result
+    }
+  }
 }
 </script>
+
+<style scoped>
+.user-photo {
+  cursor: pointer;
+  outline: none;
+}
+
+.user-photo.default {
+  align-items: center;
+  background-color: #0074fb;
+  border: 1px solid #0051b0;
+  border-radius: 2px;
+  box-sizing: border-box;
+  display: inline-flex;
+  font-weight: 600;
+  justify-content: center;
+  letter-spacing: 0.3px;
+  color: #fff;
+  height: 4rem;
+  padding: 0 1.6rem;
+  max-width: 177px;
+}
+
+.user-photo.default:hover {
+  background-color: #4c9dfc;
+}
+
+.user-photo.default:active {
+  background-color: #0051b0;
+}
+
+.user-photo-image {
+  max-width: 85px;
+  display: block;
+}
+
+.user-photo-image:hover {
+  opacity: 0.8;
+}
+
+.file-button {
+  display: none;
+}
+
+.uploaded {
+  align-items: center;
+  display: flex;
+}
+</style>

とりあえずここでは最低限の以下のことができるようにしてみました。これ半分完成だろ・・・

  • 画像のローカルマシンからの読み込み
  • 画像のサムネイル表示
  • 画像のbase64エンコード
  • 親コンポーネントにエンコード済み画像データをバインド

では順を追って説明しましょう。

アップロードボタンを押すと change event を拾って upload method が呼ばれます。

    async upload(event) {
      const files = event.target.files || event.dataTransfer.files
      const file = files[0]

      if (this.checkFile(file)) {
        const picture = await this.getBase64(file)
        this.$emit('input', picture)
      }
    },

ここでは event 情報から取得した file に関する情報を取得した後、 checkFile method に該当 file データを渡して、ファイル形式などが仕様的に問題がないかどうかをチェックしています。

    checkFile(file) {
      let result = true
      const SIZE_LIMIT = 5000000 // 5MB
      // ローカルマシンからの読み込みをキャンセルしたら処理中断
      if (!file) {
        result = false
      }
      // jpeg か png 関連ファイル以外は受付けない
      if (file.type !== 'image/jpeg' && file.type !== 'image/png') {
        result = false
      }
      // 上限サイズより大きければ受付けない
      if (file.size > SIZE_LIMIT) {
        result = false
      }
      return result
    }

問題がないと判断された場合、 getBase64 method に再度該当 file データを渡して FileReader のインスタンスメソッドを利用してエンコードを行います。

    getBase64(file) {
      return new Promise((resolve, reject) => {
        const reader = new FileReader()
        reader.readAsDataURL(file)
        reader.onload = () => resolve(reader.result)
        reader.onerror = error => reject(error)
      })
    },

このエンコード済み画像情報の値を、親コンポーネントである App.vuethis.$emit('input', picture) として返します。

※ なお、ここのイベント名が input なのは、 v-modelv-bind:valuev-on:input のシンタックスシュガーだからです。 そういえば props として value を受け取っていましたね。

a.gif

小さい方が Upload.vue コンポーネントでサイズをスタイル制御している方、大きいほうが親コンポーネント App.vue に渡された情報を読み込んでいる方です。成功しましたね!!

では仕上げに削除機能とエラーメッセージ表示機能をつけましょう!!

diff --git a/src/components/Upload.vue b/src/components/Upload.vue
index 36dcddb..59b0f4d 100644
--- a/src/components/Upload.vue
+++ b/src/components/Upload.vue
@@ -10,7 +10,17 @@
         <input ref="file" class="file-button" type="file" @change="upload" />
         <img class="user-photo-image" :src="value" />
       </label>
+
+      <button type="button" class="delete-button" @click="deleteImage">
+        削除する
+      </button>
     </div>
+
+    <ul v-if="fileErrorMessages.length > 0" class="error-messages">
+      <li v-for="(message, index) in fileErrorMessages" :key="index">
+        {{ message }}
+      </li>
+    </ul>
   </div>
 </template>

@@ -25,7 +35,8 @@ export default {
   },
   data() {
     return {
-      file: null
+      file: null,
+      fileErrorMessages: []
     }
   },
   methods: {
@@ -38,6 +49,10 @@ export default {
         this.$emit('input', picture)
       }
     },
+    deleteImage() {
+      this.$emit('input', null)
+      this.$refs.file = null
+    },
     getBase64(file) {
       return new Promise((resolve, reject) => {
         const reader = new FileReader()
@@ -48,6 +63,7 @@ export default {
     },
     checkFile(file) {
       let result = true
+      this.fileErrorMessages = []
       const SIZE_LIMIT = 5000000 // 5MB
       // キャンセルしたら処理中断
       if (!file) {
@@ -55,10 +71,12 @@ export default {
       }
       // jpeg か png 関連ファイル以外は受付けない
       if (file.type !== 'image/jpeg' && file.type !== 'image/png') {
+        this.fileErrorMessages.push('アップロードできるのは jpeg画像ファイル か png画像ファイルのみです。')
         result = false
       }
       // 上限サイズより大きければ受付けない
       if (file.size > SIZE_LIMIT) {
+        this.fileErrorMessages.push('アップロードできるファイルサイズは5MBまでです。')
         result = false
       }
       return result
@@ -114,4 +132,24 @@ export default {
   align-items: center;
   display: flex;
 }
+
+.delete-button {
+  background-color: #fff;
+  border: none;
+  color: #0074fb;
+  margin-left: 2rem;
+  padding: 0;
+}
+
+.delete-button:hover {
+  text-decoration: underline;
+}
+
+.error-messages {
+  color: #cf0000;
+  list-style: none;
+  margin: 0.4rem 0 0 0;
+  padding: 0 0.2rem;
+  font-size: 1.6rem;
+}
 </style>

b.gif

無事、謎の音楽ファイルをアップロードしようとしたら怒られちゃいましたね。削除もできて便利になりました。

終わりの言葉

サァ画像プレビューが簡単にできちゃいましたね!!!!!!

本当は画像ファイルを ajax でバックエンドにアップして表示、ダウンロードするところまでちゃんと説明したかったんですが、今回はシンプルに base64 で画面表示する感じですませてみました。

そうそう、フルットといえば、モニカの季節ですね!!!皆さんメリークリスマス!!!!!!!!!!!!!

56
46
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
56
46

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?