Nuxt3で画像アップロード機能を作る
はじめに
Webブラウザ上にファイルをドラッグ&ドロップするとサーバにアップロードできる画像アップロード機能を作りました。コードは最近リリースされたNuxt3の正式版v3.0.0
対応です。
使用するモジュールは、サーバ側がMulter 1.4.5-lts.1
でクライアント側がDropzone 6.0.0-beta.2
です。この組み合わせで非常にシンプルなコードで、使い勝手のよいアップローダができました。
動作環境
デモを動作させるにはNode v16.0以上が必要です。
特にプラットフォームに限定しないと思いますが、ここではプラットフォームOSはWindows10を使用しています。
デモ
デモプログラムは以下の場所にあります。GitHubのリポジトリをZipファイルでダウンロードしてください。
実行
デモのZipファイルを解凍し、ディレクトリ内でコンソールから以下のコマンドを実行します。
yarn install
yarn dev
起動後、Webブラウザでhttp://localhost:3000にアクセスします。
操作
エクスプローラからドラッグした画像ファイルをWebブラウザ上の枠線内にドロップします。
ドロップした画像ファイルは即座にサーバへアップロードされます。
サーバにアップロードされた画像ファイルはupload
フォルダに格納されます。
npmパッケージ
直接package.jsonに組み込んだnpmパッケージは、multer
とvue2-dropzone-vue3
です。
multer
サーバ側の画像ファイル受信部分はMulterを使用しています。
Multerはv2が出ていますが、APIが使いづらそうなのでv1を使用しました。
vue2-dropzone-vue3
Webブラウザ上で画像ファイルをドロップする部分はDropzoneを使用しています。
Nuxt3のクライアントにDropzone
を組み込むために、Dropzone
をラップしたvue2-dropzone-vue3
を使用しました。
vue2-dropzone-vue3
ではDropzone
の6.0.0-beta.2
がインストールされます。(以下のサイトでは名称がvue-dropzone
になっていますが、生成されるモジュールはvue2-dropzone-vue3
です。)
プログラム解説
動作の中心となるプログラムを解説します。ここに載せているコードだけでほとんど動きます。
クライアント側
クライアント側のコードはpages/index.vue
ファイルでDropzone
の設定を行っています。
vue2-dropzone-vue3
モジュールのデフォルトのCSSではアイコンの表示が崩れるのでCSSの修正を入れてます。
<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のオプションは以下にドキュメントがあります。
エラー処理
画像ファイルのアップロード処理では、エラーはクライアント側とサーバ側の両方で発生します。Dropzone
が検知したクライアントとサーバのエラーは②で処理しています。
Dropzone
のエラー処理でエラーメッセージを表示します。
以下のようにアップロードに失敗したファイルのエラーメッセージがサムネール上に表示されます。
左のサムネールは画像サイズが制限を超えた場合のエラーでクライアント側検知のエラーです。
右のサムネールはサーバからHTTPレスポンスエラー(422)を受信し、サーバ側で付加されたエラーメッセージをそのまま表示しています。
②のイベント関数では、クライアント側かサーバ側かのエラーの違いでmessage
パラメータの型が異なります。クライアント側のエラー、つまりDropzone
が検知したエラーの場合はmessage
パラメータはテキスト型になり、サーバ側のエラーの場合はObject型になります。
Dropzone
がサーバ側が正常かエラーかを判断するのは、HTTPレスポンスコードが200かそれ以外かです。
message
パラメータがObject型の場合は、サーバ側で付加されたメッセージをサムネール上に設定しています。クライアント側のテキスト型メッセージの場合は、既存の処理でサムネール上に設定されます。
サーバ側
サーバ側のコードはserver/api/file.js
ファイルでMulter
が処理します。
このファイルは、サーバAPIのURLパス/api/file
で起動されます。
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
の処理に移ります。
①の処理でdestination
、filename
などでアップロードされたファイルの保存先を決めます。
②でアップロードできるファイルを制限します。ここではファイルの拡張子程度のチェックです。厳密にファイルタイプをチェックするにはファイルの内容をじかにチェックした方がよいでしょう。
コールバック関数cb
の第1引数にError
オブジェクトを渡すと、Multer
はファイル保存しないで例外を発生させます。(③)
Multer
のその他のAPIは、以下にドキュメントがあります。
エラー処理
Multer
でエラーを検出例外は④のcatch部で捕捉され、クライアントへエラー通知します。
Nuxt3のサーバAPIの仕様では、defineEventHandler()
が何らかの値をreturn
で返せば、HTTPレスポンスコード200でreturn
で返した値が返ります。
200以外のHTTPレスポンスコードを返したい場合はcreateError()
を使用します。
Multer
のエラーの場合はエラーメッセージをそのままクライアントに返しています。
おわりに
コールバック型からPromiseなど、Javascriptの使い方が大きく変化したのに加え、Nuxt3の関数の仕様も大きく変わっているので、それぞれのモジュールの動きを調整して統一感のある動きにまとめるのはなかなか難しいですね。
サーバ側のモジュールもいくつか試してみて、Multerが一番シンプルにうまくまとまりました。
応用もいろいろ利きそうなので、いろいろ拡張してみてください。
Discussion