如何在Node.js应用程序中使用Formidable上传文件

2,335 阅读10分钟

在Node.js应用程序中使用Formidable上传文件

每当我们在任何网站的客户端提交一个表单时,所有的表单数据都会到服务器端。通常情况下,表单数据在我们提交给服务器之前会被编码。我们可以通过在HTML的<form> 标签中指定enctype属性来做到这一点。

如果我们不指定它,表单数据会被编码为默认类型。

简介

当我们处理纯文本数据如姓名、电子邮件、密码等时,通常会出现这种情况。但如果我们上传的是某种文件,我们需要指定enctype属性的值为 "multipart/form-data"。当我们使用有文件上传控件的表单时,我们需要这个值。

在这篇文章中,我们将学习如何使用node.js、express和formidable来上传文件。Formidable是一个Node.js模块,用于解析表单数据,特别是文件上传。

前提条件

  1. 对Node.js和NPM有良好的理解。
  2. MongoDB必须安装在你的设备中,你需要对数据库有一个基本的了解。
  3. 对命令行或代码编辑器中的集成终端有良好的理解。

本教程的目标

  • 如何设计一个用于发布多部分表单数据的API端点。
  • 如何使用Formidable来处理文件的上传。
  • 如何在服务器端管理和存储这些文件。
  • 如何在前台查看这些文件。

然而,由于这些主题有一半已经在我之前的文章中涵盖了,因此我将在需要的地方参考那篇文章。

项目设置

这些步骤基本上涉及以下内容。

  1. 文件夹结构
  2. 用Mongoose设置MongoDB和
  3. 渲染HTML文件

另外,供参考。你的app.js文件和文件夹结构应该是这样的。

app.js

文件夹结构。

├───model (folder) <br/>
│ └───fileSchema.js (file) <br/>
├───node_modules (folder) <br/>
├───public (folder) <br/>
│ └───css (folder), files(folder) <br/>
|──────└───style.css (file) <br/>
|───views (folder) <br/>
│────└───index.ejs (file) <br/>
├───app.js (file) <br/>
├───package-lock.json (file) <br/>
├───package.json (file) <br/>
├───server.js (file) <br/>

Formidable

如前所述,formidable 是一个Node.js模块,用于解析表单数据,特别是文件上传。

让我们从安装formidable开始。

在你的终端写下这个命令。

npm install formidable

安装包后,我们将在app.js文件的顶部导入它。

const formidable = require("formidable");

然后我们将创建一个API端点来上传文件。

注意:确保用于渲染页面的端点在所有API端点的最后。

// API Endpoint for uploading file
app.post("/api/uploadFile", (req, res) => {
  // Stuff to be added soon
});

让我们开始使用formidable

这个包的工作原理是创建一个来自客户端的表单数据的实例。它将是一个带有一些默认键值对的对象实例,我们可以按照我们的要求进行修改。

让我们来看看它。

在上面的API回调函数中添加这段代码。

const form = formidable.IncomingForm();
console.log(form);

我们可以通过在这个API上发送带文件的请求来查看表单数据。但是,在此之前,我们需要在我们的HTML代码中,即在index.ejs 文件中做一些小改动。

将表单中的action属性的值改为/api/uploadFile

<form action="/api/uploadFile" enctype="multipart/form-data" method="POST">
  <input type="file" class="admin__input" id="myFile" name="myFile" />
  <input class="admin__submit" type="submit" />
</form>

如果你在重启服务器后从你渲染的网页中上传一些东西,你应该在你的终端看到这样的东西。

IncomingForm {
  _events: [Object: null prototype] {},
  _eventsCount: 0,
  _maxListeners: undefined,
  error: null,
  ended: false,
  maxFields: 1000,
  maxFieldsSize: 20971520,
  maxFileSize: 209715200,
  keepExtensions: false,
  uploadDir: 'C:\\Users\\SARTHA~1\\AppData\\Local\\Temp',
  encoding: 'utf-8',
  headers: null,
  type: null,
  hash: false,
  multiples: false,
  bytesReceived: null,
  bytesExpected: null,
  _parser: null,
  _flushing: 0,
  _fieldsSize: 0,
  _fileSize: 0,
  openedFiles: [],
  [Symbol(kCapture)]: false
}

