手把手教你學node.js 之使用 eventproxy 控制併發

使用 eventproxy 控制併發

目標

創建一個 lesson4 項目,在其中編寫代碼。html

代碼的入口是 app.js,當調用 node app.js 時,它會輸出 CNode(https://cnodejs.org/ ) 社區首頁的全部主題的標題,連接和第一條評論,以 json 的格式。node

輸出示例:jquery

[
  {
    "title": "【公告】發招聘帖的同窗留意一下這裏",
    "href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12",
    "comment1": "呵呵呵呵"
  },
  {
    "title": "發佈一款 Sublime Text 下的 JavaScript 語法高亮插件",
    "href": "http://cnodejs.org/topic/54207e2efffeb6de3d61f68f",
    "comment1": "沙發!"
  }
]

挑戰

以上文目標爲基礎,輸出 comment1 的做者,以及他在 cnode 社區的積分值。git

示例:github

[
  {
    "title": "【公告】發招聘帖的同窗留意一下這裏",
    "href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12",
    "comment1": "呵呵呵呵",
    "author1": "auser",
    "score1": 80
  },
  ...
]

知識點

1.體會 Node.js 的 callback hell 之美編程

2.學習使用 eventproxy 這一利器控制併發json

課程內容

注意,cnodejs.org 網站有併發鏈接數的限制,因此當請求發送太快的時候會致使返回值爲空或報錯。建議一次抓取3個主題便可。文中的40只是爲了方便講解api

這一章咱們來到了 Node.js 最牛逼的地方——異步併發的內容了。數組

上一課咱們介紹瞭如何使用 superagent 和 cheerio 來取主頁內容,那隻須要發起一次 http get 請求就能辦到。但此次,咱們須要取出每一個主題的第一條評論,這就要求咱們對每一個主題的連接發起請求,並用 cheerio 去取出其中的第一條評論。併發

CNode 目前每一頁有 40 個主題,因而咱們就須要發起 1 + 40 個請求,來達到咱們這一課的目標。

此次課程咱們須要用到三個庫:superagent cheerio eventproxy(https://github.com/JacksonTian/eventproxy )

手腳架的工做各位本身來,咱們一步一步來一塊兒寫出這個程序。

首先 app.js 應該長這樣,咱們先獲取到首頁的全部的連接:

var superagent = require('superagent');
var cheerio = require('cheerio');
// url 模塊是 Node.js 標準庫裏面的
// http://nodejs.org/api/url.html
var url = require('url');

var cnodeUrl = 'https://cnodejs.org/';

superagent.get(cnodeUrl)
  .end(function (err, res) {
    if (err) {
      return console.error(err);
    }
    var topicUrls = [];
    var $ = cheerio.load(res.text);
    // 獲取首頁全部的連接
    $('#topic_list .topic_title').each(function (idx, element) {
      var $element = $(element);
      // $element.attr('href') 原本的樣子是 /topic/542acd7d5d28233425538b04
      // 咱們用 url.resolve 來自動推斷出完整 url,變成
      // https://cnodejs.org/topic/542acd7d5d28233425538b04 的形式
      // 具體請看 http://nodejs.org/api/url.html#url_url_resolve_from_to 的示例
      var href = url.resolve(cnodeUrl, $element.attr('href'));
      topicUrls.push(href);
    });

    console.log(topicUrls);
  });

運行 node app.js

輸出以下圖:
image

OK,這時候咱們已經獲得全部 url 的地址了,接下來,咱們把這些地址都抓取一遍,就完成了,Node.js 就是這麼簡單。

抓取以前,仍是得介紹一下 eventproxy 這個庫。

用 js 寫過異步的同窗應該都知道,若是你要併發異步獲取兩三個地址的數據,而且要在獲取到數據以後,對這些數據一塊兒進行利用的話,常規的寫法是本身維護一個計數器。

先定義一個 var count = 0,而後每次抓取成功之後,就 count++。若是你是要抓取三個源的數據,因爲你根本不知道這些異步操做到底誰先完成,那麼每次當抓取成功的時候,就判斷一下 count === 3。當值爲真時,使用另外一個函數繼續完成操做。

而 eventproxy 就起到了這個計數器的做用,它來幫你管理到底這些異步操做是否完成,完成以後,它會自動調用你提供的處理函數,並將抓取到的數據當參數傳過來。

假設咱們不使用 eventproxy 也不使用計數器時,抓取三個源的寫法是這樣的:

// 參考 jquery 的 $.get 的方法
$.get("http://data1_source", function (data1) {
  // something
  $.get("http://data2_source", function (data2) {
    // something
    $.get("http://data3_source", function (data3) {
      // something
      var html = fuck(data1, data2, data3);
      render(html);
    });
  });
});

上述的代碼你們都寫過吧。先獲取 data1,獲取完成以後獲取 data2,而後再獲取 data3,而後 fuck 它們,進行輸出。

但你們應該也想到了,其實這三個源的數據,是能夠並行去獲取的,data2 的獲取並不依賴 data1 的完成,data3 同理也不依賴 data2。

因而咱們用計數器來寫,會寫成這樣:

(function () {
  var count = 0;
  var result = {};

  $.get('http://data1_source', function (data) {
    result.data1 = data;
    count++;
    handle();
    });
  $.get('http://data2_source', function (data) {
    result.data2 = data;
    count++;
    handle();
    });
  $.get('http://data3_source', function (data) {
    result.data3 = data;
    count++;
    handle();
    });

  function handle() {
    if (count === 3) {
      var html = fuck(result.data1, result.data2, result.data3);
      render(html);
    }
  }
})();

若是咱們用 eventproxy,寫出來是這樣的:

var ep = new eventproxy();
ep.all('data1_event', 'data2_event', 'data3_event', function (data1, data2, data3) {
  var html = fuck(data1, data2, data3);
  render(html);
});

$.get('http://data1_source', function (data) {
  ep.emit('data1_event', data);
  });

$.get('http://data2_source', function (data) {
  ep.emit('data2_event', data);
  });

$.get('http://data3_source', function (data) {
  ep.emit('data3_event', data);
  });

好看多了是吧,也就是個高等計數器嘛。

ep.all('data1_event', 'data2_event', 'data3_event', function (data1, data2, data3) {});

這一句,監聽了三個事件,分別是 data1_event, data2_event, data3_event,每次當一個源的數據抓取完成時,就經過 ep.emit() 來告訴 ep 本身,某某事件已經完成了。

當三個事件未同時完成時,ep.emit() 調用以後不會作任何事;當三個事件都完成的時候,就會調用末尾的那個回調函數,來對它們進行統一處理。

eventproxy 提供了很多其餘場景所需的 API,但最最經常使用的用法就是以上的這種,即:

1.先 var ep = new eventproxy(); 獲得一個 eventproxy 實例。

2.告訴它你要監聽哪些事件,並給它一個回調函數。ep.all('event1', 'event2', function (result1, result2) {})。

3.在適當的時候 ep.emit('event_name', eventData)。
eventproxy 這套處理異步併發的思路,我一直以爲就像是彙編裏面的 goto 語句同樣,程序邏輯在代碼中隨處跳躍。原本代碼已經執行到 100 行了,忽然 80 行的那個回調函數又開始工做了。若是你異步邏輯複雜點的話,80 行的這個函數完成以後,又激活了 60 行的另一個函數。併發和嵌套的問題雖然解決了,但老祖宗們消滅了幾十年的 goto 語句又回來了。

至於這套思想糟糕不糟糕,我我的卻是以爲仍是不糟糕,用熟了看起來蠻清晰的。不過 js 這門渣渣語言原本就亂嘛,什麼變量提高(http://www.cnblogs.com/damonlan/archive/2012/07/01/2553425.html )啊,沒有 main 函數啊,變量做用域啊,數據類型經常簡單得只有數字、字符串、哈希、數組啊,這一系列的問題,都不是事兒。

編程語言美醜啥的,咱心中有佛就好。

回到正題,以前咱們已經獲得了一個長度爲 40 的 topicUrls 數組,裏面包含了每條主題的連接。那麼意味着,咱們接下來要發出 40 個併發請求。咱們須要用到 eventproxy 的 #after API。

你們自行學習一下這個 API 吧:https://github.com/JacksonTian/eventproxy#%E9%87%8D%E5%A4%8D%E5%BC%82%E6%AD%A5%E5%8D%8F%E4%BD%9C

我代碼就直接貼了哈。

// 獲得 topicUrls 以後

// 獲得一個 eventproxy 的實例
var ep = new eventproxy();

// 命令 ep 重複監聽 topicUrls.length 次(在這裏也就是 40 次) `topic_html` 事件再行動
ep.after('topic_html', topicUrls.length, function (topics) {
  // topics 是個數組,包含了 40 次 ep.emit('topic_html', pair) 中的那 40 個 pair

  // 開始行動
  topics = topics.map(function (topicPair) {
    // 接下來都是 jquery 的用法了
    var topicUrl = topicPair[0];
    var topicHtml = topicPair[1];
    var $ = cheerio.load(topicHtml);
    return ({
      title: $('.topic_full_title').text().trim(),
      href: topicUrl,
      comment1: $('.reply_content').eq(0).text().trim(),
    });
  });

  console.log('final:');
  console.log(topics);
});

topicUrls.forEach(function (topicUrl) {
  superagent.get(topicUrl)
    .end(function (err, res) {
      console.log('fetch ' + topicUrl + ' successful');
      ep.emit('topic_html', [topicUrl, res.text]);
    });
});

輸出長這樣:

image

附完整代碼:

var eventproxy = require('eventproxy')
var superagent = require('superagent')
var cheerio = require('cheerio')

//url 模塊是Node.js 標準庫裏面的
//http://nodejs.org/api/url.html
var url = require('url')

var cnodeUrl = 'https://cnodejs.org/'

superagent.get(cnodeUrl)
  .end(function(err,res){
    if (err) {
      return console.log(err);
    }
    var topicUrls = []
    var $ = cheerio.load(res.text)
    //獲取首頁全部的連接
    $('#topic_list .topic_title').each(function(idx, element){
      var $element = $(element)
      // $element.attr('href') 原本的樣子是 /topic/542acd7d5d28233425538b04
      // 咱們用 url.resolve 來自動推斷出完整 url,變成
      //https://cnodejs.org/topic/542acd7d5d28233425538b04 的形式
      // 具體請看 http://nodejs.org/api/url.html#url_url_resolve_from_to 的示例
      //http://nodejs.cn/api/url.html#url_url_resolve_from_to//中文版😳
      var href = url.resolve(cnodeUrl, $element.attr('href'));
      topicUrls.push(href)
    })
    console.log('topicUrls:',topicUrls)
    // 獲得 topicUrls 以後
    // 獲得一個 eventproxy 實例
    var ep = new eventproxy();

    //命令 ep 重複監聽 topicUrls.length 次(在這裏也就是 40次)`topic_html` 事件再行動
    ep.after('topic_html',topicUrls.length, function(topics){
      // topics 是個數組,包含了 40次 em.emit('topic_html',pair)中的 那個 40個pair
      // 開始行動
      topics = topics.map(function(topicPair){
        // 接下來都是 jq的用法了
        var topicUrl = topicPair[0];
        var topicHtml = topicPair[1];
        var $ = cheerio.load(topicHtml);
        return({
          title: $('.topic_full_title').text().trim(),
          href: topicUrl,
          comment1: $('.reply_content').eq(0).text().trim()
        })
      })
      console.log('final:')
      console.log(topics)
    })
    topicUrls.forEach(function (topicUrl) {
      superagent.get(topicUrl)
        .end(function (err,res) {
          console.log('fetch'+topicUrl+'successful')
          ep.emit('topic_html',[topicUrl, res.text])
        })
    })
  })

閱讀原文

相關文章
相關標籤/搜索