高速に画像アップロードがされているように見せる簡単な方法


Web アプリケーションで画像のアップロードはよくある機能です。その画像アップロードを高速にされているように見せることでユーザ体験を良くする（かもしれない）方法を紹介します。いたってよくある普通のアプローチですが、手軽にできるので参考にしてみてください。

## なんでこんなことをするの？

最近のモバイル機器ではカメラ性能の向上と共にファイルサイズも向上しています。つまり回線環境が良かったとしてもアップロード時間がそれなりにかかり、さらにサーバサイドでリサイズ処理をしていたりすると、より時間がかかります。

そこで画面上にはアップロードする前に、画像を表示させておいて裏でアップロード処理を行う非同期処理にすることで、ユーザとしては高速にアップロードがされたというフィードバックを与えることができます。

## 実装例

今回は Vue.js を使ったコードでサンプルコードを紹介します。Vue.js 関係なく JavaScript の話なので、どんなフレームワークでも使えると思います。

### 普通にアップロードして画像を表示する

まずは普通に画像を選択して POST アップロードして表示するサンプルコードです。アップロードが完了すると JSON で画像の URL が返却されるので、それを表示します。アップロード中はインジケータのアニメーションを表示します。

![1.gif](https://qiita-image-store.s3.amazonaws.com/0/2152/74c1fa1b-8a74-8236-9662-b06d621c0852.gif "1.gif")


アニメーション GIF で動作イメージをキャプチャしました。これは 4G 回線 (上り 22Mbps) をシミュレーションし、約 2MB 程度の画像ファイルをアップロードしています。遅くはないけど、アップロードしているなという感じはあります。

```vue
<template>
  <div>
    <p><input type="file" @change="upload" accept="image/*" /></p>
    <indicator :loading="loading" />
    <dl v-if="image">
      <dt>アップロードした画像</dt>
      <dd><img :src="image"></dd>
    </dl>
  </div>
</template>

<script>
import axios from 'axios'
import Indicator from './Indicator'
export default {
  name: 'HelloWorld',
  components: {
    Indicator
  },
  data () {
    return {
      image: '',
      loading: false
    }
  },
  methods: {
    upload (e) {
      this.loading = true
      const file = e.target.files[0]
      const reader = new FileReader()
      reader.onload = (e) => {
        this.postData(e.target.result)
      }
      reader.readAsDataURL(file)
    },
    postData (image) {
      const params = new FormData()
      params.append('image', image)
      axios.post('http://0.0.0.0:9999/', params).then(res => {
        this.loading = false
        this.image = res.data.url
      })
    }
  }
}
</script>

<style scoped>
  dl, dt, dd {
    margin: 0;
    padding: 0;
  }
  img {
    width: 90%;
    margin: auto;
  }
</style>
```

### すぐに画像を表示させる

次に画像を選択したときにもう表示をさせてしまって非同期でアップロード処理をし、アップロードが完了したら改めて差し替えるという方法に変更してみます。

![2.gif](https://qiita-image-store.s3.amazonaws.com/0/2152/80109454-190e-e7a4-4ac8-9ea359729378.gif "2.gif")

キャプチャを見てもらうとわかりますが高速に表示されているのがわかります。アップロードを待たずに表示させているので当然ですが、こちらのほうがユーザ体験が良くなるシーンはあると思います。

やっていることはシンプルで `new FileReader()` で画像ファイルを読み込んだ Base64 エンコーディングされた `data:` URL のリソースを当てているだけです。その後、POST した正規の URL に差し替えをしています。

```vue
<template>
  <div>
    <p><input type="file" @change="upload" accept="image/*" /></p>
    <indicator :loading="loading" />
    <dl v-if="image">
      <dt>アップロードした画像</dt>
      <dd><img :src="image"></dd>
    </dl>
  </div>
</template>

<script>
import axios from 'axios'
import Indicator from './Indicator'
export default {
  name: 'HelloWorld',
  components: {
    Indicator
  },
  data () {
    return {
      image: '',
      loading: false
    }
  },
  methods: {
    upload (e) {
      this.loading = true
      const file = e.target.files[0]
      const reader = new FileReader()
      reader.onload = (e) => {
        this.loading = false
        // ここで base64 された画像データを表示させる
        this.image = e.target.result
        this.postData()
      }
      reader.readAsDataURL(file)
    },
    postData () {
      const params = new FormData()
      params.append('image', this.image)
      axios.post('http://0.0.0.0:9999/', params).then(res => {
        // 改めて画像を差し替える
        this.image = res.data.url
      })
    }
  }
}
</script>

<style scoped>
  dl, dt, dd {
    margin: 0;
    padding: 0;
  }
  img {
    width: 90%;
    margin: auto;
  }
</style>
```

ただし、実際にはアップロードが完了しているわけではないのでブラウザを閉じたり、画面遷移するとアップロード処理が失敗するということがあります。採用する際には気をつけてください。

ブラウザを閉じる際にアラートを表示させるには `beforeunload` イベントを利用するといいかもしれません。

- https://developer.mozilla.org/ja/docs/Web/Reference/Events/beforeunload

```javascript
created () {
  window.addEventListener('beforeunload', (e) => {
    e.returnValue = ''
  })
}
```