Express文件上传multer库学习记录

559 阅读5分钟

应用中间件

express提供中间件,对请求中不同格式的数据进行解析,以便于拿到数据。

 const express = require('express')
 ​
 const app = express()
 ​
 // 对格式为JSON的body数据进行解析
 app.use(express.json()) 
 // 对格式为 x-www-form-urlencoded的数据进行解析,并且使用Node内置qs模块(extended:true)
 // 否则,使用第三方库 qs
 app.use(express.urlencoded({extended:true}))
 ​
 app.post('/home',(req,res,next)=>{
     console.log(req.body);
     res.end('home post')
 }) 
 ​
 // 开启监听
 app.listen(8090,()=>{
     console.log('中间件服务器开启成功~');
 })

Note:

  1. 若框架没有提供这些中间件,则需要自己在app.use中对不同的req.body的数据格式进行解析,然后使用next调用栈中下一个中间件,进行路径和方法的匹配。
  2. 使用app.use表示无论什么方法和路径,先对 content-type中的数据进行解析,转为对象,赋值给req.body,使得next调用时,后面匹配的中间件能够拿到解析后的数据。

form-data解析:一般用于上传图片文件

使用第三方库 multer

初步使用
 npm install multer

image-20220808200230088

 const express = require('express')
 const multer = require('multer')
 ​
 const app = express()
 ​
 const upload = multer()
 // 表示 form-data中普通的数据
 app.use(upload.any())
 ​
 app.post('/upload',(req,res,next)=>{
     console.log(req.body);
     res.end('用户上传成功~~')
 })
 ​
 app.listen(8090,()=>{
     console.log('form-data解析服务器开启成功~');
 })
 -------------------------------------------
 form-data解析服务器开启成功~
 [Object: null prototype] { name: 'yuxi' }

但是,若要上传图片文件,并且将上传的图片文件放在服务器中某位置:

 const express = require('express')
 const multer = require('multer')
 ​
 const app = express()
 // 表示将上传的文件指定地址存放,若没有该文件,则创建文件夹
 const upload = multer({
     dest:'./uploadImg/'
 })
 app.use(express.json())
 app.use(express.urlencoded({extended:true}))
 // 表示 form-data中普通数据的处理,不能用于文件的处理
 app.use(upload.any())
 ​
 // 这里使用连续中间件,upload.single('img')表示解析单个文件夹,且key值为 ‘img’,
 // 进行处理后,传递给后面的中间件 next ,然后返回结果给客户端
 app.post('/upload',upload.single('file'),(req,res,next)=>{
     console.log(req.files);
     res.end('用户上传成功~~')
 })
 ​
 app.listen(8090,()=>{
     console.log('form-data解析服务器开启成功~');
 })
Note
  1. 此时由于使用 app.use(upload.any()),报错

     Error: Unexpected end of form
         at Multipart._final (F:\vscode\练习\Node\express\node_modules\busboy\lib\types\multipart.js:588:17)
         at callFinal (_stream_writable.js:598:10)
         at processTicksAndRejections (internal/process/task_queues.js:80:21)
    
  2. 此时我们图片没有后缀,而且不能直接打开,我们给图片添加后缀和重命名

     const path = require('path')
     const express = require('express')
     const multer = require('multer')
     ​
     const app = express()
     // 为上传的文件添加后缀和文件名
     const storage = multer.diskStorage({
         // cb是callback回调函数的缩写
         destination:(req,file,cb)=>{
             cb(null,'./uploadImg/')
         },
         filename:(req,file,cb)=>{
             // 文件名取 时间戳和原文件的后缀名进行组合
             cb(null,Date.now()+path.extname(file.originalname))
         }
     })
     // 表示将上传的文件指定地址存放,若没有该文件,则创建文件夹
     const upload = multer({
         // dest:'./uploadImg/'
         storage,
     })
     app.use(express.json())
     app.use(express.urlencoded({extended:true}))
     // 表示 form-data中普通数据的处理
     // app.use(upload.any())
     ​
     // 这里使用连续中间件,upload.single('img')表示解析单个文件夹,且key值为 ‘img’,
     // 进行处理后,传递给后面的中间件 next ,然后返回结果给客户端
     app.post('/upload',upload.single('file'),(req,res,next)=>{
         // console.log(req.body);
         console.log(req.file);
         res.end('用户上传成功~~')
     })
     ​
     app.listen(8090,()=>{
         console.log('form-data解析服务器开启成功~');
     })
    

    此时,将 app.use(upload.any())进行注释后,此时图片文件上传没有问题,不会报错。同时也能正常进行图片解析:

    image-20220809152340579

错误分析

1.只使用 app.use(upload.any()),同时上传文本域和图片。

 app.use(upload.any())
 ​
 app.post('/upload',(req,res,next)=>{
     // console.log(req.body);
     console.log(req.file);
     res.end('用户上传成功~~')
 })

image-20220809152943622

打印结果:

 [Object: null prototype] { name: 'yuxi' }
 [
   {
     fieldname: 'file',
     originalname: '微信图片_20220704132002.jpg',
     encoding: '7bit',
     mimetype: 'image/jpeg',
     destination: './uploadImg/',
     filename: '1660030167604.jpg',
     path: 'uploadImg\1660030167604.jpg',
     size: 51988
   },
   {
     fieldname: undefined,
     originalname: 'v2-f75d8361b30214ea6dff5a3c2ce952de_r.jpg',
     encoding: '7bit',
     mimetype: 'image/jpeg',
     destination: './uploadImg/',
     filename: '1660030167607.jpg',
     path: 'uploadImg\1660030167607.jpg',
     size: 620080
   }
 ]

事实上可以成功处理数据,并且显示解析后的文件和文本域对象。

Note:

