Страницы

воскресенье, 22 декабря 2013 г.

Управление потоком выполнения в Node.js.

В завершении предыдущего поста по теме я обратил внимание на асинхронность выполнения запросов, в связи с чем признался в лукавстве. На этот раз обещаю исправиться :). Рассмотрим тему управления потоком выполнения в Node.js: последовательный и параллельный варианты, на примере созданного ранее кода.

Для начала напомню в чем же состоял обман. Приведу последний вариант кода приложения, в который я добавил несколько комментариев для наглядности:
var request = require('request'),
    cheerio = require('cheerio'),
    http = require('http'),
    url = 'http://www.vr-online.ru';

var cache = {  
  push: function(o) {
    for (var i=0; i<this.data.length; i++) {
      if (this.data[i]['name'] === o.name) {
        this.data[i]['count']++;
        return;
      }
    }
    o['count'] = 1;
    this.data.push(o);
  },
  build: function() {
    this.page = '<html><meta http-equiv="Content-Type" content="text/html; charset=utf-8">' + 
        '<style type="text/css">img{width:40px;height:40px;}</style><body><ul id="auth"></ul></body></html>';
    var $ = cheerio.load(this.page), ul = $('#auth'), self = this;
    for (var i=0; i<this.data.length; i++) {
      // здесь вызываем функцию, в которой начинается асинхронное выполнение
      getImg(this.data[i], function(author) {
        ul.append('<li>' + '<a href="http://' + url + author['url'] + '">' + '<img src="' + author['img'] + '"> ' + 
            author['name'] + '</a> - ' + author['count'] + '</li>');
        self.page = $.html();
        console.log('author = ', author['name']); // выполняется после сообщения о завершении построения кэша
      });
    }    
    console.log('--- Cache is built ---\n'); // выполняется прежде завершения выполнения запросов
  }
};

function getImg(author, cb) {
  // здесь начинается асинхронное выполнение
  request(url + author['url'], function(err, res, body) {
    if (!err && res.statusCode == 200) {
      var $ = cheerio.load(body);
      var img = $('img', '.profile').first().attr('src');
      if (img.indexOf('http://') !== 0) img = url + img;
      author['img'] = img;      
      cb(author);
    }
  });
}

function buildCache() {
  console.log('\n--- Building cache ---\n');
  request(url, function(err, res, body) {
    if (!err && res.statusCode == 200) {
      var $ = cheerio.load(body);
      cache.data = [];
      $('p a','.meta').each(function() {        
        cache.push({name: this.text(), url: this.attr('href')});
      });      
      cache.build();
    }
  });
}

buildCache();
setInterval(function() {buildCache();}, 60000);

http.createServer( function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});  
  res.end(cache.page);
}).listen('3000', '127.0.0.1', function() {
  console.log('--- Server started on http://localhost:3000 ---\n');
});

Выполним код, спустя минуту наблюдаем следующее:

Понятно, что с выводом в консоль сообщения о завершении построения кэша мы спешим, так как после нашего сообщения продолжается асинхронное выполнение запросов на сервер.
Я не случайно решил подождать минуту прежде чем сделать скриншот, так как хочу обратить внимание на случайный порядок вывода в консоль имен авторов - еще одно доказательство асинхронности выполнения кода.

В нашем случае наиболее очевидный вариант - дождаться завершения выполнения всех запросов, после чего вывести сообщение о завершении построения кэша. Так называемый параллельный вариант - он уже используется, необходимо только просечь момент завершения последнего запроса.
Для этого добавим в код переменную - счетчик выполненных асинхронных запросов, а в функции обратного вызова будем увеличивать значение этой переменной и проверять, не равно ли ее значение длине массива, содержащего информацию об авторах, а следовательно и количеству запросов.
var request = require('request'),
    cheerio = require('cheerio'),
    http = require('http'),
    url = 'http://www.vr-online.ru';

var cache = {  
  push: function(o) {
    for (var i=0; i<this.data.length; i++) {
      if (this.data[i]['name'] === o.name) {
        this.data[i]['count']++;
        return;
      }
    }
    o['count'] = 1;
    this.data.push(o);
  },
  build: function() {
    this.page = '<html><meta http-equiv="Content-Type" content="text/html; charset=utf-8">' + 
        '<style type="text/css">img{width:40px;height:40px;}</style><body><ul id="auth"></ul></body></html>';
    var $ = cheerio.load(this.page), ul = $('#auth'), self = this;
    var counter = 0; // счетчик выполненных асинхронных запросов
    for (var i=0; i<this.data.length; i++) {
      // здесь вызываем функцию, в которой начинается асинхронное выполнение
      getImg(this.data[i], function(author) {
        ul.append('<li>' + '<a href="http://' + url + author['url'] + '">' + '<img src="' + author['img'] + '"> ' + 
            author['name'] + '</a> - ' + author['count'] + '</li>');
        self.page = $.html();
        counter++; // увеличиваем значение счетчика
        console.log('author = ', author['name'], ' counter = ', counter);
        // сравниваем значение счетчика с длиной массива - вычисляем последний запрос
        if (counter === self.data.length) console.log('\n--- Cache is built ---');
      });
    }    
  }
};

