Express+multer 文件上传,并在 router 中指定文件存放路径

内容简单说明

  文件上传是 web 开发中比较常见的一个功能虽然说起来是文件上传,实际上,可以看做是对 multipart/form-data 数据的处理。在 npm 中,有很多处理类似数据的库,包括周下载量近 2kw 的 form-data,周下载量近 3mw 的 formidable。

  不过,如果 nodejs 后端使用的 express 框架,其官方也有一个自己的文件上传中间件,用它自己的话来说就是:“Multer 是一个 node.js 中间件,用于处理 multipart/form-data 类型的表单数据,它主要用于上传文件。”

  使用 multer 比较简单,一般就是
    1、导入 multer,
    2、指定文件上传地址(如果有必要的话,不指定只是写到内存中),
    3、在 router 的路径后,回调函数前,写一个upload.single(photo)(单文件)或者upload.array('photos', 12)(多文件),在 router 的回调中,就可以使用req.file 或者 req.files获取文件了。

  在这里,因为指定的上传地址是在 multer(opts)中的 opts 配置,所以 opts 配置号一个地址之后,后续修改就不是那么方便。如果需要对不同文件不同路由路径指定不同的文件上传地址,那应该如何处理?

  multer 的简单使用后文会给个示例,但是最终的目的,是想要在 express 的 router 回调函数中,可以指定文件上传的路径,而不是所有的文件都上传到唯一指定的路径。例如,路由是“testUpload”,我在 router 处理时指定存放到测试使用的上传路径。路由是“formalUpload”,我在处理时可以指定存放到正式的上传路径。

express+multer 基本文件上传示例

  因为主要是测试 multer 内容,所以一切从简,就在一个简单的 express 项目中测试就好

1、创建一个 express 项目(前提:已安装 express-generator),并安装 multer

express --view=ejs express-mutler-demo
// 进入项目根目录
npm i multer

2、上传页面编写

  修改 views/index.ejs 的标签内容如下:

  

Express + multer 簡陋上傳文件

0

  代码内容很简单,就是一个 form 用来模拟文件上传,为了最简单,直接使用的 XMLHttpRequest 实现上传,还没事整了个进度条。
  本来想用原始的方法,还是引入了 jquery。更简略类似下面也 ok。


  依旧以第一个为准,页面大概是这个样子(运行 express 项目,在 localhost:3000 看到):

input画面

3、multer 的简单配置

  新建一个 util/Upload.js,编写 multer 配置并导出:

const multer = require('multer');

// 文件上传配置
const fileStorage = multer.diskStorage({
    destination: function (req, file, callback) {
        callback(null, "/defaultUploadDir");
    },
    filename: function (req, file, callback) {
        callback(null, file.originalname);
    }
});
// 导出配置
module.exports = {
    fileUpdate: multer({ 'storage': fileStorage }),
}

注意:上传地址 “/defaultUploadDir”要先手动创建,否则报错。

4、在对应 router 中使用 multer

  在 routes/index.js 中,添加以下 router 代码:

router.post('/upload', upload.fileUpdate.single('file'), function (req, res, next) {

  const file = req.file;
  console.log(file);

  //如果得到了文件,就返回上传成功
  if (file) {
    return res.status(200).json({ success: true });
  } else {
    return res.status(500).json({ success: false });
  }
});

  记得在最上面引入 multer 配置:

const upload = require('../util/Upload');

  几个简单注意点:
    1、这个路由路径和路由方法,要和前台页面中的 action 和 method 一致;
    2、多文件就要 upload.array(),单文件就用 upload.single()(后续都是单文件示例中说明);
    3、第二点()里面的标志字符串要和前台页面中的name 属性一致。

  如果步骤都正确,成功上传,应该可以看到前台页面如下:
上传成功

  router 的回调中取得上传文件的信息,如下:
上传成功router取得文件信息

  文件上传的位置:
第一次上传成功后台地址