这里使用diskStorage处理文件上传的地址和文件名,其中 destination可以为字符串也可以为函数,但是两者使用上有区别:

 // 为上传的文件添加后缀和文件名
 const storage = multer.diskStorage({
     // cb是callback回调函数的缩写
     // destination:(req,file,cb)=>{
     //     cb(null,'./uploadImg/')
     // },
     destination:"./uploadImg/",
     filename:(req,file,cb)=>{
         // 文件名取 时间戳和原文件的后缀名进行组合
         cb(null,Date.now()+path.extname(file.originalname))
     }
 })
  • 字符串,将根据字符串,创建文件夹
  • 回调函数中,传入的字符串表示文件放入的地址不会创建该文件夹(若没有的话)

查看源码(部分):

 var fs = require('fs')
 var os = require('os')
 var path = require('path')
 var crypto = require('crypto')
 var mkdirp = require('mkdirp')
 ​
 function getFilename (req, file, cb) {
   crypto.randomBytes(16, function (err, raw) {
     cb(err, err ? undefined : raw.toString('hex'))
   })
 }
 ​
 function getDestination (req, file, cb) {
   // 操作系统默认的临时文件夹
   cb(null, os.tmpdir())
 }
 ​
 function DiskStorage (opts) {
   // 拿到自定义文件名的函数,若没有,则使用默认
   this.getFilename = (opts.filename || getFilename)
 ​
   if (typeof opts.destination === 'string') {
     // 创建文件夹
     mkdirp.sync(opts.destination)
     this.getDestination = function ($0, $1, cb) { cb(null, opts.destination) }
   } else {
     // 若没有添加自定义的文件路径,使用默认的
     this.getDestination = (opts.destination || getDestination)
   }
 }

但是无论如何,在使用字符串和回调函数后,最终都会将传入的地址传递或赋值给this.getDestination,最后this.storage中有自定义的路径存放的地址和文件名(当然,也有diskStorage原型上的函数,会在后续解析时使用到)。

2.只使用 upload.single('file'),同时上传文本域和图片

  • 多张图片:报错

因为传入文件时,会获取文件名的数量,若有多张图片,则报错

源码部分:(function wrappedFileFilter部分)

 Multer.prototype._makeMiddleware = function (fields, fileStrategy) {
   // console.log(fields);
   function setup () {
     var fileFilter = this.fileFilter
     // 为了规定对应文件名的数量,以便后续处理时,通过key值动态处理其数值,使得程序根据不同值进行判断
     var filesLeft = Object.create(null)
 ​
     fields.forEach(function (field) {
       if (typeof field.maxCount === 'number') {
         filesLeft[field.name] = field.maxCount
       } else {
         // 没有限制数量,则为无穷,可以处理多个文件
         filesLeft[field.name] = Infinity
       }
     })
 ​
     function wrappedFileFilter (req, file, cb) {
        //打印不同文件名,在filesLeft表中对应的数值
       //console.log(filesLeft[file.fieldname]);
       // 需要传入文件名
       if ((filesLeft[file.fieldname] || 0) <= 0) {
         return cb(new MulterError('LIMIT_UNEXPECTED_FILE', file.fieldname))
       }
       // 限制文件名的数量:一次处理一个文件,若有多个同名,则第二次处理时,其值 `<=0`,此时将会报错
       filesLeft[file.fieldname] -= 1
       // 过滤文件
       fileFilter(req, file, cb)
     }
 ​
     return {
       limits: this.limits,
       preservePath: this.preservePath,
       storage: this.storage,
       fileFilter: wrappedFileFilter,
       fileStrategy: fileStrategy
     }
   }
 ​
   return makeMiddleware(setup.bind(this))
 }
 ----------------------------
 {
   fieldname: 'file',
   originalname: '微信图片_20220704132002.jpg',
   encoding: '7bit',
   mimetype: 'image/jpeg'
 }
 {
   fieldname: 'file',
   originalname: 'v2-f75d8361b30214ea6dff5a3c2ce952de_r.jpg',
   encoding: '7bit',
   mimetype: 'image/jpeg'
 }

filesLeft[file.fieldname] -= 1 一次处理一个文件,若有多个同名,则第二次处理时,其值 <=0,此时将会报错:

image-20220809170146656

源码中查看:

image-20220809170311301

若是多张不同key值的图片,仍然会报错,因为当进行不同于限制名的key值时,在filesLeft[file.fieldname]无法查找到值,则仍然将会报错。

原因:在文件上传之前,就会根据single()中的值,创建对应的一张表,确定数值,后续图片上传时,非key值就为undefined,表中没有。

  • 一张图片,正确解析
总结

无论使用upload.any还是upload.single,都可以对数据进行解析,但是后者只能解析对应key值的一张图片。且两者不可同时使用(不代表不能同时写,而是中间件在匹配的时候,文件不能同时匹配两个,如下:use是不限制方法和路径的)

 // app.use(upload.any())
 ​
 // 这里使用连续中间件,upload.single('img')表示解析单个文件夹,且key值为 ‘img’,
 // 进行处理后,传递给后面的中间件 next ,然后返回结果给客户端
 app.post('/upload',upload.single('file'),(req,res,next)=>{
     console.log(req.body);
     console.log(req.file);
     res.end('用户上传成功~~')
 })

但是仍然存在问题,目前并不是很清楚,为什么两者同时使用错误的原因是什么,给出的错误提示是:

 Error: Unexpected end of form
     at Multipart._final (F:\vscode\练习\Node\express\node_modules\busboy\lib\types\multipart.js:588:17)
     at callFinal (_stream_writable.js:598:10)
     at processTicksAndRejections (internal/process/task_queues.js:80:21)

再贴一些在github上找到的一些回答,大佬可以解答下吗?

github.com/mscdex/busb…

image-20220809194030042

\