Nodejs http + promise

0x0 前言

有了前面的《使用Promise解决多层异步调用的简单学习》《如何使用Nodejs进行批量下载》两篇文章的基础。在《如何使用Nodejs进行批量下载》中我们看到,Nodejs中的http下载充斥着各种异步回调,我们必须小心的组织这些回调才能使代码清晰可读,以免陷入回调地狱。而Promise这一机制的出现就是致力于解决多层异步回调的,那么我们能轻易就能想到,为什么不用Promise来重构nodejs的http下载代码呢?于是本文就诞生了。

0x1 现有的代码

《如何使用Nodejs进行批量下载》这篇文章结束时,最终的代码在这里
https://github.com/knightingal/SimpleDownloader/blob/master/index.js

我们可以看到,其实还是比较乱的。

现在我们就先用一个Promise开始重构。

0x2 第一个Promise

我们先将函数startDownloadTask中的http.request调用封装入一个Promise中。代码做如下变更:

     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();

修改为

 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

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的兑现。

var startDownloadTask = function(imgSrc, dirName, index) {
  console.log("start downloading " + imgSrc);
  startRequest(imgSrc).then(function(res) {
    getHttpReqCallback(imgSrc, dirName, index)(res);
  });

}

于是我们更进一步的实现了请求的发起和请求结果处理之间的解耦。

0x3 第二个Promise

事情到这里也才刚刚进行了一半,因为我们可以看到,getHttpReqCallback这个函数里面也是一大坨一大坨说不清道不明的东西。

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;
}

这个函数主要做的事情就是读取请求的响应,把消息体写入预先给定的文件里。这里涉及到两个异步过程,上一篇文章中讲到,这两个过程如果处理不好,很容易把文件写崩。好在现在这段代码难看归难看,但是已经能比较好的处理这两件事了。我们现在要着手处理的是代码比较难看的问题。

为了解决这个问题,我们先把函数在它被调用的地方展开,也就是startRequestthen回调里面。

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用来处理请求的返回

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内联进来

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的兑现过程也封装进函数

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);

}

最终的结果就是这样,我们有了三个各自独立的函数:startRequestsolveResponsesolveResData,每一个函数各自处理从请求的发起,到接收响应,到保存最终响应结果中的某一个阶段。由于拆成了3个函数,所以每一个函数的结构都不是很复杂难懂。最后通过一组Promise链式调用将3个实际是并发执行的过程用一个看似串联的结构组织起来。

至此大功告成。

完整的代码见这里
https://github.com/knightingal/SimpleDownloader/blob/UsePromise/index.js

0x4 One more thing?

就在我研究怎么在Nodejs中将http api和Promise结合起来用的时候,外面的高手们也在捣鼓差不多的事情,于是有一天我无意间发现了这么个东西

Fetch API

通俗易懂的解释就是,这货就是把网页开发中常用的Ajax用Promise进行封装,思路和我这篇文章中的基本一致。

下面是代码示例:

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上已经实装了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容