LoginSignup
8
4

Next.jsで画像ファイルをサーバー内に保存する方法【Next.js・TypeScript・formidable】

Last updated at Posted at 2022-11-19

「Next.js・画像保存」で調べてもなかなかそれっぽい記事が見つからなくて苦労したのでうまくいったものをここに書きます。

環境構築

以下を実行してプロジェクトを作成

npx create-next-app nextjs-image-upload --typescript

以下のサイトを参考にChakra UIをインストール
(※今回はChakra UIをフロント部分で使用しているため)

formidableをインストール

FormDataを変換するのに必要

npm i formidable
npm i @types/formidable

swiperをインストール

npm install swiper

主にpagesフォルダを触っていきます。

フロント部分・データ送信

Chakra UIを使用するために_app.tsxを編集

_app.tsx
import "../styles/globals.css";
import { ChakraProvider } from "@chakra-ui/react";
import type { AppProps } from "next/app";

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ChakraProvider>
      <Component {...pageProps} />
    </ChakraProvider>
  );
}

export default MyApp;

フォーム画面とデータ送信

swiper選択画像のプレビュー表示をスライドにしている

onSubmit関数が送信処理

この中でFormDataを作成してファイルなどを入れてfetchでPOST送信している

index.tsx
import {
  Container,
  FormLabel,
  Heading,
  Image,
  Input,
} from "@chakra-ui/react";
import type { NextPage } from "next";
import { ChangeEvent, FormEvent, useRef, useState } from "react";
import { Swiper, SwiperSlide } from "swiper/react"; //カルーセル用のタグをインポート
import { Pagination, Navigation } from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation"; // スタイルをインポート
import "swiper/css/pagination"; // スタイルをインポート

const Home: NextPage = () => {
  const [images, setImages] = useState<Blob[]>([]);
  const inputNameRef = useRef<HTMLInputElement>(null);
  const inputFileRef = useRef<HTMLInputElement>(null);

  // 以下送信処理
  const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log("送信");

    const name = inputNameRef.current?.value

    const formData = new FormData();
    
    for await(const [i, v] of Object.entries(images)) {
      formData.append('files' , v);
    }
    formData.append("name", name || "");


    const post = await fetch(`${window.location.href}api/upload`, {
      method: "POST",
      body: formData,
    }); 

    console.log(await post.json());
  };

  const handleOnAddImage = (e: ChangeEvent<HTMLInputElement>) => {
    if (!e.target.files) return;
    setImages([...e.target.files]);
  };

  return (
    <Container pt="10">
      <Heading>Image Form</Heading>
      <form onSubmit={onSubmit} encType='multipart/form-data'>
        <FormLabel htmlFor="postName">名前</FormLabel>
        <Input
          type="text"
          id="postName"
          placeholder="Name"
          size="lg"
          ref={inputNameRef}
        />
        <FormLabel htmlFor="postImages">画像</FormLabel>
        <Input
          type="file"
          id="postImages"
          multiple
          accept="image/*,.png,.jpg,.jpeg,.gif"
          onChange={handleOnAddImage}
          ref={inputFileRef}
        />
        <Input type="submit" value="送信" margin="10px auto" variant="filled" />
      </form>
      <Container>
        <Swiper
          slidesPerView={1} //一度に表示するスライドの数
          modules={[Navigation, Pagination]}
          pagination={{
            clickable: true,
          }} // 何枚目のスライドかを示すアイコン、スライドの下の方にある
          navigation //スライドを前後させるためのボタン、スライドの左右にある
          loop={true}
        >
          {images.map((image, i) => (
            <SwiperSlide key={i}>
              <Image
                src={URL.createObjectURL(image)}
                w="full"
                h="40vw"
                objectFit="cover"
              />
            </SwiperSlide>
          ))}
        </Swiper>
      </Container>
    </Container>
  );
};

export default Home;

サーバー側のAPI・画像を取得と保存

pages/apiフォルダの中にupload.tsファイルを作成する

FormDataで送信された内容をformidableで解析する

日本語の情報が少なかった、あっても古かったり…

/public/imagesディレクトリがないと正常に動かないので作成すること、ここに画像が保存される。

upload.ts
import type { NextApiRequest, NextApiResponse } from "next";
import formidable from "formidable";
import { createWriteStream } from "fs";

export const config = {
  api: {
    bodyParser: false,
  },
};
type Data = {
  msg?: string;
};
export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  if (req.method !== "POST") return;

  const form = formidable({ multiples: true, uploadDir: __dirname });

  form.onPart = (part) => {
    
    // let formidable handle only non-file parts
    if (part.originalFilename === "" || !part.mimetype) {
      // used internally, please do not override!
      form._handlePart(part);
    } else if (part.originalFilename) {

      // 以下でファイルを書き出ししている      

      console.log(part.name);
      // /public/imagesディレクトリがないと正常に動かないので作成すること
      const path =
        "./public/images/" + new Date().getTime() + part.originalFilename;
      const stream = createWriteStream(path);
      part.pipe(stream);

      part.on("end", () => {
        console.log(part.originalFilename + " is uploaded");
        stream.close();
      });

    }
  };

  // input[type="file"]以外の値はここから見れた
  form.on('field', (name, value) => {
    console.log(name);
    console.log(value);
  })

  // これを実行しないと変換できない
  form.parse(req)

  // これでもinput[type="file"]以外の値はここから見れるが、fileは見れない
  // form.parse(req, async (err, fields, files) => {
  //   console.log("fields:", fields); // { name: '*'}
  //   console.log("files:", files); // {}

  //   res.status(200).json({ name: "!!!" });
  // });

  // レスポンス
  res.status(200).json({ msg: "success!!" });
}

8
4
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
8
4