百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

扒一扒Nodejs formidable的onPart

zhezhongyun 2025-02-17 15:01 72 浏览

话说使用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是必须的;如果大家伙有什么更好的方法,欢迎与我分享!简单的记录,与君共勉,谢谢你能看到这儿!

相关推荐

DNF无色流派还在继续,重力之泉龙战八荒测评

作者:礁石22222前言本篇为115级套装天天鉴栏目,来帮助各位读者对于新版本的装备有一个更清晰的认知。115级套装分为了稀有到太初5个品级,所有套装的稀有品级属性是一致的,从神器开始出现分歧。通过积...

《暗黑破坏神2重制版》常用符文之语P3

大家好我是游戏小白,继续补充一下《暗黑破坏神2重制版》常用的符文之语,主要给大家总结一下前期过渡常用符文之语。没看过之前关于符文之语总结的小伙伴可以翻翻前面的文章。1、钢铁符文之语钢铁造价极低但性价比...

魔兽怀旧服:P1一款法系BIS披风,获取方式隐蔽,需完成875个任务

在魔兽怀旧服WLK版本,依旧存在许多实用的制造业装备,特别是在P1阶段,制造业装备的耐用性和性价比是最高的,不仅可以帮助玩家快速过渡到团本,甚至还有个别制造业装备超越了团本掉落的强度,除了玩家近期讨论...

分手类型——过渡阶段

过度阶段一.内涵:类似于反复期,在这个阶段儿可能会出现两种可能性。1.感性想分手,但理性上舍不得。感性上我完全不想跟他相处,但理性上我又觉得他身上有很多对我有利的,对我未来有机会有利的东西。二.理性...

《最后的信仰》新手开局保姆级指南职业选择、属性加点与开荒策略

《最后的信仰》作为类魂动作游戏,开局选择直接影响开荒体验。本文针对新手玩家,从职业特性、属性分配到武器过渡,提炼高效开荒公式,助你避开陷阱,快速掌握战斗节奏。一、职业选择:斗士/盗贼优先,法系/...

DNF回血秘方揭示,夏日前买必看篇

作者:辽宁吴彦祖前言(省流速览)夏日礼包购买理由:夏日礼包是DNF四大礼包之一(新春&耕耘&夏日&金秋),错过销售日期后续想获得部分道具难度极大。主打暖暖时装、特色补齐、海量打...

DNF手游:55级粉装有大作用!强化继承大法,可节省大量幸运符

55级粉装的自身属性,实际上比较一般,但它可以用来作为“过渡胚子”,能够帮大家节省很多幸运符和宇宙精华!1、强化继承大法因为不断有玩家翻出了55级团本武器,这把武器肯定是当前版本毋庸置疑的版本答案,但...

魔兽世界50级职业任务装备如何选择,手把手教学

魔兽世界50级职业任务,我们装备应该如何选择,今天分身一个文章告诉你,我们知道BWL开放,也会开放50级的职业任务,那么50级的职业任务,对某些职业来说还是非常重要的,因为给的装备。有的甚至可以用到7...

暗牧的T5与散件如何取舍?认准自己的团队地位才最重要

牧师作为《魔兽世界》中的老牌职业历经许久已经收获了不少的信仰者,而在笔者看来牧师的最大特色便是风格完全不同的三系专精,在TBC时期,Raid本中的牧师大多为神牧,而戒律牧基本只活跃在竞技场和战场上,而...

DNF:魂异界传说宝珠曝光!属性设计一般般,男枪第五转职专属

魂异界地下城“炒冷饭”,定位新春活动副本,奖励道具覆盖面广,涉及白金徽章、转职书、矛盾材料等等。解锁魂异界次元等级,还能兑换传说宝珠,属性也逐渐浮出水面,却比较鸡肋,“抠门”发挥的淋漓尽致!太“抠门”...

SwiftUI入门五:让视图和过渡动起来

在使用SwiftUI的时候,无论效果在哪里,我们都可以单独的让视图的变化动起来,或者让视图的状态的变化动态化。SwiftUI会为我们处理那些组合的、层叠的以及可中断的动画的复杂性。在这个教程中,我们会...

DNF:又是变强的一年?2024耕耘礼包提升率揭晓

作者:assddde前言国服耕耘礼包的内容已经爆料了。对去年拉满耕耘的奶系职业的而言,今年的提升点为纹章加入了1%的增益量增幅。对C而言,今年换装称号中还加入了buff换装词条。而对于错过了新春套的C...

魔兽世界:TBC第一阶段还有必要刷T4套吗,D3套能否过渡到T5套?

T4套真的不如D3套?TBC怀旧服P1阶段目前已经走过大半,作为这个阶段装备等级最高的套装T4套装,游戏中有很大争议。比如猎人玩家会选择D3套,直接跳过T4到T5阶段,而法师甚至会选择继续使用T3套装...

《异世界勇者》390版本开荒&毕业攻略——狂暴战

虽然说这个版本是防战的本命版本,但是从大家催更的频率来看,狂暴战依旧是碾压的优势,今天给大家分享一下390版本狂暴战的毕业游玩思路,希望对你有帮助。今天给大家带来的是手动速刷版的攻略,想要挂机过本需要...

飞飞重逢:装备属性卡全攻略,五色神卡助你战力飙升快速获取

在游戏中,装备属性卡是提升战斗力的关键道具,它能赋予装备特殊的元素属性,不仅大幅提升攻击力,还能针对不同怪物打出克制伤害。属性卡分为火、水、风、土、电五种元素,每种都能为装备附加独特的攻击特效。那么如...