Skip to content

Nest 单文件上传

1. 安装依赖

bash
npm install --save @nestjs/platform-express multer

npm install -D @types/multer

npm install nanoid

创建一个模块 upload

upload.module.ts

ts
import { Module } from "@nestjs/common";
import { UploadService } from "./upload.service";

@Module({
  providers: [UploadService],
  exports: [UploadService],
})
export class UploadModule {}

upload.service.ts

ts
import { HttpException, Inject, Injectable } from "@nestjs/common";
import { deleteFile } from "../../commonModules/utils/storage";
import { ConfigService } from "@nestjs/config";
@Injectable()
export class UploadService {
  @Inject()
  private configService: ConfigService;
  async uploadsingle(files, pathname) {
    if (files["status"] == 403) {
      for (let i = 0; i < files["value"].length; i++) {
        deleteFile(files["value"][i].path, true);
      }
      return {
        code: 403,
        data: "",
        message: files["data"],
      };
      // throw new HttpException(files['message'], 403);
    } else {
      const url: string[] = [];
      files.forEach((item) => {
        url.push(
          this.configService.get("APP_STATIC_DOMAIN") +
            item.destination +
            "/" +
            item.filename
        );
      });
      return {
        code: 200,
        url,
      };
    }
  }
}

创建 utils/storage.ts

ts
import * as multer from "multer";
import * as path from "path";
import * as fs from "fs";
import { customAlphabet } from "nanoid";
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    // 获取模块名称
    console.log(req.body.module);
    // 获取年份
    const year = new Date().getFullYear();
    // 获取月份
    const month = new Date().getMonth() + 1;
    // 月份前面补0
    const formattedMonth = month.toString().padStart(2, "0");
    // 获取日期
    const day = new Date().getDate();
    // 日期前面补0
    const formattedDay = day.toString().padStart(2, "0");
    // 基础拼接路径
    let basicdir =
      "/" +
      process.env.APP_STATIC +
      "/" +
      process.env.APP_UPLOAD_PATH +
      "/" +
      req.body.module +
      "/";
    // 最终路径
    let dir = basicdir + year + "/" + formattedMonth + "/" + formattedDay + "/";
    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true });
    }
    cb(null, dir);
  },
  filename: (req, file, cb) => {
    // 获取当前时间戳
    const timestamp = new Date().getTime();
    const nanoid = customAlphabet(
      "1234567890abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ",
      6
    );
    const randomName = nanoid();
    return cb(
      null,
      `${timestamp}_${randomName}${path.extname(file.originalname)}`
    );
  },
});
/**
 * @param { delPath:String } (需要删除文件的地址)
 * @param { direct:Boolean } (是否需要处理地址)
 */
const deleteFile = (delPath, direct) => {
  delPath = direct ? delPath : path.join(__dirname, delPath);
  try {
    /**
     * @des 判断文件或文件夹是否存在
     */
    if (fs.existsSync(delPath)) {
      console.log(delPath);
      fs.unlinkSync(delPath);
    } else {
      console.log("inexistence path:", delPath);
    }
  } catch (error) {
    console.log("del error", error);
  }
};

export { storage, deleteFile };

创建管道验证 upload.pipe.ts

  • common/pipe/upload.pipe.ts
ts
import { ArgumentMetadata, Injectable, PipeTransform } from "@nestjs/common";
import dotenv from "dotenv";
dotenv.config();
@Injectable()
export class UploadPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    if (value.length > 1) {
      let count = 0;
      let type = 0;
      if (value.length > Number(process.env.APP_UPLOAD_MAX_FILE)) {
        return {
          status: 403,
          data: `图片个数不能超过${Number(process.env.APP_UPLOAD_MAX_FILE)}`,
          value: value,
        };
      }
      // 多图
      value.map((item: any) => {
        // 判断大小
        if (item.size > 1024 * 1024 * 10) {
          count++;
        }
        // 判断类型
        if (item.mimetype.indexOf("image") == -1) {
          type++;
        }
      });
      if (count > 0) {
        return {
          status: 403,
          data: "图片大小不能超过10M",
          value: value,
        };
      }
      if (type > 0) {
        return {
          status: 403,
          data: "必须是图片",
          value: value,
        };
      }
      return value;
    } else {
      // 单图
      if (value[0].size > 1024 * 1024 * 10) {
        return {
          status: 403,
          data: "图片大小不能超过10M",
          value: value,
        };
      }
      if (value[0].mimetype.indexOf("image") == -1) {
        return {
          status: 403,
          data: "必须是图片",
          value: value,
        };
      }
      return value;
    }
  }
}

