如何处理从Node.js到Express的文件上传问题(附代码实例)

178 阅读7分钟

最近,我正在开发一个Node.js的CLI,它可以帮助我的团队更容易地用Markdown写文章。该流程要求对本地的*.md文件进行解析、格式化、转换为.docx*,然后作为Google Doc文件上传到Google Drive,所有这些都需要从终端运行一条命令。

我以几种不同的方式来处理这个项目,最初希望能用无服务器函数来处理后端。在这条路上我遇到了很多死胡同,最终决定建立一个Express服务器,并最终将其托管在Heroku上。

为了将.docx文件内容从我的CLI上传到安全的后端端点,然后上传到用户认证的Google Drive上,我需要一种方法来直接从Node.js中POST multipart/form-data (无需浏览器)。这需要几个第三方库,而且要让它们都能很好地配合起来是相当困难的。互联网上没有很好的资源可以提供帮助,而且还涉及到很多试验和错误。这篇文章将解释我是如何做到的。

前提条件

为了跟上这个教程,你需要以下条件:

  • 在你的机器上安装了Node.js,同时还有一个软件包管理器,例如 [npm](https://www.npmjs.com/)
  • 一个文本编辑器

设置你的应用程序结构

在你的终端或命令提示符中,导航到你的一般项目或开发目录,并运行以下命令:

mkdir multipart_demo
cd multipart_demo
mkdir node_app
npx express-generator express_app
cd express_app
npm install

这些命令将创建一个目录,容纳两个项目:你的Node.js应用程序和你的Express API。它们还将为Express后端提供支架(用 express-generator),并为你的新Express应用程序安装所需的依赖项。

构建你的Node.js应用程序的支架

在你的终端,导航到你的父目录multipart-demo,然后进入你的Node.js应用程序,执行以下命令:

cd ..
cd node_app

初始化一个新的Node.js应用程序:

npm init -y

安装你在这个项目中需要的第三方依赖项:

npm install form-data axios

POST 你将使用form-data 库在你的Node.js应用程序中用键/值对创建一个 "表单"。axios 将被用来将表单数据上传到你的Express应用程序。

编写代码来上传你的文件

使用你喜欢的文本编辑器,打开在你的node_app文件夹中创建的名为index.js的文件。如果这个文件没有为你创建,现在就创建它。

在这个文件中,在顶部添加以下代码:

const fs = require('fs');
const axios = require('axios');
const FormData = require('form-data');

这段代码将导入你安装的两个第三方依赖,以及允许你与文件系统互动的本地fs 模块

在这几行下面,添加以下函数和函数调用:

const upload = async () => {
  try {
    const file = fs.createReadStream('./myfile.txt');
    const title = 'My file';
  
    const form = new FormData();
    form.append('title', title);
    form.append('file', file);
  
    const resp = await axios.post('http://localhost:3000/upload', form, {
      headers: {
        ...form.getHeaders(),
      }
    });
  
    if (resp.status === 200) {
      return 'Upload complete';
    } 
  } catch(err) {
    return new Error(err.message);
  }
}

upload().then(resp => console.log(resp));

upload() 函数是一个异步函数。在该函数内部有一个try/catch 块。这意味着该函数将 "尝试 "在try 块内做任何事情。如果在任何时候代码遇到了错误,它将立即执行catch 块,在那里你可以访问违规的错误。

它在try 块中尝试做的第一件事是创建两个变量:filetitle

第3行,即创建file 变量的地方,之所以突出显示,是因为你需要用你想上传的文件的路径来替换显示的文件路径。这个变量现在代表你的文件的可读流

如果你只是想跟着做,而且心中没有特定的文件,可以创建一个名为myfile.txt的新文件,里面有一些随机的文本,并将其保存在index.js旁边的node_app文件夹中。

title 变量是你将存储你要上传的文件的标题的地方--你可以把它改成你喜欢的任何字符串值。

在这些变量的下面是创建表单本身的地方。两个键/值对被附加到表单上:一个是文件,一个是它的标题。你可以以这种方式向form 对象添加代表你的表单数据的任何数量的键/值对。

接下来,整个表单被发布到你的 Express 后台(在本教程后面你将创建一个端点),使用axios 。请注意,你正在将一个带有头信息的选项对象传递给axios.post() 方法。form-data 库通过给你一个.getHeaders() 方法来确保你的头信息被正确设置,这个方法将返回适合你的表单数据的头信息。

如果没有错误,那么一个 "上传完成 "的消息将被记录到控制台。否则,错误信息将被记录下来。

处理表单数据

现在你已经成功发送了你的文件,你需要能够在你的后台处理它。

在你的终端,导航到你的Express应用程序:

cd ..
cd express_app

Multer是一个Node.js中间件,可以处理multipart/form-data 。运行以下命令来安装multer 包:

npm install multer

在你的文本编辑器中,打开multipart_demo/express_app/routes文件夹中的index.js文件。删除该文件的内容,并将其替换为以下内容:

var express = require('express');
var router = express.Router();

const multer  = require('multer');
const upload = multer({ dest: os.tmpdir() });

router.post('/upload', upload.single('file'), function(req, res) {
  const title = req.body.title;
  const file = req.file;

  console.log(title);
  console.log(file);

  res.sendStatus(200);
});

module.exports = router;

在这段代码中,有几件事需要注意--重要的几行已经被突出显示。

在第5行,在导入multer 包后,你准备通过调用multer() 函数来使用Multer,向它传递一个带有dest 键的选项对象,并将返回的对象保存到一个叫做upload 的变量中。关于配置Multer的其他方法,包括将文件保存到内存而不是磁盘上,请参见文档。

在这种情况下,你要指定一个dest 的值,因为Multer需要把你上传的文件保存在某个地方。对于我的应用程序,我只是暂时需要这个文件,所以我把目的地设置为我服务器的*/tmp*文件夹。如果你需要上传的文件持续存在,你可以把它们存储在你服务器上的另一个目录中,只要你在这个对象中指定路径即可。

在第7行,在执行API端点的回调函数之前,你要调用upload 对象上的single() 方法。你可以在upload 对象上调用许多方法,这些方法适用于不同的数据配置。在这个例子中,由于你只上传一个文件,你将使用single() 方法。

此时,端点的回调函数中可用的req 对象将与你所习惯的略有不同。尽管在你提出POST 请求时没有将任何body 值附加到表单上,你的非文件表单字段的值将在req.body 。你上传的文件将可在req.file 。在上面的代码中,这两个对象都被记录在控制台中。为了看看它们是什么样子的,请测试一下这个项目。

测试该应用程序

在你的终端,确保你仍然在express_app 目录内,然后运行以下命令:

npm start

这将启动你在PORT 3000 的本地服务器。

打开一个新的终端窗口,并导航回你的Node.js项目:

cd <YOUR PARENT DIRECTORY PATH>/multipart_demo/node_app

要执行你的脚本,运行以下命令:

node index.js

如果POST 请求成功,你会在终端看到 "Upload complete "信息,否则你会看到一个错误信息:

Screenshot showing upload complete message in console from node app

在运行你的Express应用程序的终端中再检查一下。你会看到你的文件的标题,以及代表它的对象。你会看到,如果你把文件存储在磁盘上,会有一个path ,你可以使用这个路径来创建另一个可读流,就像我为了把文件上传到Google Drive而需要的那样:

Screenshot showing file object and file title in terminal after posting to express app