Next.jsでPublic配下に画像をアップロードする記事が少なかったので、備忘録として残しておきます。
画像のアップローダの作り方は
【React/TypeScript】単一画像をReact Hook Formを使って送信する
の上記記事を参考にしてつくりました。(画像アップローダーの詳しい説明は上記記事に詳しく解説されており、非常にわかりやすいです)
ここでは、Next.js配下のPublic配下のimagesフォルダに画像を保存するまでの流れを記載していきます。
事前準備
- formidable-serverlessのモジュールの追加
npm i formidable-serverless
フロントエンドの処理
ざっと、フロントエンドの処理です。
inputにref属性を加え、画像に変更があった場合、新しい画像を取得するコンポーネントです。
import React, { InputHTMLAttributes, forwardRef } from 'react'
export type Props = {
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
id: InputHTMLAttributes<HTMLInputElement>['id']
}
const InputImage = forwardRef<HTMLInputElement, Props>(({ onChange, id }, ref) => {
return <input ref={ref} id={id} type='file' accept='image/*' onChange={onChange} hidden />
})
InputImage.displayName = 'InputImage'
export default InputImage
プレビュー用の画像処理
import { useEffect, useState } from 'react'
type Args = {
file: File | null
}
export const useGetImageUrl = ({ file }: Args) => {
const [imageUrl, setImageUrl] = useState('')
useEffect(() => {
if (!file) {
return
}
let reader: FileReader | null = new FileReader()
reader.onloadend = () => {
// base64のimageUrlを生成する。
const base64 = reader && reader.result
if (base64 && typeof base64 === 'string') {
setImageUrl(base64)
}
}
reader.readAsDataURL(file)
return () => {
reader = null
}
}, [file])
return {
imageUrl,
}
}
メインのフロントエンドの処理は、画像の処理のみ書いてます。tailwind.cssを使っています。
import Image from 'next/image';
import React, { FC, useRef, useState } from 'react';
import InputImage from '@/components/InputImage';
import { useGetImageUrl } from '@/hook/useGetImageUrl';
// 画像のID,ファイルのサイズ
const IMAGE_ID = 'imageId';
const FIELD_SIZE = 210;
const Post: FC = () => {
const fileInputRef = useRef<HTMLInputElement>(null);
const [imageFile, setImageFile] = useState<File | null>(null);
// 画像のURLを取得
const { imageUrl } = useGetImageUrl({ file: imageFile });
// ファイルの設定
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.currentTarget?.files && e.currentTarget.files[0]) {
const targetFile = e.currentTarget.files[0];
setImageFile(targetFile);
}
};
// 画像のキャンセルの設定
const handleClickCancelButton = (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
e.preventDefault();
setImageFile(null);
// <input />タグの値をリセット
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!imageFile) {
alert('画像を選択してください');
return;
}
try {
const formData = new FormData();
const file = imageFile;
const blob = file.slice(0, file.size, file.type);
// ファイル名をユニークにする
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const newFile = new File([blob], uniqueSuffix + '-' + file.name, {
type: file.type,
});
formData.append('file', newFile);
const response = await fetch('/api/images/upload', {
method: 'POST',
body: formData,
});
if (response.status === 200) {
console.log('画像がアップロードされました');
/*
newFile.name:画像名
成功した時にDBに画像名を登録する処理を追加したい場合その処理を記述
*/
} else {
console.log('アップロードに失敗しました');
throw new Error('アップロードに失敗しました');
}
alert('登録が完了しました');
} catch (error) {
console.error('画像のファイル登録が失敗しました。', error);
}
return (
<div>
<h2>画像アップローダー</h2>
<form onSubmit={handleSubmit}>
<div className="flex rounded-lg bg-white shadow-sm my-4">
<span className="px-4 py-2 inline-flex items-center min-w-fit rounded-s-md border border-e-0 border-gray-200 bg-gray-600 text-sm text-gray-200 ">
画像をアップロードする
</span>
<label
htmlFor={IMAGE_ID}
className={
'border-white-3px-dotted w-[' +
FIELD_SIZE +
'px] h-[' +
FIELD_SIZE +
'px] flex justify-center items-center overflow-hidden cursor-pointer bg-sky-200'
}
>
{imageUrl && imageFile ? (
<Image
src={imageUrl}
alt="アップロード画像"
width={FIELD_SIZE}
height={FIELD_SIZE}
className="object-cover w-full h-full"
/>
) : (
'+ 画像をアップロード'
)}
{/* ダミーインプット: 見えない */}
<InputImage
ref={fileInputRef}
id={IMAGE_ID}
onChange={handleFileChange}
/>
</label>
<div style={{ height: 20 }} />
{/* キャンセルボタン */}
<button
className="font-bold bg-gray-200 p-4"
onClick={(e) => handleClickCancelButton(e)}
>
キャンセル
</button>
</div>
<button
type="submit"
className="w-full sm:w-auto py-3 px-4 inline-flex justify-center items-center gap-x-2 text-md font-semibold rounded-lg border border-transparent bg-gradient-to-r from-purple-600 to-blue-400 hover:opacity-80 text-white disabled:opacity-50 disabled:pointer-events-none"
>
画像をアップロードする
</button>
</form>
</div>
);
};
};
と、参考記事を例にすると、このように、フォームを作成することができます。
APIの作成
それでは、ここから、Public配下に画像を設置するようにapiを作っていきます。
import fs from 'fs'
import path from 'path'
import formidable, { File, IncomingForm } from 'formidable-serverless'
import { NextApiRequest, NextApiResponse } from 'next'
// ファイルのアップロード処理
export const config = {
api: {
bodyParser: false,
},
}
export default async function handler(req: NextApiRequest | File | Date, res: NextApiResponse) {
const form = new IncomingForm()
form.uploadDir = './public/images'
form.keepExtensions = true
form.parse(req, (err, fields, files: { [key: string]: any }) => {
if (err) {
res.status(500).json({ error: 'ファイルのアップロード中にエラーが発生しました' })
return
}
const file = files.file
if (!file) {
res.status(400).json({ error: 'ファイルが見つかりません' })
return
}
// アップロードされたファイルをpublic/imagesに移動
const newPath = path.join(process.cwd(), 'public', file.name)
fs.rename(file.path, newPath, (err) => {
if (err) {
res.status(500).json({ error: 'ファイルの保存中にエラーが発生しました' })
return
}
res.status(200).json({ message: 'ファイルが正常にアップロードされました' })
return
})
})
}
ただこのままだと、型エラーが出ます。
それを消していきたいと思います。
プロジェクト配下か、types
のディレクトリに、formidable-serverless.d.ts
を設置します。
declare module 'formidable-serverless' {
import { IncomingForm as OriginalIncomingForm, File } from 'formidable'
export interface IncomingForm extends OriginalIncomingForm {
uploadDir: string
keepExtensions: boolean
parse(req: any, callback: (err: any, fields: any, files: { [key: string]: File }) => void): void
}
export interface File {
size: number
path: string
name: string
type: string
hash: string
lastModifiedDate?: Date
originalFilename: string
toJSON: () => string
}
// formidableのIncomingFormを拡張
export class IncomingForm extends OriginalIncomingForm {
parse(
req: any,
callback: (err: any, fields: any, timestamp: any, files: { [key: string]: File }) => void,
): void
}
}
型は便宜上anyにしている個所がありますが、厳密に指定した場合は、適宜変更すると良いと思います。
参考になりましたら幸いです。