在开发自己的博客引擎的过程中,遇到的第一个问题就是如何实现图片的上传。
如果对express中的app.post('/', function (req, res))
方法熟悉的话,方法中的req.files
已经代表的是上传的文件,打印出来之后可以看到文件的一系列属性,比如说通过上传的路由,重命名之后的名称,存放的路径,文件的大小等等:
{ size:74643, path:'/tmp/8ef9c52abe857867fd0a4e9a819d1876', name:'edge.png', type:'image/png', hash:false, lastModifiedDate:ThuAug09201220:07:51 GMT-0700(PDT), _writeStream:{ path:'/tmp/8ef9c52abe857867fd0a4e9a819d1876', fd:13, writable:false, flags:'w', encoding:'binary', mode:438, bytesWritten:74643, busy:false, _queue:[], _open:[Function], drainable:true}, length:[Getter], filename:[Getter], mime:[Getter]}}
但有几个非常常规的功能无法通过这些个参数实现的,比如
- 如何判断上传成功?
- 如何显示上传进度?
- 如何在上传之前限制图片的大小和分辨率?
- 如何重命名图片并且另取存放路径?
并且解决以上的几个问题靠原生的框架是不够的,于是想到了 Modules,找到了star数比较多的node-formidable,按照tutorial,在app.post
中的贴上了示例代码:
var formidable =require('formidable'); app.post('/upload',function(req, res){var form =new formidable.IncomingForm({ form.keepExtensions =false;//keep .jpg/.png form.uploadDir ="upload/";//upload path}); form.parse(req,function(err, fields, files){ console.log("parse!");});//bind event handler form.on("progress",function(err){}) form.on("complete",function(err){})})
但运行之后,发现所有的事件回调并没有被触发(因为所有handler中的console.log
都没有被打印出来)。 可图片却正常上传了! ,或许是 我们使用formidable的方式不对,忽略了什么吧。
Google了几篇关于formidable使用的文章。确定我们使用formidable的方式是没有问题的。于是尝试把问题定位到是不是这个组件在整体架构中的问题,比如和express的组件有了冲突?
首先看看是不是express的问题,先从和上传文件相关的 req.files
入手。果然,在关于 req.files
的 API 中看到,其实 bodyParser()
和node-formidable
模块其实早已经被整合在一起,甚至参数都已经可以通用:
The bodyParser() middleware utilizes the node-formidable module internally, and accepts the same options. An example of this is the keepExtensions formidable option, defaulting to false which in this case gives you the filename "/tmp/8ef9c52abe857867fd0a4e9a819d1876" void of the ".png" extension. To enable this, and others you may pass them to bodyParser():
也就是说,我们在初始化 bodyParser()
的时候就可以设置有关上传的一些参数了,比如可以限制上传文件的大小,改变上传路径,自动重命名后保留文件后缀
app.use(express.bodyParser({ uploadDir:"media/upload/", keepExtensions:true, limit:10000000}));
非常好!但是仍然不够,只有给上传的不同阶段绑定不同的处理函数,这样才能更灵活的控制。
这次去express在github上托管的代码看看,看看在issue中有没有相同问题的人——果然是有的,比如这个 问题:
Is there an event I can listen to to get the progress of an file upload? Apologies for posting this as an issue.
但是express的作者的回答差点让我石化了:
not currently no. modern browsers and moving on in the future the client-side can report progress fine so I feel like the average case wont really need this, maybe even some do now, but that being said you can still disable the multipart support and use formidable directly for the events. Maybe sometime I'll add some event handlers but that wouldn't really be very middleware-like so it would be kinda awkward I think.
他认为现代浏览器在不远的将来就会有报告上传文件进度的功能,所以他反而删除了?但万幸的是他告诉我们如果真的有这个需求的话,可以在禁用multipart这个中间件(middleware)的同时,使用formidable。于是我尝试这么做:
app.disable("multipart");
但是没有起任何作用。
继续按图索骥,考虑到multipart应该算作是一个属于connect的中间件,我们来到了multipart的 官方文档,找到了关于它的说明,更关键的说关于初始化时option的说明:
The options passed are merged with formidable's IncomingForm object, allowing you to configure the upload directory, size limits, etc. For example if you wish to change the upload dir do the following.
更关键的是关于它 defer 这个参数的说明:
defer defers processing and exposes the Formidable form object as req.form. next() is called without waiting for the form's "end" event. This option is useful if you need to bind to the "progress" event, for example.
一切豁然开朗了!只要开启defer这个参数,就可以开启关于各种上传事件的绑定。不多说了,展示完整的代码吧:
app.configure(function(){ app.use(express.bodyParser({ keepExtensions:true, limit:10000000,// 10M limit defer:true//enable event }));}) app.post('/upload',function(req, res){//bind event handler req.form.on('progress',function(bytesReceived, bytesExpected){}); req.form.on('end',function(){ console.log(req.files); res.send("done");});})
所有的事件列表和事件的参数列表都可以参照 node-formidable 的文档,在这里就不多赘述了。
最后回答文章开头的那几个问题:
- 问:如何限制图片大小
if(req.files.image.size >307200)// 300 * 1024{ msg +='File size no accepted. Max: 300k; }
- 问:如何显示上传进度
req.form.on('progress',function(bytesReceived, bytesExpected){ console.log(((bytesReceived / bytesExpected)*100)+"% uploaded");});
- 问:如何修改文件名
var fs =require('fs'); fs.renameSync(req.files.image.path,'public/files/img'+ new_name);
本文同时也发布在我的个人博客qingbob