🩱

Nuxt3で画像アップロード機能を作る

2022/11/24に公開

はじめに

Webブラウザ上にファイルをドラッグ&ドロップするとサーバにアップロードできる画像アップロード機能を作りました。コードは最近リリースされたNuxt3の正式版v3.0.0対応です。

使用するモジュールは、サーバ側がMulter 1.4.5-lts.1でクライアント側がDropzone 6.0.0-beta.2です。この組み合わせで非常にシンプルなコードで、使い勝手のよいアップローダができました。

動作環境

デモを動作させるにはNode v16.0以上が必要です。
特にプラットフォームに限定しないと思いますが、ここではプラットフォームOSはWindows10を使用しています。

デモ

デモプログラムは以下の場所にあります。GitHubのリポジトリをZipファイルでダウンロードしてください。

https://github.com/czbone/nuxt3-image-upload-demo

実行

デモのZipファイルを解凍し、ディレクトリ内でコンソールから以下のコマンドを実行します。

yarn install
yarn dev

起動後、Webブラウザでhttp://localhost:3000にアクセスします。

操作

エクスプローラからドラッグした画像ファイルをWebブラウザ上の枠線内にドロップします。
ドロップした画像ファイルは即座にサーバへアップロードされます。
サーバにアップロードされた画像ファイルはuploadフォルダに格納されます。

npmパッケージ

直接package.jsonに組み込んだnpmパッケージは、multervue2-dropzone-vue3です。

multer

サーバ側の画像ファイル受信部分はMulterを使用しています。
Multerはv2が出ていますが、APIが使いづらそうなのでv1を使用しました。

https://github.com/expressjs/multer

vue2-dropzone-vue3

Webブラウザ上で画像ファイルをドロップする部分はDropzoneを使用しています。

https://www.dropzone.dev/

Nuxt3のクライアントにDropzoneを組み込むために、Dropzoneをラップしたvue2-dropzone-vue3を使用しました。
vue2-dropzone-vue3ではDropzone6.0.0-beta.2がインストールされます。(以下のサイトでは名称がvue-dropzoneになっていますが、生成されるモジュールはvue2-dropzone-vue3です。)

https://github.com/twickstrom/vue-dropzone

プログラム解説

動作の中心となるプログラムを解説します。ここに載せているコードだけでほとんど動きます。

クライアント側

クライアント側のコードはpages/index.vueファイルでDropzoneの設定を行っています。
vue2-dropzone-vue3モジュールのデフォルトのCSSではアイコンの表示が崩れるのでCSSの修正を入れてます。

pages/index.vue
<template>
  <vue-dropzone
    ref="myVueDropzone"
    id="dropzone"
    :options="dropzoneOptions"
    @vdropzone-error="uploadError"
  />
</template>
<script>
import vueDropzone from 'vue2-dropzone-vue3'

export default {
  components: {
    vueDropzone,
  },
  data () {
    return {
      dropzoneOptions: {  // ...①
        url: '/api/file',
        thumbnailWidth: 150,  // サムネールの幅
        maxFilesize: 0.5, // ファイルの最大サイズ(Mバイト)
        dictFileTooBig: 'ファイルサイズの上限は {{maxFilesize}} MB です\n(size: {{filesize}} MB)',
        dictDefaultMessage: 'ファイルをここにドロップ、またはマウスクリックでアップロード',
      },
    }
  },
  methods:{
    uploadError(file, message, xhr){  // ...②
      if (message instanceof Object){   // サーバ側でエラーの場合はObjectが来る
        file.previewElement.querySelectorAll('.dz-error-message span')[0].textContent = message.message
      }
    }
  }
}
</script>
<style>
/* アイコンの表示位置を修正 */
.vue-dropzone > .dz-preview .dz-success-mark, .vue-dropzone > .dz-preview .dz-error-mark {
  width: 54px;
  left: 50%;
  margin-left: -27px;
}
.vue-dropzone > .dz-preview .dz-success-mark {
  background-color: green;
}
.vue-dropzone > .dz-preview .dz-error-mark {
  background-color: red;
}
</style>

Dropzoneのオプション