其他模块使用

  • 第一步 先引入 upload.module.ts

  • 第二步 在 xxx.service.ts 中注入 uploadService

  • 第三步 写控制器

ts
  // 上传文件
  @Post('uploadOnefile')
  // 不验证token了我这里就是举例子
  @ByGuardpass()
  @UseInterceptors(
    // 独立自己的
    FilesInterceptor('image', 10000, {
      storage: storage,
    }),
  )
  async upload(
    @Body() body: any,
    @UploadedFiles(UploadPipe)
    files: Array<Express.Multer.File>,
  ) {
    let name = body.module;

    return this.uploadService.uploadsingle(files, name);
  }

对应前端代码

特别注意

注意

  1. 使用 FormData 和 fetch API 上传图片(示例代码,需要根据后端 API 调整)

  2. const formData = new FormData(); formData.append("module", "books");

// 添加其他字段,例如文件名 formData.append("image", file);

// 'image' 是后端期待的字段名,根据需要调整

const app = await NestFactory.create(AppModule, { cors: true }); 一定要开启跨域

代码如下

html
<!--
 * @Author: jsopy
 * @Date: 2025-09-12 19:38:28
 * @LastEditTime: 2025-12-26 12:16:24
 * @FilePath: /html/index.html
 * @Description: 
 * 
-->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>图片上传</title>
  </head>
  <body>
    <form id="uploadForm" enctype="multipart/form-data">
      <label for="imageInput">选择图片:</label>
      <input type="file" id="imageInput" name="image" accept="image/*" />
      <button type="button" onclick="uploadImage()">上传图片</button>
    </form>
    <div id="preview"></div>

    <script>
      function uploadImage() {
        const fileInput = document.getElementById("imageInput");
        const file = fileInput.files[0]; // 获取选中的文件
        if (!file) {
          alert("请选择一个文件");
          return;
        }

        // 创建预览图片的URL(可选)
        const preview = document.getElementById("preview");
        const img = document.createElement("img");
        img.src = URL.createObjectURL(file); // 创建图片的URL用于预览
        img.style.maxWidth = "100%"; // 设置最大宽度以适应容器
        preview.innerHTML = ""; // 清空之前的预览内容
        preview.appendChild(img); // 将图片添加到预览区域

        // 使用FormData和fetch API上传图片(示例代码,需要根据后端API调整)
        const formData = new FormData();
        // 必须他放在第一个位置
        formData.append("module", "user"); // 添加其他字段,例如文件名
        formData.append("image", file); // 'image' 是后端期待的字段名,根据需要调整

        fetch("http://localhost:5000/v1/testdemo/uploadOnefile", {
          // '/upload' 是你的后端处理上传的URL,需要替换为实际URL
          method: "POST",
          body: formData,
        })
          .then((response) => response.json()) // 假设服务器返回JSON格式的响应
          .then((data) => {
            console.log("Success:", data); // 处理响应数据,例如显示上传成功信息等
            if (data.code == 200) {
              alert("上传成功");
            } else {
              alert("上传失败");
            }
          })
          .catch((error) => {
            console.error("Error:", error); // 处理错误情况,例如显示错误信息等
            alert("上传失败"); // 或者在页面上显示错误消息等操作
          });
      }
    </script>
  </body>
</html>