关于使用 Connect-Busboy 实现文件上传 优化说明
文章目录
这篇博文完全上关于上一篇的优化 先看上一篇 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实现异步非阻塞
- 然后我读取刚才的临时文件到字节数组,转化为 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 了.