话说使用Nodejs实现一个文件上传,还是蛮简单的,基于Express4.x一般也就formidable用的多些吧;基本的不多说了,github一下都会的;接着《也说文件上传之兼容IE789的进度条---丢掉flash》,新版的大文件上传,最后就差断点续传了,业余跟进中...;对于IE789,在文件上传这块,算是与HTML5无缘了,当然我也选择丢掉了flash,就用最原始的input[type="file"]+hideIframe+轮询;OK,IE789可以凉快去了,BSIE!
那么,现代浏览器上就不一样了;大家都知道用HTML5上传大文件必然会选择分段,files API的file.slice(start,end)+formData;简单的将就看吧:
1 var uploader=function(){ 2 3 //.... 4 5 function Files(obj){ 6 this.files=obj.files; 7 this.__token__=utils.getRandomStr(); 8 this.url=obj.url||location.href; 9 this.chunkSize=obj.chunkSize||200*1024; 10 this.chunks=Math.ceil(this.files.size/this.chunkSize); 11 this.index=0; 12 this.onprogress=obj.onprogress||function(p){console.log(p);}; 13 } 14 Files.prototype={ 15 postFiles:function(){ 16 var $self=this; 17 //大于50M 断点续传 18 if (this.files.size>50*1024*1024) { 19 var fileReader = new FileReader(),spark = new SparkMD5.ArrayBuffer(); 20 fileReader.onload = function (e) { 21 spark.append(e.target.result); 22 $self.hash=spark.end(); 23 window.__hash__=$self.hash; 24 var stored=localStorage.getItem(‘fileUploadInfos‘); 25 //断点信息 26 $self.postSlice(); 27 }; 28 fileReader.readAsArrayBuffer(this.files.slice(0, 10240)); 29 }else{ 30 this.postSlice(); 31 }; 32 }, 33 postSlice:function(){ 34 var $self=this; 35 if (this.index>=this.chunks) { 36 return false; 37 }; 38 this.start=this.index*this.chunkSize; 39 this.end=Math.min(this.files.size,this.start+this.chunkSize); 40 41 var self=this; 42 var fd = new FormData(); 43 fd.append("sliceData", this.files.slice(this.start,this.end)); 44 this.url=//url datas 45 var xhr = new XMLHttpRequest(); 46 xhr.upload.addEventListener("progress", function(evt){ 47 if (evt.lengthComputable) { 48 var led=self.index*self.chunkSize*1+evt.loaded*1; 49 var p=parseFloat((led)/self.files.size*100).toFixed(2); 50 self.onprogress&&self.onprogress(p); 51 }else { 52 console.log(‘unable to compute‘); 53 } 54 }, false); 55 xhr.addEventListener("load", function(){ 56 self.index++; 57 self.postSlice(); 58 eval(xhr.responseText); 59 }, false); 60 xhr.open("POST", this.url); 61 // xhr.addEventListener("error", uploadFailed, false); 62 xhr.addEventListener("abort", function () { 63 //记录断点信息 64 }, false); 65 xhr.send(fd); 66 } 67 } 68 69 return { 70 Files:Files 71 //..... 72 } 73 }(); 74 75 if (this.files) { 76 var Files=new uploader.Files({ 77 files:this.files[0], 78 chunkSize:10*1024*1024, 79 onprogress:function(p){ 80 callbk(p); 81 } 82 }); 83 Files.postFiles(); 84 }
好吧,其实大家都懂,我就不多BB了;还是说formidable吧,既然用到分段上传,formidable的一般做法肯定是行不通的;不过github上人家也说了,onPart或许可以。。。。。。原谅我英语有点low,一知半解;原文这样的:
You may overwrite this method if you are interested in directly accessing the multipart stream. Doing so will disable any
‘field‘
/‘file‘
events processing which would occur otherwise, making you fully responsible for handling the processing.form.onPart = function(part) { part.addListener(‘data‘, function() { // ... }); }
If you want to use formidable to only handle certain parts for you, you can do so:
form.onPart = function(part) { if (!part.filename) { // let formidable handle all non-file parts form.handlePart(part); } }
也就是我们需要使用onPart来分段接收前端发过来的数据,然后合成一个文件,生成到指定目录;
当使用formData上传时,在request headers里我们会看到有项request payload,也就是我们发送过去的数据,这是未解析的原始数据;那么,难道我们还要自己解析吗?不会玩了。。。
扒一扒formidable的源代码,会发现有好几个_parser结尾的js文件;再看incoming_form.js里有这么一段:
1 IncomingForm.prototype._parseContentType = function() { 2 if (this.bytesExpected === 0) { 3 this._parser = dummyParser(this); 4 return; 5 } 6 7 if (!this.headers[‘content-type‘]) { 8 this._error(new Error(‘bad content-type header, no content-type‘)); 9 return; 10 } 11 12 if (this.headers[‘content-type‘].match(/octet-stream/i)) { 13 this._initOctetStream(); 14 return; 15 } 16 17 if (this.headers[‘content-type‘].match(/urlencoded/i)) { 18 this._initUrlencoded(); 19 return; 20 } 21 22 if (this.headers[‘content-type‘].match(/multipart/i)) { 23 var m = this.headers[‘content-type‘].match(/boundary=(?:"([^"]+)"|([^;]+))/i); 24 if (m) { 25 this._initMultipart(m[1] || m[2]); 26 } else { 27 this._error(new Error(‘bad content-type header, no multipart boundary‘)); 28 } 29 return; 30 } 31 32 if (this.headers[‘content-type‘].match(/json/i)) { 33 this._initJSONencoded(); 34 return; 35 } 36 37 this._error(new Error(‘bad content-type header, unknown content-type: ‘+this.headers[‘content-type‘])); 38 };
这几条if很是让人欣喜啊,有木有?特别是看到这句:
this.headers[‘content-type‘].match(/boundary=(?:"([^"]+)"|([^;]+))/i);
这不是在解决咱在request headers里看到的request payload吗?终于在心中大喜,咱不用自己解析那堆数据了;接着往下看:
1 IncomingForm.prototype.onPart = function(part) { 2 // this method can be overwritten by the user 3 this.handlePart(part); 4 }; 5 6 IncomingForm.prototype.handlePart = function(part) { 7 var self = this; 8 9 if (part.filename === undefined) { 10 var value = ‘‘ 11 , decoder = new StringDecoder(this.encoding); 12 13 part.on(‘data‘, function(buffer) { 14 self._fieldsSize += buffer.length; 15 if (self._fieldsSize > self.maxFieldsSize) { 16 self._error(new Error(‘maxFieldsSize exceeded, received ‘+self._fieldsSize+‘ bytes of field data‘)); 17 return; 18 } 19 value += decoder.write(buffer); 20 }); 21 22 part.on(‘end‘, function() { 23 self.emit(‘field‘, part.name, value); 24 }); 25 return; 26 } 27 28 this._flushing++; 29 30 var file = new File({ 31 path: this._uploadPath(part.filename), 32 name: part.filename, 33 type: part.mime, 34 hash: self.hash 35 }); 36 37 this.emit(‘fileBegin‘, part.name, file); 38 39 file.open(); 40 this.openedFiles.push(file); 41 42 part.on(‘data‘, function(buffer) { 43 if (buffer.length == 0) { 44 return; 45 } 46 self.pause(); 47 file.write(buffer, function() { 48 self.resume(); 49 }); 50 }); 51 52 part.on(‘end‘, function() { 53 file.end(function() { 54 self._flushing--; 55 self.emit(‘file‘, part.name, file); 56 self._maybeEnd(); 57 }); 58 }); 59 };
至此,终于明白作者的话了;自己处理上传的数据,是在handlePart中通过part.on(‘data‘)和part.on(‘end‘)来收集分段数据,然后生成文件的;那么使用分段上传的话,我们就需要在Nodejs里重写form.handlePart了;
1 form.handlePart=function(part) { 2 var dd=[],ll=0; 3 part.on(‘data‘, function(data) { 4 if (data.length == 0) { 5 return; 6 } 7 dd.push(data); 8 ll+=data.length; 9 }); 10 11 part.on(‘end‘, function() { 12 var p=‘./public/imgs/‘+uploadToken+‘_‘+req.query.name; 13 fs.open(p, ‘a‘, function (err, fd) { 14 if (err) { 15 throw err; 16 } 17 fs.write(fd, Buffer.concat(dd,ll),0, ll,0,function(){ 18 if (req.query.chunks==req.query.index*1+1) { 19 res.write(bk); 20 } 21 fs.close(fd,function(){}); 22 res.end(); 23 }); 24 }); 25 } 26 }); 27 }
拿到data后生成文件并不难,fs.writeFile、stream都可以的;原谅我初入Nodejs,怎么感觉最后一步的写入文件,这两种方式都特慢呢?不能忍啊,再探!
试来试去,最后还是选择在接收到第一段数据时就生成文件,之后接收到的数据直接push进去;即上面的fs.write(fd,buffer,offset,length,position,cb);话说明显快了不少呢!而且,意外的收获是:想一想接下来还要实现断点续传呢!想一想,貌似这样做,基本等于Nodejs端的断点续传已经实现了呢;前端记录断点的位置,下次上传时从断点位置开始,然后直接push到这个没上传完的文件里;
到这里,Nodejs端的分段接收文件就可以的了,而且还为之后的断点续传做了个很好的铺垫呢;
好了,对于大文件上传,formidable能做的差不多就这么多了,onPart是必须的;如果大家伙有什么更好的方法,欢迎与我分享!简单的记录,与君共勉,谢谢你能看到这儿!
原文来自:花满楼(http://www.famanoder.com/bokes/57586f3c09c0517810c81633)