Nodejs http + promise
0x1 现有的代码
前一篇文章结束时,我们可以看到,其实还是比较乱的。
现在我们就先用一个Promise开始重构。
0x2 第一个Promise
我们先将函数startDownloadTask
中的http.request
调用封装入一个Promise中。代码做如下变更:
1 2 3 4 5 6 | var req = http.request(imgSrc, getHttpReqCallback(imgSrc, dirName, index)); req.on( 'error' , function (e){ console.log( "request " + imgSrc + " error, try again" ); startDownloadTask(imgSrc, dirName, index); }); req.end(); |
修改为
1 2 3 4 5 6 7 8 9 10 11 12 | new Promise( function (resolve, rej) { var req = http.request(imgSrc, function (res) { resolve(res); }); req.on( 'error' , function (e){ console.log( "request " + imgSrc + " error, try again" ); startDownloadTask(imgSrc, dirName, index); }); req.end(); }).then( function (res) { getHttpReqCallback(imgSrc, dirName, index)(res); }); |
可以看到,对http.request
的调用被放到了Promise的主体里面,而http.request
的回调放到了Promise的then
函数里。相比Nodejs的原生异步代码结构:
Promise封装后的结构更贴近同步代码的思维模式。
这个效果有多赞,不用我多说了吧。
接下来我们将构建Promise的代码摘出来封装成一个函数startRequest
1 2 3 4 5 6 7 8 9 10 11 12 | function startRequest(imgSrc) { return new Promise( function (resolve, rej) { var req = http.request(imgSrc, function (res) { resolve(res); }); req.on( 'error' , function (e){ console.log( "request " + imgSrc + " error, try again" ); startDownloadTask(imgSrc, dirName, index); }); req.end(); }) } |
这个函数中,请求的Promise构建出来之后并不立刻去兑现他。而是交给了函数的调用者,自行实现Promise的兑现。
1 2 3 4 5 6 7 | var startDownloadTask = function (imgSrc, dirName, index) { console.log( "start downloading " + imgSrc); startRequest(imgSrc).then( function (res) { getHttpReqCallback(imgSrc, dirName, index)(res); }); } |
于是我们更进一步的实现了请求的发起和请求结果处理之间的解耦。
0x3 第二个Promise
事情到这里也才刚刚进行了一半,因为我们可以看到,getHttpReqCallback
这个函数里面也是一大坨一大坨说不清道不明的东西。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | function getHttpReqCallback(imgSrc, dirName, index) { var fileName = index + "-" + path.basename(imgSrc); var callback = function (res) { console.log( "request: " + imgSrc + " return status: " + res.statusCode); var contentLength = parseInt(res.headers[ 'content-length' ]); var fileBuff = []; res.on( 'data' , function (chunk) { var buffer = new Buffer(chunk); fileBuff.push(buffer); }); res.on( 'end' , function () { console.log( "end downloading " + imgSrc); if (isNaN(contentLength)) { console.log(imgSrc + " content length error" ); return ; } var totalBuff = Buffer.concat(fileBuff); console.log( "totalBuff.length = " + totalBuff.length + " " + "contentLength = " + contentLength); if (totalBuff.length < contentLength) { console.log(imgSrc + " download error, try again" ); startDownloadTask(imgSrc, dirName, index); return ; } fs.appendFile(dirName + "/" + fileName, totalBuff, function (err){}); }); }; return callback; } |
这个函数主要做的事情就是读取请求的响应,把消息体写入预先给定的文件里。这里涉及到两个异步过程,上一篇文章中讲到,这两个过程如果处理不好,很容易把文件写崩。好在现在这段代码难看归难看,但是已经能比较好的处理这两件事了。我们现在要着手处理的是代码比较难看的问题。
为了解决这个问题,我们先把函数在它被调用的地方展开,也就是startRequest
的then
回调里面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | var startDownloadTask = function (imgSrc, dirName, index) { console.log( "start downloading " + imgSrc); startRequest(imgSrc).then( function (res) { var fileName = index + "-" + path.basename(imgSrc); console.log( "request: " + imgSrc + " return status: " + res.statusCode); var contentLength = parseInt(res.headers[ 'content-length' ]); var fileBuff = []; res.on( 'data' , function (chunk) { var buffer = new Buffer(chunk); fileBuff.push(buffer); }); res.on( 'end' , function () { console.log( "end downloading " + imgSrc); if (isNaN(contentLength)) { console.log(imgSrc + " content length error" ); return ; } var totalBuff = Buffer.concat(fileBuff); console.log( "totalBuff.length = " + totalBuff.length + " " + "contentLength = " + contentLength); if (totalBuff.length < contentLength) { console.log(imgSrc + " download error, try again" ); startDownloadTask(imgSrc, dirName, index); return ; } fs.appendFile(dirName + "/" + fileName, totalBuff, function (err){}); }); }); } |
虽然和一般的代码重构的套路相反,但是我们很快会看到为什么要这样做。
接下来我们添加第二个Promise用来处理请求的返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | var startDownloadTask = function (imgSrc, dirName, index) { console.log( "start downloading " + imgSrc); startRequest(imgSrc).then( function (res) { console.log( "request: " + imgSrc + " return status: " + res.statusCode); var contentLength = parseInt(res.headers[ 'content-length' ]); var fileBuff = []; return new Promise( function (resolve, rej) { res.on( 'data' , function (chunk) { var buffer = new Buffer(chunk); fileBuff.push(buffer); }); res.on( 'end' , function () { resolve({ "contentLength" : contentLength, "fileBuff" : fileBuff}) }); }); }).then( function (data) { var contentLength = data.contentLength; var fileBuff = data.fileBuff; var fileName = index + "-" + path.basename(imgSrc); console.log( "end downloading " + imgSrc); if (isNaN(contentLength)) { console.log(imgSrc + " content length error" ); return ; } var totalBuff = Buffer.concat(fileBuff); console.log( "totalBuff.length = " + totalBuff.length + " " + "contentLength = " + contentLength); if (totalBuff.length < contentLength) { console.log(imgSrc + " download error, try again" ); startDownloadTask(imgSrc, dirName, index); return ; } fs.appendFile(dirName + "/" + fileName, totalBuff, function (err){}); }); } |
尽管还是有点难看,但是结构比之前要清晰一些。对于请求响应的处理,data
事件直接在Promise的主体里面搞定,因为要做的事情不是很复杂。而end
事件里,我们将重组后的响应消息体和头域中的消息体长度值打包成js对象,发往第二个Promise的兑现里面处理。
接下来将第二个Promise和之前一样,封装进返回Promise的函数,并且将startRequest
内联进来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | var startDownloadTask = function (imgSrc, dirName, index) { function startRequest(imgSrc) { return new Promise( function (resolve, rej) { var req = http.request(imgSrc, resolve); req.on( 'error' , function (e){ console.log( "request " + imgSrc + " error, try again" ); startDownloadTask(imgSrc, dirName, index); }); req.end(); }); } function solveResponse(res) { console.log( "request: " + imgSrc + " return status: " + res.statusCode); var contentLength = parseInt(res.headers[ 'content-length' ]); var fileBuff = []; return new Promise( function (resolve, rej) { res.on( 'data' , function (chunk) { var buffer = new Buffer(chunk); fileBuff.push(buffer); }); res.on( 'end' , function () { resolve({ "contentLength" : contentLength, "fileBuff" : fileBuff}) }); }); } console.log( "start downloading " + imgSrc); startRequest(imgSrc).then(solveResponse).then( function (data) { var contentLength = data.contentLength; var fileBuff = data.fileBuff; var fileName = index + "-" + path.basename(imgSrc); console.log( "end downloading " + imgSrc); if (isNaN(contentLength)) { console.log(imgSrc + " content length error" ); return ; } var totalBuff = Buffer.concat(fileBuff); console.log( "totalBuff.length = " + totalBuff.length + " " + "contentLength = " + contentLength); if (totalBuff.length < contentLength) { console.log(imgSrc + " download error, try again" ); startDownloadTask(imgSrc, dirName, index); return ; } fs.appendFile(dirName + "/" + fileName, totalBuff, function (err){}); }); } |
尽管还是有点难看,但是结构比之前要清晰一些。对于请求响应的处理,data
事件直接在Promise的主体里面搞定,因为要做的事情不是很复杂。而end
事件里,我们将重组后的响应消息体和头域中的消息体长度值打包成js对象,发往第二个Promise的兑现里面处理。
接下来将第二个Promise和之前一样,封装进返回Promise的函数,并且将startRequest
内联进来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | var startDownloadTask = function (imgSrc, dirName, index) { function startRequest(imgSrc) { return new Promise( function (resolve, rej) { var req = http.request(imgSrc, resolve); req.on( 'error' , function (e){ console.log( "request " + imgSrc + " error, try again" ); startDownloadTask(imgSrc, dirName, index); }); req.end(); }); } function solveResponse(res) { console.log( "request: " + imgSrc + " return status: " + res.statusCode); var contentLength = parseInt(res.headers[ 'content-length' ]); var fileBuff = []; return new Promise( function (resolve, rej) { res.on( 'data' , function (chunk) { var buffer = new Buffer(chunk); fileBuff.push(buffer); }); res.on( 'end' , function () { resolve({ "contentLength" : contentLength, "fileBuff" : fileBuff}) }); }); } console.log( "start downloading " + imgSrc); startRequest(imgSrc).then(solveResponse).then( function (data) { var contentLength = data.contentLength; var fileBuff = data.fileBuff; var fileName = index + "-" + path.basename(imgSrc); console.log( "end downloading " + imgSrc); if (isNaN(contentLength)) { console.log(imgSrc + " content length error" ); return ; } var totalBuff = Buffer.concat(fileBuff); console.log( "totalBuff.length = " + totalBuff.length + " " + "contentLength = " + contentLength); if (totalBuff.length < contentLength) { console.log(imgSrc + " download error, try again" ); startDownloadTask(imgSrc, dirName, index); return ; } fs.appendFile(dirName + "/" + fileName, totalBuff, function (err){}); }); } |
最后将对响应消息体的处理,即第二个Promise的兑现过程也封装进函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | var startDownloadTask = function (imgSrc, dirName, index) { function startRequest(imgSrc) { return new Promise( function (resolve, rej) { var req = http.request(imgSrc, resolve); req.on( 'error' , function (e){ console.log( "request " + imgSrc + " error, try again" ); startDownloadTask(imgSrc, dirName, index); }); req.end(); }); } function solveResponse(res) { console.log( "request: " + imgSrc + " return status: " + res.statusCode); var contentLength = parseInt(res.headers[ 'content-length' ]); var fileBuff = []; return new Promise( function (resolve, rej) { res.on( 'data' , function (chunk) { var buffer = new Buffer(chunk); fileBuff.push(buffer); }); res.on( 'end' , function () { resolve({ "contentLength" : contentLength, "fileBuff" : fileBuff}) }); }); } function solveResData(data) { var contentLength = data.contentLength; var fileBuff = data.fileBuff; var fileName = index + "-" + path.basename(imgSrc); console.log( "end downloading " + imgSrc); if (isNaN(contentLength)) { console.log(imgSrc + " content length error" ); return ; } var totalBuff = Buffer.concat(fileBuff); console.log( "totalBuff.length = " + totalBuff.length + " " + "contentLength = " + contentLength); if (totalBuff.length < contentLength) { console.log(imgSrc + " download error, try again" ); startDownloadTask(imgSrc, dirName, index); return ; } fs.appendFile(dirName + "/" + fileName, totalBuff, function (err){}); } console.log( "start downloading " + imgSrc); startRequest(imgSrc) .then(solveResponse) .then(solveResData); } |
最终的结果就是这样,我们有了三个各自独立的函数:startRequest
、solveResponse
、solveResData
,每一个函数各自处理从请求的发起,到接收响应,到保存最终响应结果中的某一个阶段。由于拆成了3个函数,所以每一个函数的结构都不是很复杂难懂。最后通过一组Promise链式调用将3个实际是并发执行的过程用一个看似串联的结构组织起来。
至此大功告成。
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | ( function () { "use strict" ; const http = require( "http" ); const fs = require( "fs" ); const path = require( "path" ); const urlList = [ "http://content.battlenet.com.cn/wow/media/wallpapers/patch/fall-of-the-lich-king/fall-of-the-lich-king-1920x1080.jpg" , "http://content.battlenet.com.cn/wow/media/wallpapers/patch/black-temple/black-temple-1920x1200.jpg" , "http://content.battlenet.com.cn/wow/media/wallpapers/patch/zandalari/zandalari-1920x1200.jpg" , "http://content.battlenet.com.cn/wow/media/wallpapers/patch/rage-of-the-firelands/rage-of-the-firelands-1920x1200.jpg" , "http://content.battlenet.com.cn/wow/media/wallpapers/patch/fury-of-hellfire/fury-of-hellfire-3840x2160.jpg" , ]; function getHttpReqCallback(imgSrc, dirName, index) { var fileName = index + "-" + path.basename(imgSrc); var callback = function (res) { console.log( "request: " + imgSrc + " return status: " + res.statusCode); var contentLength = parseInt(res.headers[ 'content-length' ]); var fileBuff = []; res.on( 'data' , function (chunk) { var buffer = new Buffer(chunk); fileBuff.push(buffer); }); res.on( 'end' , function () { console.log( "end downloading " + imgSrc); if (isNaN(contentLength)) { console.log(imgSrc + " content length error" ); return ; } var totalBuff = Buffer.concat(fileBuff); console.log( "totalBuff.length = " + totalBuff.length + " " + "contentLength = " + contentLength); if (totalBuff.length < contentLength) { console.log(imgSrc + " download error, try again" ); startDownloadTask(imgSrc, dirName, index); return ; } fs.appendFile(dirName + "/" + fileName, totalBuff, function (err){}); }); }; return callback; } var startDownloadTask = function (imgSrc, dirName, index) { function startRequest(imgSrc) { return new Promise( function (resolve, rej) { var req = http.request(imgSrc, resolve); req.on( 'error' , function (e){ console.log( "request " + imgSrc + " error, try again" ); startDownloadTask(imgSrc, dirName, index); }); req.end(); }); } function solveResponse(res) { console.log( "request: " + imgSrc + " return status: " + res.statusCode); var contentLength = parseInt(res.headers[ 'content-length' ]); var fileBuff = []; return new Promise( function (resolve, rej) { res.on( 'data' , function (chunk) { var buffer = new Buffer(chunk); fileBuff.push(buffer); }); res.on( 'end' , function () { resolve({ "contentLength" : contentLength, "fileBuff" : fileBuff}) }); }); } function solveResData(data) { var contentLength = data.contentLength; var fileBuff = data.fileBuff; var fileName = index + "-" + path.basename(imgSrc); console.log( "end downloading " + imgSrc); if (isNaN(contentLength)) { console.log(imgSrc + " content length error" ); return ; } var totalBuff = Buffer.concat(fileBuff); console.log( "totalBuff.length = " + totalBuff.length + " " + "contentLength = " + contentLength); if (totalBuff.length < contentLength) { console.log(imgSrc + " download error, try again" ); startDownloadTask(imgSrc, dirName, index); return ; } fs.appendFile(dirName + "/" + fileName, totalBuff, function (err){}); } console.log( "start downloading " + imgSrc); startRequest(imgSrc) .then(solveResponse) .then(solveResData); } urlList.forEach( function (item, index, array) { startDownloadTask(item, './' , index); }) })(); |
0x4 One more thing?
就在我研究怎么在Nodejs中将http api和Promise结合起来用的时候,外面的高手们也在捣鼓差不多的事情,于是有一天我无意间发现了这么个东西
通俗易懂的解释就是,这货就是把网页开发中常用的Ajax用Promise进行封装,思路和我这篇文章中的基本一致。
下面是代码示例:
1 2 3 4 5 6 7 8 9 10 | var myImage = document.querySelector( 'img' ); fetch( 'flowers.jpg' ) .then( function (response) { return response.blob(); }) .then( function (myBlob) { var objectURL = URL.createObjectURL(myBlob); myImage.src = objectURL; }); |
虽然还没有写进正式标准,但是在最新的firefox和chrome上已经实装了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
2019-08-20 Mysql(MyISAM和InnoDB)及Btree和索引优化
2019-08-20 MySQL创建数据表时设定引擎MyISAM/InnoDB
2019-08-20 MYSQL 中 MyISAM与InnoDB两者之间区别与选择,详细总结,性能对比