正如你所看到的,我们在这个对象中有许多属性,包括encoding,maxFileSize 等。通过这些属性,我们可以在我们的规范中配置formidable。

我们必须创建一个变量,指向我们想要存储文件的目录/文件夹。要做到这一点,在创建表单实例后添加以下代码。

const uploadFolder = path.join(__dirname, "public", "files");

现在,我们的uploadFolder 变量指向public 目录下的文件夹,该目录存在于我们项目的根级别。

现在让我们通过改变表单实例中的几个属性来改变配置。

// Basic Configuration
form.multiples = true;
form.maxFileSize = 50 * 1024 * 1024; // 5MB
form.uploadDir = uploadFolder;
console.log(form);
  1. multiples 属性默认设置为false,正如我们在控制台记录的表单对象中看到的那样。我们可以把它设置为 "true",这样用户就可以一次添加多个文件。另外,我们必须在HTML中的输入标签中添加多个属性,以确保用户能够上传多个文件。

  2. 我们可以限制用户上传的文件的大小。默认是200MB,但我们将其限制为5MB。

  3. 最后但并非最不重要的是,我们必须将上传目录改为我们创建的目录。

你还可以改变许多其他的属性,但这对大多数的使用情况来说已经足够了。

如果我们在控制台中再记录一次表单实例,我们可以看到上述属性的不同。

IncomingForm {
  _events: [Object: null prototype] {},
  _eventsCount: 0,
  _maxListeners: undefined,
  error: null,
  ended: false,
  maxFields: 1000,
  maxFieldsSize: 20971520,
  maxFileSize: 52428800,
  keepExtensions: false,
  uploadDir: 'D:\\Sarthak\\upload-multipart-form-data-using-formidable\\public\\files',
  encoding: 'utf-8',
  headers: null,
  type: null,
  hash: false,
  multiples: true,
  bytesReceived: null,
  bytesExpected: null,
  _parser: null,
  _flushing: 0,
  _fieldsSize: 0,
  _fileSize: 0,
  openedFiles: [],
  [Symbol(kCapture)]: false
}

由于我们已经完成了基本配置,我们可以开始解析我们的文件。要做到这一点,我们使用一个内置的解析函数,我们可以在我们的表单实例上调用。

在配置下面添加这段代码。

// Parsing
form.parse(req, async (err, fields, files) => {
  console.log(fields);
  console.log(files);
  if (err) {
    console.log("Error parsing the files");
    return res.status(400).json({
      status: "Fail",
      message: "There was an error parsing the files",
      error: err,
    });
  }
});

.parse(request, callback)

"parse "函数解析一个包含表单数据的传入的Node.js请求。如果提供了一个回调,所有的字段和文件都会被收集并传递给回调。

我们的目的是根据我们自己的需要来解析和存储这些文件,因此我们需要在工作之前看一下这些文件。因此,我们有两个日志语句来看看我们在回调函数中得到的数据。

另外,我们将在第一步就处理任何可能出现的错误。这是因为我们不希望解析任何有一些潜在错误的文件。我们通过检查是否有任何错误来做到这一点。如果我们确实遇到了任何错误,我们可以发送状态代码为400的响应,描绘出一个错误的请求。

如果你通过提交文件看到这个代码,你会在控制台看到解析过的表单数据的日志。注意,在你的public下的files文件夹中已经创建了一个新文件。但是,该文件将是不可读的,因为该文件还没有扩展名。

你记录的数据应该是这样的。

{}
{
  myFile: File {
    _events: [Object: null prototype] {},
    _eventsCount: 0,
    _maxListeners: undefined,
    size: 735154,
    path: 'D:\\Sarthak\\upload-multipart-form-data-using-multer\\public\\files\\upload_d235df36a536ff3bc3cbfa8ac0f86e2f',
    name: 'College ID.pdf',
    type: 'application/pdf',
    hash: null,
    lastModifiedDate: 2021-05-30T12:40:05.872Z,
    _writeStream: WriteStream {
      _writableState: [WritableState],
      _events: [Object: null prototype] {},
      _eventsCount: 0,
      _maxListeners: undefined,
      path: 'D:\\Sarthak\\upload-multipart-form-data-using-multer\\public\\files\\upload_d235df36a536ff3bc3cbfa8ac0f86e2f',
      fd: 4,
      flags: 'w',
      mode: 438,
      start: undefined,
      autoClose: true,
      pos: undefined,
      bytesWritten: 735154,
      closed: false,
      [Symbol(kFs)]: [Object],
      [Symbol(kCapture)]: false,
      [Symbol(kIsPerformingIO)]: false
    },
    [Symbol(kCapture)]: false
  }
}

