• 首页
  • nodejs
  • 关于使用 Connect-Busboy 实现文件上传 优化说明

关于使用 Connect-Busboy 实现文件上传 优化说明

upload.jpg


文章目录


这篇博文完全上关于上一篇的优化 先看上一篇 node.js 在 Express4.0 框架使用 Connect-Busboy 实现文件上传
因为从上次博客改用 connect-busboy 来上传文件后,发现了明显的一个bug


bug 说明

文件显示上传 100% ,然后预览的时候,偶尔会发现图片只能显示一部分
这种情况在 png 格式 图片尤其严重
昨天重新 review 代码,发现一个bug ,当然和 connect-busboy 一点关系都没有,而是涉及到流的处理过程.



这里把上一篇 blog 里贴出的上传代码在放上来分析一下

function upload(req, res, next) {
   req.busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
       var tmp_path=path.join(os.tmpDir(), path.basename(filename));
       file.pipe(fs.createWriteStream(tmp_path));
       file.on('end',function(){
           var uuid = tool.generateUUID();
           commfile.savePathFile(uuid, mimetype, tmp_path, true, function(err, fileBase64) {
               if (err) {
                   res.json({
                       success:false,
                       url:''
                   });
               }else{
                   res.json({
                       success: true,
                       url: '/file/' + uuid
                   });
               }
           });
       });
   });

   req.pipe(req.busboy);
   }

针对代码,我们细致理一下
req.busboy.on('file' ,function(......).....)

1 .req.busboy.on 方法用来监听 form 提交的参数的事件,其中 on('file' ) 是监听 form 提交二进制文件流事件.
上面的监听事件的回调函数传递过来几个参数,我们重点看下 file 参数
file 是二进制文件流句柄,它是一个标准的 stream
2 .接着我取到服务器临时文件目录 ,和 文件名,组合成一个文件路径
类似这样的路径: /Users/zhangzhi/data/temp/aaa.png
var tmp_path=path.join(os.tmpDir(), path.basename(filename));
3 .接着我把 file 流写入上面定义的文件路径
file.pipe(fs.createWriteStream(tmp_path));

  • 这里出现了一个流程冗余问题

上面用到了 pipe 希望读一下我的另外一篇文章了解 pipe 原理 node.js 在io实现异步非阻塞

  1. 然后我读取刚才的临时文件到字节数组,转化为 base64 字符串,存储在了 LevelDB数据库
    file.on('end' ....)
  • bug 就出现在这里

我们回头分析一下上面出现的流程冗余问题及bug


流程冗余

假如我的图片是直接保存在服务器的某个文件夹下面,那么上面的 1,2,3 流程是没有问题,(当然不需要临时文件目录,直接写入到服务器指定的保存图片目录即可)
但是我目前的博客图片全部在 levelDB 数据库里,所以没有必要先在临时目录生成图片,然后读取图片字节数组,再转化为base64串写入数据库
中间的弯路就是保存临时文件再读取

我们已经拿到了 file 上传文件流句柄,所以完全可以直接塞到二进制数组中,临时保存,毕竟不是上面巨大文件,对内存容忍度为0 ,如下代码

var dataArr = [];
file.on("data", function (chunk){
      dataArr.push(chunk);
});

上面我们监听 file 的 data 事件,通过源源不断的字节流传输到服务器,我们就不管三七二十一先塞进 dataArr 二进制数组中
那么接着我们要做什么,我们直接可以把上面的 dataArr 数据转化成一个 base64 字符串存入数据库,具体的代码实现在下面,我们该分析下上面的bug了


bug出现

我们上面说到了第 4 步有一个隐形的bug ,在上传文件的时候偶尔出现,尤其是 png 图片时,100% 暴露,叔能忍,婶婶也忍不了啊......
为什么第 4 步这里有bug ?
还是脱离不了整个流程,简单回顾下.


上传的文件流 file 写入到临时文件目录中 ,假设这个过程叫 A
我们读取 临时文件 文件转化为 base64 字符串, 这个过程叫 B
我们应该什么时候去读取 临时文件,应该在 A 过程全部结束,
而 A 过程结束是指 file 上传文件流全部传完 并且 文件流全部写入到了临时文件
但是我们上面 监听的 file.on('end',function(){...} 只是监听上传文件流结束的事件,具体这个文件流是否完全写入到临时文件,根本不知道,但是这时我们已经做了一个错误的决定,就是开始读取 临时文件 转化为base64 字符串
所以: 当文件流仅仅是通过 http 传完时,但是并没有写完到临时文件,这是我们立即读取图片,一定是不完整的图片

如果这里一定要这样去做,改如果修改一下代码呢:

function upload(req, res, next) {
    req.busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
        var tmp_path=path.join(os.tmpDir(), path.basename(filename));
        var passedLength = 0;
        var writeStream = fs.createWriteStream(tmp_path);
        file.on('data',function(trunk){
                passedLength += chunk.length;
                 if (writeStream.write(chunk) === false) {
                        file.pause();
                 }
        });
        file.on('end',function(){
            //当没有数据读取,关闭写入数据流
            writeStream.end();
        });
        writeStream.on('drain', function() {
        //速度写入完毕后
        //这里我们才去执行把图片文件转换成 base64 字符串写入数据库操作
        var uuid = tool.generateUUID();
            commfile.savePathFile(uuid, mimetype, tmp_path, true, function(err, fileBase64) {
                if (err) {
                    res.json({
                        success:false,
                        url:''
                    });
                }else{
                    res.json({
                        success: true,
                        url: '/file/' + uuid
                    });
                }
           });      
        });
    
    req.pipe(req.busboy);
    }

上面的代码是没有经过测试的,直接敲上去,只是为了说明按照原来流程的正确处理方式



流程问题理清了,bug 也找到了.下面做如下修复

先上代码

req.busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
        var dataArr = [], len = 0,strBase64;
        file.on("data", function (chunk){
            dataArr.push(chunk);
            len += chunk.length;
        });
        file.on("end", function () {
            strBase64 = Buffer.concat( dataArr, len ).toString('base64');
            var uuid=tool.generateUUID();
            commfile.saveFile(uuid,mimetype,strBase64,function(err){
                if (err) {
                    res.json({
                        success:false,
                        url:''
                    });
                }else{
                    res.json({
                        success: true,
                        url: '/file/' + uuid
                    });
                }
            })
        });

    });

    req.pipe(req.busboy);

简单说明一下上面的代码
file.on("data" 事件实时把字节流写入数组中
file.on("end" 当上传文件字节流结束后, 把字节数组转化为 base64 字符串
strBase64 = Buffer.concat( dataArr, len ).toString('base64');
最后我们把 base64 字符串写入数据库
这样上传图片再也不会出现写入数据库的图片不完整的bug 了.

出自:关于使用 Connect-Busboy 实现文件上传 优化说明

回到顶部