在開發自己的博客引擎的過程中,遇到的第一個問題就是如何實現圖片的上傳。
如果對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