function getImg(author, cb) {
  // здесь начинается асинхронное выполнение
  request(url + author['url'], function(err, res, body) {    
    if (!err && res.statusCode == 200) {
      var $ = cheerio.load(body);
      var img = $('img', '.profile').first().attr('src');
      if (img.indexOf('http://') !== 0) img = url + img;
      author['img'] = img;      
      cb(author);
    }
  });
}

function buildCache() {
  console.log('\n--- Building cache ---\n');  
  request(url, function(err, res, body) {
    if (!err && res.statusCode == 200) {
      var $ = cheerio.load(body);
      cache.data = [];
      $('p a','.meta').each(function() {        
        cache.push({name: this.text(), url: this.attr('href')});
      });      
      cache.build();
    }
  });
}

buildCache();
setInterval(function() {buildCache();}, 60000);

http.createServer( function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});  
  res.end(cache.page);
}).listen('3000', '127.0.0.1', function() {
  console.log('--- Server started on http://localhost:3000 ---\n');
});

Выполняем код:

Вот теперь все по-чесноку :).

А что если нам захотелось бы отправлять запросы на сервер последовательно, друг за другом?
В этом случае нашей функции обратного вызова не мешало бы присвоить имя - так ее и назовем - callback, для того, чтобы отправлять ее в качестве аргумента функции getImg() из нее же самой. Переменная - счетчик здесь тоже будет очень даже кстати, а цикл явно лишний.
var request = require('request'),
    cheerio = require('cheerio'),
    http = require('http'),
    url = 'http://www.vr-online.ru';

var cache = {  
  push: function(o) {
    for (var i=0; i<this.data.length; i++) {
      if (this.data[i]['name'] === o.name) {
        this.data[i]['count']++;
        return;
      }
    }
    o['count'] = 1;
    this.data.push(o);
  },
  build: function() {
    this.page = '<html><meta http-equiv="Content-Type" content="text/html; charset=utf-8">' + 
        '<style type="text/css">img{width:40px;height:40px;}</style><body><ul id="auth"></ul></body></html>';
    var $ = cheerio.load(this.page), ul = $('#auth'), self = this;
    var counter = 0; // счетчик выполненных асинхронных функций    
    // здесь вызываем функцию, в которой начинается асинхронное выполнение
    getImg(this.data[counter], function callback(author) { // присвоим функции имя
      ul.append('<li>' + '<a href="http://' + url + author['url'] + '">' + '<img src="' + author['img'] + '"> ' + 
          author['name'] + '</a> - ' + author['count'] + '</li>');
      self.page = $.html();
      counter++; // увеличиваем значение счетчика
      console.log('author = ', author['name'], ' counter = ', counter);
      // сравниваем значение счетчика с длиной массива - вычисляем последний запрос
      if (counter === self.data.length) {
        console.log('\n--- Cache is built ---');
      } else {
        getImg(self.data[counter], callback); // вызываем функцию еще раз
      }
    });    
  }
};

function getImg(author, cb) {
  // здесь начинается асинхронное выполнение
  request(url + author['url'], function(err, res, body) {    
    if (!err && res.statusCode == 200) {
      var $ = cheerio.load(body);
      var img = $('img', '.profile').first().attr('src');
      if (img.indexOf('http://') !== 0) img = url + img;
      author['img'] = img;      
      cb(author);
    }
  });
}

function buildCache() {
  console.log('\n--- Building cache ---\n');  
  request(url, function(err, res, body) {
    if (!err && res.statusCode == 200) {
      var $ = cheerio.load(body);
      cache.data = [];
      $('p a','.meta').each(function() {        
        cache.push({name: this.text(), url: this.attr('href')});
      });      
      cache.build();
    }
  });
}

buildCache();
setInterval(function() {buildCache();}, 60000);

http.createServer( function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});  
  res.end(cache.page);
}).listen('3000', '127.0.0.1', function() {
  console.log('--- Server started on http://localhost:3000 ---\n');
});

Выполним код:

Приложение отработало заметно медленнее. Зато мы с вами научились строить последовательный поток выполнения. Разумеется в нашем случае это не самый лучший вариант, я бы даже сказал совсем не вариант, но в качестве иллюстрации вполне сгодится.

В завершение замечу, что освещенный вопрос может показаться трудным для понимания. Я постарался изложить материал как можно более доступно, но если вы считаете, что у меня это не особенно получилось - жалуйтесь :).