①でDropzoneのオプションを設定しています。
urlでアップロード先のURLを指定します。値の/api/fileがサーバ側で実行されるファイルはserver/api/file.jsです。

その他のDropzoneのオプションは以下にドキュメントがあります。

https://docs.dropzone.dev/configuration/basics/configuration-options

エラー処理

画像ファイルのアップロード処理では、エラーはクライアント側とサーバ側の両方で発生します。Dropzoneが検知したクライアントとサーバのエラーは②で処理しています。

Dropzoneのエラー処理でエラーメッセージを表示します。
以下のようにアップロードに失敗したファイルのエラーメッセージがサムネール上に表示されます。

左のサムネールは画像サイズが制限を超えた場合のエラーでクライアント側検知のエラーです。
右のサムネールはサーバからHTTPレスポンスエラー(422)を受信し、サーバ側で付加されたエラーメッセージをそのまま表示しています。

②のイベント関数では、クライアント側かサーバ側かのエラーの違いでmessageパラメータの型が異なります。クライアント側のエラー、つまりDropzoneが検知したエラーの場合はmessageパラメータはテキスト型になり、サーバ側のエラーの場合はObject型になります。
Dropzoneがサーバ側が正常かエラーかを判断するのは、HTTPレスポンスコードが200かそれ以外かです。
messageパラメータがObject型の場合は、サーバ側で付加されたメッセージをサムネール上に設定しています。クライアント側のテキスト型メッセージの場合は、既存の処理でサムネール上に設定されます。

サーバ側

サーバ側のコードはserver/api/file.jsファイルでMulterが処理します。
このファイルは、サーバAPIのURLパス/api/fileで起動されます。

server/api/file.js
import multer from 'multer'
import { callNodeListener } from 'h3'

const storage = multer.diskStorage({  // ...①
  // ファイルの保存先を指定
  destination: function (req, file, cb) {
    cb(null, "upload")
  },
  // ファイル名を指定(オリジナルのファイル名を保持)
  filename: function (req, file, cb) {
    cb(null, file.originalname)
  }
})

const upload = multer({
  storage: storage,
  fileFilter: (req, file, cb) => {  // ...②
    // 画像ファイルに制限
    if (file.mimetype == "image/png" || file.mimetype == "image/jpeg" || file.mimetype == "image/gif") {
      cb(null, true)
    } else {
      cb(new Error('MIMEタイプが不正です'))  // ...③
    }
  }
})

export default defineEventHandler(async (event) => {  // ...④
  try {
    await callNodeListener(upload.single('file'), event.req, event.res)
    return { success: true }
  } catch (e) {
    return createError({
      message: e.message,
      statusCode: 422,
      statusMessage: 'Unprocessable Entity'
    })
  }
})

MulterのAPI

外部から最初に実行されるのは④のdefineEventHandler()です。
callNodeListener()Multerの処理に移ります。

①の処理でdestinationfilenameなどでアップロードされたファイルの保存先を決めます。

②でアップロードできるファイルを制限します。ここではファイルの拡張子程度のチェックです。厳密にファイルタイプをチェックするにはファイルの内容をじかにチェックした方がよいでしょう。
コールバック関数cbの第1引数にErrorオブジェクトを渡すと、Multerはファイル保存しないで例外を発生させます。(③)

Multerのその他のAPIは、以下にドキュメントがあります。

https://github.com/expressjs/multer#api

エラー処理

Multerでエラーを検出例外は④のcatch部で捕捉され、クライアントへエラー通知します。

Nuxt3のサーバAPIの仕様では、defineEventHandler()が何らかの値をreturnで返せば、HTTPレスポンスコード200returnで返した値が返ります。
200以外のHTTPレスポンスコードを返したい場合はcreateError()を使用します。

Multerのエラーの場合はエラーメッセージをそのままクライアントに返しています。

おわりに

コールバック型からPromiseなど、Javascriptの使い方が大きく変化したのに加え、Nuxt3の関数の仕様も大きく変わっているので、それぞれのモジュールの動きを調整して統一感のある動きにまとめるのはなかなか難しいですね。
サーバ側のモジュールもいくつか試してみて、Multerが一番シンプルにうまくまとまりました。
応用もいろいろ利きそうなので、いろいろ拡張してみてください。

Discussion