fields 对象是空的,我们不想要它。在文件对象中,我们可以看到我们在HTML中提到的输入标签的名称(myFile)。我们甚至可以获得带有原始扩展名的文件名。所有这些信息将帮助我们更精确地保存和管理我们的文件。

在继续前进之前,我们必须看一下一个特殊情况。由于用户可以同时上传多个文件,传入的解析数据将是一个对象的数组。因此,在进一步工作之前,我们必须每次检查我们得到的是多个文件还是单个文件。

有多个文件的数据看起来与此类似。

{
  myFile: [
    File {....},
    File {....}
  ]
}

接下来的步骤。

  1. 我们将分别处理单个文件和多个文件。
  2. 在这两种情况下,我们将通过创建一个单独的函数来检查文件是否有效。
  3. 如果文件无效,那么我们将抛出一个错误。如果它是有效的,我们将重命名它并将文件名存储在我们的数据库中。

现在,添加这段代码,然后我们将走完每个步骤。

// Check if multiple files or a single file
if (!files.myFile.length) {
  //Single file

  const file = files.myFile;

  // checks if the file is valid
  const isValid = isFileValid(file);

  // creates a valid name by removing spaces
  const fileName = encodeURIComponent(file.name.replace(/\s/g, "-"));

  if (!isValid) {
    // throes error if file isn't valid
    return res.status(400).json({
      status: "Fail",
      message: "The file type is not a valid type",
    });
  }
  try {
    // renames the file in the directory
    fs.renameSync(file.path, join(uploadFolder, fileName));
  } catch (error) {
    console.log(error);
  }

  try {
    // stores the fileName in the database
    const newFile = await File.create({
      name: `files/${fileName}`,
    });
    return res.status(200).json({
      status: "success",
      message: "File created successfully!!",
    });
  } catch (error) {
    res.json({
      error,
    });
  }
} else {
  // Multiple files
}
  1. 在第一步,我们要检查用户是否上传了多个文件。我们通过检查文件解析数据中myFile属性的长度来做到这一点。如果长度为零,那么就意味着只有一个文件被上传。

  2. 下一步是看一下上传的文件是否有效。我们通过创建一个特殊的函数来做到这一点,该函数是这样的。

const isFileValid = (file) => {
  const type = file.type.split("/").pop();
  const validTypes = ["jpg", "jpeg", "png", "pdf"];
  if (validTypes.indexOf(type) === -1) {
    return false;
  }
  return true;
};

在这个函数中,我们要提取上传文件的原始扩展名。如果它存在于我们描述的有效扩展名数组中,那么我们可以返回真,否则我们返回假。

  1. 我们正在创建一个有效的文件名,用 "破折号 "去除所有的 "空格"。这是通过正则表达式和encodeURIComponent() 函数的帮助完成的。

  2. 如果文件是无效的,我们将抛出一个错误。如果它是有效的,我们将在Node.js的核心模块fs 的帮助下,在我们的文件目录中重命名该文件。

不要忘了在你文件的顶部导入fs 模块

之后,我们可以将文件的名称存储在本教程前面托管的MongoDB云数据库中。

试着自己完成多个文件的else块!这里是整个上传函数,供参考。

app.js

有了这个,如果你将尝试上传一个具有有效扩展名的文件或图片。它将以我们定义的名称保存在你的文件目录中。同时,文件名会被储存在你的云端数据库中,这样你就可以在我们的前端访问它。

在前端查看这些文件

Formidable vs Multer

我之前的文章是关于Multer的,这篇文章是关于Formidable的。两者都是npm包,但一个作为模块,一个作为中间件。

我发现formidable 工作起来更简单,因为配置multer 要复杂得多。但是,在一些使用实例中,你必须与Multer合作。

例如,如果你选择在将图片保存在你的服务器上之前调整它们的大小,那么Multer提供了一个被称为Buffer storage 的东西,可以帮助你。

因此,这在很大程度上取决于你的使用情况和你觉得什么更容易使用。