关于使用 multer 文本域数据

  multer 的 readme 所说:”Multer 会添加一个 body 对象 以及 file 或 files 对象 到 express 的 request 对象中。 body 对象包含表单的文本域信息,file 或 files 对象包含对象表单上传的文件信息。“
  实际测试,在前台页面 index.ejs 创建 formData 后,append 一个文本数据:

let formData = new FormData();

// 补入此句
formData.append('dest', 'file_upload');

  刷新页面之后,重新上传,可以在 multer 配置中,在 diskStorage 的 destination 的 callback 中,可以得到 req.body 包含了 dest 属性。如下图:
req.body获取文本域数据

  这是好事,很好的,这样,在前台上传文件时,就可以把需要上传的地址放到这里,那么不同的文件上传就可以存放的不同的地址了。
  那么会有哪些问题呢?
    1、前端需要知道后台的上传路径,不合理。
    2、并不是所有使用 formData.append()添加的属性都能在文件上传 destination 生成前,在 req.body 中获取到。

    这是一个实际遇到的问题,我在使用 angular 时,使用 HttpClient 实现文件上传操作,类似:

  upload(file: any) {
    // 文件使用FormData发送
    const formData: FormData = new FormData();
    formData.append('file', file, file.name);
    formData.append('file_name', file, file.name);
    return this.http.post(this.URL + '/upload', formData );
  }

  后台的 req.body 在获取到上传的文件前并不会有 file_name 属性的值,即在 multer 配置在 diskStorage 的 destination 的 callback 中,可以得到 req.body 是空,在对应 upload 的 router 回调中,才取得 req.body 的 file_name 属性。

在 router 的回调中,指定文件上传的路径。

  在”关于使用 multer 文本域数据“这部分有讲到,前台直接传入文件上传的路径不合理,在接受到上传的文件前得到指定的上传路径也不一定成功,而直接使用配置好的 multer,其文件上传目的地 destination 又只有固定一个。该如何实现?

  把 multer 的配置,封装到一个返回 promise 的函数,指定传入一个文件路径参数,并在 router 的回调中使用该函数,传入上传路径。

  修改 utils/Upload.js 文件,补入以下内容:

// multer文件上传,可指定上传路径,不在router参数里直接用
let uploadFunction = (req, res, dest) => {

    let storage = multer.diskStorage({
        destination: function (req, file, cb) {
            let newDestination = dest;
            let stat = null;
            try {
                // 检查传入的路径是否存在,不存在则创件
                stat = fs.statSync(newDestination);
            } catch (err) {
                fs.mkdirSync(newDestination);
            }
            if (stat && !stat.isDirectory()) {
                throw new Error('文件目录: "' + dest + '已存在!"');
            }
            cb(null, newDestination);
        },
        filename: function (req, file, callback) {
            callback(null, file.originalname);
        }
    });

    let upload = multer({
        storage: storage
    }).single('file');

    return new Promise((resolve, reject) => {
        upload(req, res, (err) => {
            if (err) {
                return reject(err);
            }
            resolve();
        })
    })
};

  记得导出:

module.exports = {
    fileUpdate: multer({ 'storage': fileStorage }),
    uploadFunction,
}

  在 router 中使用,修改原 routes/index.js 的 upload 路由如下:

router.post('/upload', /*upload.fileUpdate.single('file'), */ async function (req, res, next) {

  // 指定文件上传路径
  let uploadPath = 'test_upload';
  // 等到文件上传完成
  await upload.uploadFunction(req, res, uploadPath);

  const file = req.file;
  console.log(req.file);
  //如果得到了文件,就返回上传成功
  if (file) {
    return res.status(200).json({ success: true });
  } else {
    return res.status(500).json({ success: false });
  }
});

  当然,await 需要在 async 函数中使用,也最好放到 trycatch 中。

  如果步骤正确,结果应该和第一步中的一样,文件上传成功。在后台的项目中会新建一个 test_upload 文件夹,并有上传的文件。
第二次文件上传成功位置

  代码已放到 github,有需求可查阅。

  以上内容,全部亲测有效,如果有问题,请提出交流,谢谢。