Skip to content

Latest commit

 

History

History
1028 lines (753 loc) · 39.8 KB

File metadata and controls

1028 lines (753 loc) · 39.8 KB

五、使用事件和子进程

正如您在本书中已经看到的,Node.js 有一个健壮的框架来处理许多例程和重要任务。在这一章中,你将全面理解你在前面章节中看到的一些概念。您将首先深入了解 Node.js 的一个基石,EventEmitters。关于这些,你将看到如何创建自定义事件和为它们添加监听器的方法,以及如何创建单个事件。所有这些都将展示如何通过在 Node.js 中有策略地实现事件来减少无休止的回调噩梦。

接下来,您将揭开用子流程扩展 Node.js 流程的神秘面纱。您将看到如何产生一个子进程,以及如何执行 shell 命令和文件。然后,您将学习如何派生一个流程,这将使我们能够在 Node.js 中对流程进行集群。

5-1.创建自定事件

问题

您已经创建了一个 Node.js 应用,但是您需要通过发出一个自定义事件在其中进行通信。

解决办法

在此解决方案中,您将创建一个 Node.js 应用,演示如何创建和侦听自定义事件。您将创建一个在超时期限到期后执行的事件。这表示操作完成时应用中会出现的情况。这将调用函数doATask,该函数将返回操作是成功还是失败的状态。有两种方法可以实现这一点。

首先,您将创建特定于状态的事件。这需要检查状态并专门为该状态创建一个事件,以及绑定到那些特定的事件来处理特殊情况。这在清单 5-1 中进行了演示。

清单 5-1 。单个状态的自定义事件

/**
* Custom Events
*/

var events = require('events'),
        emitter = new events.EventEmitter();

function doATask(status) {
        if (status === 'success') {
                emitter.emit('taskSuccess'); // Specific event
        } else if (status === 'fail') {
                emitter.emit('taskFail');
        }
}

emitter.on('taskSuccess', function() {
        console.log('task success!');
});

emitter.on('taskFail', function() {
        console.log('task fail');
});

// call task with success status
setTimeout(doATask, 500, 'success');

// set task to fail
setTimeout(doATask, 1000, 'fail');

虽然您看到这有效地使事件适当地传播,但这仍然会导致您创建两个单独的事件来发出。这可以很容易地修改成更加精简和高效的,正如你将在清单 5-2 中看到的。

清单 5-2 。一个发射器来统治他们

/**
* Custom Events
*/

var events = require('events'),
        emitter = new events.EventEmitter();

function doATask(status) {
        // This event passes arguments to detail status
        emitter.emit('taskComplete', 'complete', status);
}

// register listener for task complete
emitter.on('taskComplete', function(type, status) {
        console.log('the task is ', type, ' with status ', status);
});

// call task with success status
setTimeout(doATask, 500, 'success');

// set task to fail
setTimeout(doATask, 1000, 'fail');

这是一个更精简、更高效的实现。您可以发出适用于应用中多个状态的单个事件。在这些示例中,您看到了一个实现,其中所有事件都是从同一个源文件中处理和发出的。在清单 5-3 中,你可以看到一个共享事件发射器来发射事件的例子,该事件将在当前模块之外的模块中被接收。

清单 5-3 。发射模块

/**
* Custom Events
*/

var events = require('events'),
        emitter = new events.EventEmitter(),
        myModule = require('./5-1-3.js')(emitter);

emitter.on('custom', function() {
        console.log('custom event received');
});

emitter.emit('custom');

为应用创建自定义事件的另一种方法是利用全局流程对象。这个 Node.js 对象是一个EventEmitter,它将允许您注册将在流程中共享的事件。这种类型的事件是从清单 5-4 中的代码发出的。

清单 5-4 。在 Node.js 进程上发出一个事件

/* Module.js file */
var myMod = module.exports = {
        emitEvent: function() {
                process.emit('globalEvent');
        }
};

它是如何工作的

在这个例子中,您看到了创建自定义事件的多种方式。这些事件可以在任何有 Node.js EventEmitter的地方发出。Node.js EventEmitter类是组成应用的模块之间和内部通信的基石之一。

当您构建一个事件时,首先遇到的是EventEmitter类。此类由事件的对象集合组成,这些事件引用已注册的不同类型的事件。类型的概念就是你给你的事件起的名字,比如taskComplete或者taskFail。当您实际使用EventEmitter's发出方法发出事件时,这很重要。

清单 5-5 。EventEmitter 的 Emit 方法

EventEmitter.prototype.emit = function(type) {
  var er, handler, len, args, i, listeners;

  if (!this._events)
    this._events = {};

  // If there is no 'error' event listener then throw.
  if (type === 'error') {
    if (!this._events.error ||
        (typeof this._events.error === 'object' &&
         !this._events.error.length)) {
      er = arguments[1];
      if (this.domain) {
        if (!er) er = new TypeError('Uncaught, unspecified "error" event.');
        er.domainEmitter = this;
        er.domain = this.domain;
        er.domainThrown = false;
        this.domain.emit('error', er);
      } else if (er instanceof Error) {
        throw er; // Unhandled 'error' event
      } else {
        throw TypeError('Uncaught, unspecified "error" event.');
      }
      return false;
    }
  }

  handler = this._events[type];

  if (typeof handler === 'undefined')
    return false;

  if (this.domain && this !== process)
    this.domain.enter();

  if (typeof handler === 'function') {
    switch (arguments.length) {
      // fast cases
      case 1:
        handler.call(this);
        break;
      case 2:
        handler.call(this, arguments[1]);
        break;
      case 3:
        handler.call(this, arguments[1], arguments[2]);
        break;
      // slower
      default:
        len = arguments.length;
        args = new Array(len - 1);
        for (i = 1; i < len; i++)
          args[i - 1] = arguments[i];
        handler.apply(this, args);
    }
  } else if (typeof handler === 'object') {
    len = arguments.length;
    args = new Array(len - 1);
    for (i = 1; i < len; i++)
      args[i - 1] = arguments[i];

    listeners = handler.slice();
    len = listeners.length;
    for (i = 0; i < len; i++)
      listeners[i].apply(this, args);
  }

  if (this.domain && this !== process)
    this.domain.exit();

  return true;
};

这种方法包括两个主要部分。首先,是对“错误”事件的特殊处理。这将按照您的预期发出错误事件,除非错误事件没有侦听器。在这种情况下,Node.js 将抛出错误,该方法将返回 false。此方法的第二部分是处理非错误事件的部分。

在检查以确保指定类型的给定事件有处理程序之后,Node.js 接着检查事件处理程序是否是一个函数。如果是函数,Node.js 将解析来自 emit 方法的参数,并将这些参数应用到处理程序。这就是清单 5-2 中的将参数传递给taskComplete事件的方式。提供的额外参数在 emit 方法调用处理程序时应用。

其他解决方案都使用相同的发射方法,但是它们以不同的方式获得发射事件的结果。清单 5-4 表示一个在整个应用中共享的 Node.js 模块。这个模块包含一个函数,该函数将向应用的其余部分发出一个事件。在这个解决方案中实现这一点的方法是利用主 Node.js 进程是一个EventEmitter的知识。这意味着您只需通过调用process.emit('globalEvent')来发出事件,共享该进程的应用的一部分将接收该事件。

5-2.为自定义事件添加侦听器

问题

在上一节中,您已经发出了自定义事件,但是如果没有合适的方法绑定到这些事件,您将无法使用它们。为此,您需要向这些事件添加侦听器。

解决办法

这个解决方案是第 5-1 节的对应部分。在上一节中,您实现了EventEmitters并发出了事件。现在,您需要为这些事件添加侦听器,以便可以在您的应用中处理它们。这个过程就像发射事件一样简单,如清单 5-6 所示。

清单 5-6 。向自定义事件和系统事件添加事件监听器

/**
* Custom Events
*/

var events = require('events'),
        emitter = new events.EventEmitter();

function doATask(status) {
        if (status === 'success') {
                emitter.emit('taskSuccess'); // Specific event
        } else if (status === 'fail') {
                emitter.emit('taskFail');
        }
        // This event passes arguments to detail status
        emitter.emit('taskComplete', 'complete', status);
}
emitter.on('newListener', function(){
        console.log('a new listener was added');
});
emitter.on('taskSuccess', function() {
        console.log('task success!');
});

emitter.on('taskFail', function() {
        console.log('task fail');
});

// register listener for task complete
emitter.on('taskComplete', function(type, status) {
        console.log('the task is ', type, ' with status ', status);
});

// call task with success status
setTimeout(doATask, 2e3, 'success');

// set task to fail
setTimeout(doATask, 4e3, 'fail');

您还可以将您的EventEmitter传递给一个外部模块,然后从那个单独的代码段中监听事件。

清单 5-7 。从外部模块监听事件

/**
* External Module
*/
module.exports = function(emitter) {
        emitter.on('custom', function() {
                console.log('bazinga');
        });
};

正如您使用 Node.js 进程EventEmitter发出事件一样,您可以将侦听器绑定到该进程并接收事件。

清单 5-8 。Node.js 进程范围侦听器

/**
* Global event
*/
var ext = require('./5-1-5.js');

process.on('globalEvent', function() {
        console.log('global event');
});

ext.emitEvent();

它是如何工作的

当您检查清单 5-6 中的解决方案时,您应该很快注意到如何向事件添加监听器。这和调用EventEmitter.on()方法 一样简单。的。EventEmitteron方法接受两个参数:一是事件类型名;第二,侦听器回调,它将接受传递给emit()事件的任何参数。的。on方法实际上只是EventEmitter addListener函数 的包装器,它采用相同的两个参数。您可以直接调用此方法来代替调用。on功能一样。

清单 5-9 。事件发射器 addListener

EventEmitter.prototype.addListener = function(type, listener) {
  var m;

  if (typeof listener !== 'function')
    throw TypeError('listener must be a function');

  if (!this._events)
    this._events = {};

  // To avoid recursion in the case that type === "newListener"! Before
  // adding it to the listeners, first emit "newListener".
  if (this._events.newListener)
    this.emit('newListener', type, typeof listener.listener === 'function' ?
              listener.listener : listener);

  if (!this._events[type])
    // Optimize the case of one listener. Don't need the extra array object.
    this._events[type] = listener;
  else if (typeof this._events[type] === 'object')
    // If we've already got an array, just append.
    this._events[type].push(listener);
  else
    // Adding the second element, need to change to array.
    this._events[type] = [this._events[type], listener];

  // Check for listener leak
  if (typeof this._events[type] === 'object' && !this._events[type].warned) {
    var m;
    if (this._maxListeners !== undefined) {
      m = this._maxListeners;
    } else {
      m = EventEmitter.defaultMaxListeners;
    }

    if (m && m > 0 && this._events[type].length > m) {
      this._events[type].warned = true;
      console.error('(node) warning: possible EventEmitter memory ' +
                    'leak detected. %d listeners added. ' +
                    'Use emitter.setMaxListeners() to increase limit.',
                    this._events[type].length);
      console.trace();
    }
  }

  return this;
};

EventEmitter.prototype.on = EventEmitter.prototype.addListener;

从源代码片段中可以看出,addListener方法完成了几项任务。首先,在验证侦听器回调是一个函数之后,addListener方法发出它自己的事件“newListener”,以表明已经添加了一个给定类型的新侦听器。

发生的第二件事是addListener函数将监听器函数推到它所绑定的事件。在上一节中,这个函数成为了每种事件类型的处理函数。根据发射器本身提供的参数数量,emit()函数将对该函数执行.call().apply()操作。

最后在addListener函数中,你会发现 Node.js 非常友好,试图保护你免受潜在的内存泄漏。它通过检查侦听器的数量是否超过预定义的限制(默认为 10)来实现这一点。当然,您可以通过使用setMaxListeners()方法将这个值配置为一个更高的值,当您超过这个侦听器数量时,会出现一个有用的警告。

5-3.实现一次性事件

问题

您需要在 Node.js 应用中实现一个只希望执行一次的事件。

解决办法

假设您有一个要完成重要任务的应用。这个任务需要完成,但只能完成一次。假设您有一个监听聊天室成员的事件,该成员要么退出应用,要么断开连接。这个事件只需要处理一次。将该事件向应用的其他用户广播两次是没有意义的,因此您限制了处理该事件的次数。

有两种方法可以做到这一点。一种是手动处理注册事件,然后在接收到一次事件后删除事件侦听器。

清单 5-10 。手动注册一次事件侦听器

/**
* Implementing a One time event
*/

var events = require('events'),
                emitter = new events.EventEmitter();

function listener() {
        console.log('one Timer');
        emitter.removeListener('oneTimer', listener);
}
emitter.on('oneTimer', listener);

emitter.emit('oneTimer');
emitter.emit('oneTimer');

这需要在侦听器函数中进行二次调用,以便能够从事件中移除侦听器。随着项目的增长,这可能会变得难以处理,因此 Node.js 有一个本机实现来实现同样的效果。

清单 5-11 。emtter.once()

/**
* Implementing a One-time event
*/

var events = require('events'),
                emitter = new events.EventEmitter();

/* EASIER */

emitter.once('onceOnly', function() {
        console.log('one Only');
});

emitter.emit('onceOnly');
emitter.emit('onceOnly');

它是如何工作的

第一个注册一个事件侦听器只绑定一次所发出的事件的例子非常容易理解。首先用侦听器的函数回调绑定到事件。然后,在侦听器中处理该回调,并从事件中移除该侦听器。这可以防止对来自同一发射器的事件进行任何进一步的处理。

这是因为removeListener方法 的缘故,它接受一个事件类型和一个特定的监听器函数。

清单 5-12 。event 发射器 removeListener 方法

EventEmitter.prototype.removeListener = function(type, listener) {
  var list, position, length, i;

  if (typeof listener !== 'function')
    throw TypeError('listener must be a function');

  if (!this._events || !this._events[type])
    return this;

  list = this._events[type];
  length = list.length;
  position = -1;

  if (list === listener ||
      (typeof list.listener === 'function' && list.listener === listener)) {
    this._events[type] = undefined;
    if (this._events.removeListener)
      this.emit('removeListener', type, listener);

  } else if (typeof list === 'object') {
    for (i = length; i-- > 0;) {
      if (list[i] === listener ||
          (list[i].listener && list[i].listener === listener)) {
        position = i;
        break;
      }
    }

    if (position < 0)
      return this;

    if (list.length === 1) {
      list.length = 0;
      this._events[type] = undefined;
    } else {
      list.splice(position, 1);
    }

    if (this._events.removeListener)
      this.emit('removeListener', type, listener);
  }

  return this;
};

removeListener函数 将通过递归搜索 events 对象来定位需要移除的特定事件,以便找到您正在搜索的类型和函数组合。然后,它将移除事件绑定,以便侦听器不再在后续事件中注册。

一个类似于手工发射一次函数的方法是EventEmitter.once方法 。

清单 5-13 。EventEmitter once 方法

EventEmitter.prototype.once = function(type, listener) {
  if (typeof listener !== 'function')
    throw TypeError('listener must be a function');

  function g() {
    this.removeListener(type, g);
    listener.apply(this, arguments);
  }

  g.listener = listener;
  this.on(type, g);

  return this;
};

该方法接受您希望一次性绑定到的侦听器和事件类型。然后,它创建一个内部函数,该函数将应用侦听器。在内部函数中调用此侦听器之前,实际的侦听器将从事件中移除。这就像您的自定义一次性方法一样,因为它会在事件第一次执行时移除侦听器。

在下一节中,我们将研究如何使用这些事件和自定义事件来减少 Node.js 应用中的回调量。

5-4.使用事件减少回调

问题

您有一个 Node.js 应用,它有多个回调函数。这段代码已经变得有点笨拙,所以你想通过利用EventEmitter来减少回调。

解决办法

想象一下,一个购物应用必须访问数据库中的数据,操作这些数据,然后刷新数据库并将状态发送回客户端。这可能是获取购物车、添加商品,并让客户知道购物车已经更新。第一个例子是使用回调编写的。

清单 5-14 。使用回调的购物车

var initialize = function() {
        retrieveCart(function(err, data) {
                if (err) console.log(err);

                data['new'] = 'other thing';

                updateCart(data, function(err, result) {
                        if (err) console.log(err);

                        sendResults(result, function(err, status) {
                                if (err) console.log(err);
                                console.log(status);
                        });
                });
        });
};

// simulated call to a database
var retrieveCart = function(callback) {
        var data = { item: 'thing' };
        return callback(null, data );
};
// simulated call to a database
var updateCart = function(data, callback) {
        return callback(null, data);
};

var sendResults = function(data, callback) {
        console.log(data);
        return callback(null, 'Cart Updated');
};

initialize();

首先,理解在 Node.js 中使用回调并没有错。事实上,这可能是在 Node.js 中处理异步编程的最流行的方式。然而,你也可以看到,在你有大量必须连续发生的回调的情况下,比如在清单 5-14 中,代码可能变得不那么容易理解。为了纠正这一点,您可以合并事件,以更简洁的方式管理应用流。

清单 5-15 。使用事件代替回调

/**
* Reducing callbacks
*/

var events = require('events');

var MyCart = function() {
        this.data = { item: 'thing' };
};
MyCart.prototype = new events.EventEmitter();

MyCart.prototype.retrieveCart = function() {
        //Fetch Data then emit
        this.emit('data', this.data);
};

MyCart.prototype.updateCart = function() {
        // Update data then emit
        this.emit('result', this.data);
};

MyCart.prototype.sendResults = function() {
        console.log(this.data);
        this.emit('complete');
};

var cart = new MyCart();

cart.on('data', function(data) {
        cart.data['new'] = 'other thing';
        cart.updateCart();
});

cart.on('result', function(data) {
        cart.sendResults(data);
});

cart.on('complete', function() {
        console.log('Cart Updated');
});

cart.retrieveCart();

对于同一任务的不同解决方案,两者都使用了相似数量的代码,但是通过转移到事件驱动的模块而不是回调流,回调的数量已经大大减少了。

它是如何工作的

当您想要检查对应用至关重要的代码时,独占使用回调可能会成为一种负担。这也会让参与项目的开发人员感到头疼,因为他们可能不太熟悉项目,不知道给定的回调函数嵌套在哪里。当您希望重构应用以添加另一个要在回调期间执行的方法时,这也会成为一个问题。这些都是您可能选择迁移到事件驱动模型的原因。

清单 5-11 的事件驱动解决方案中,首先创建一个名为MyCart的新对象。你可以假设MyCart用存储在其中的一个项目MyCart初始化。data。然后你的MyCart对象继承了eventsEventEmitter对象。这意味着MyCart可以通过 Node.js 事件模块发送和接收数据。

既然您的对象可以发出和监听事件,您可以通过使用EventEmitter的方法来扩充您的对象。例如,您创建了一个retrieveCart方法,它将从数据存储中获取数据;一旦完成,就会发出'【T2]'事件,传递从购物车中检索到的任何数据。类似地,您创建一个updateCart方法和一个sendResults方法,这两个方法将提醒客户端更新的结果。

然后实例化一个新的MyCart实例。这个新的购物车现在可以绑定到将从MyCart对象发送的事件。您有一个单独的函数来处理每个事件。这使得代码更易于维护,并且在许多情况下更易于扩展。例如,假设您需要为MyCart添加另一个日志功能。现在,您可以将其绑定到每个事件并记录交互,而无需重写整个回调流。

5-5.生下一个孩子。产卵

问题

您需要创建一个子进程来执行 Node.js 应用中的辅助操作。

解决办法

您希望从 Node.js 应用中派生出一个子进程的原因有很多。其中几个可以简单地执行命令行任务,而不需要为应用要求或构建整个模块。在这个解决方案中,您将突出显示两个命令行应用和第三个解决方案,它们将从 spawn 方法执行另一个 Node.js 进程。

清单 5-16 。产卵的孩子

/**
* .spawn
*/

var spawn = require('child_process').spawn,
                pwd = spawn('pwd'),
                ls = spawn('ls', ['-G']),
                nd = spawn('node', ['5-4-1.js']);

pwd.stdout.setEncoding('utf8');

pwd.stdout.on('data', function(data) {
        console.log(data);
});

pwd.stderr.on('data', function(data) {
        console.log(data);
});

pwd.on('close', function(){
        console.log('closed');
});

ls.stdout.setEncoding('utf8');

ls.stdout.on('data', function(data) {
                console.log(data);
});

nd.stdout.setEncoding('utf8');

nd.stdout.on('data', function(data) {
        console.log(data);
});

第一个 spawn 是在当前目录下运行'pwd'命令;第二个是列出该目录中的所有文件。这些只是内置于操作系统中的命令行实用程序。但是,此解决方案中的第三个示例执行命令来运行 Node.js 文件;然后,像前面的例子一样,将输出记录到控制台。

它是如何工作的

产卵是调用 Node.js 中子进程的一种方法,也是child_process模块的一种方法。child_process模块创建了一种通过stdoutstdinstderr传输数据流的方式。由于这个模块的性质,这可以以非阻塞的方式完成,很好地适应 Node.js 模型。

child_process spawn 方法将实例化一个ChildProcess对象,这是一个 Node.js EventEmitter。与ChildProcess对象相关的事件如表 5-1 所示。

表 5-1 。ChildProcess 事件

事件 详述
'消息' 传输一个消息对象,它是 JSON 或一个值。这也可以将套接字或服务器对象作为可选的第二个参数进行传输。
'错误' 将错误传输到回调。当子进程无法生成、无法终止或消息传输失败时,会发生这种情况。
'关闭' 当子进程的所有 stdio 流都完成时发生。这将发送退出代码和与之一起发送的信号。
'断开连接' 方法终止连接时发出。子对象(或父对象)上的 disconnect()方法。
'退出' 在子进程结束后发出。如果进程正常终止,code 是进程的最终退出代码,否则为 null。如果进程因收到信号而终止,signal 是信号的字符串名称,否则为 null。

除了这些ChildProcess事件,产生的孩子也是一个流,正如你在上面看到的。该流包含来自子进程的标准 I/O 的数据。在表 5-2 中列出了与这些流相关的方法,以及子流上的其他方法。

表 5-2 。子进程和其他方法的流事件

方法 描述
。标准输入设备 代表子进程 stdin 的可写流。
。标准输出 表示子进程的标准输出的可读流。
。标准错误 子进程的 stderr 的可读流。
。pid 子进程进程标识符(PID)。
。杀 终止一个进程,可以选择发送终止信号。
。拆开 断开与父级的连接。
。派遣 向. fork 进程发送消息。(在第 5-8 节中有更多的细节。)

现在您对什么是ChildProcess以及它如何适应child_process模块有了更多的了解,您可以看到您的生成子流程的解决方案直接调用了流程。您创建了三个衍生进程。其中每一个都以命令参数开始。这个参数,'pwd、' 'ls、' '【T4]'是将被执行的命令,就像您在终端应用的命令行上运行它一样。child_process.spawn方法中的下一个参数是传递给命令参数的可选参数数组。

您会看到,本例中衍生的进程与在终端命令行中运行以下内容是一样的:

$ pwd &
$ ls –G &
$ node 5-4-1.js &

您也可以从您派生的进程中读取这些命令的输出。这是通过监听child_process.stdout流实现的。如果绑定到数据事件,您会看到这些命令的标准输出,就像在终端中运行命令一样。在第三个 spawn 的例子中,您可以看到本章前一节中整个模块的输出。

还有一个可选的第三个参数可以出现在child_process.spawn方法中。该参数表示帮助设置衍生进程的一组选项。这些选项的值如表 5-3 所示。

表 5-3 。繁殖选项

[计]选项 类型 描述
粗木质残体 线 子进程的当前工作目录。
刺痛 数组或字符串 子进程的 stdio 配置。
自定义 Fds 排列 不推荐使用的功能。
包封/包围(动词 envelop 的简写) 目标 环境键值对。
分离的 布尔代数学体系的 这个子进程将成为一个组长。
用户界面设计(User Interface Design 的缩写) 数字 设置进程的用户标识。
眩倒病 数字 设置进程的组标识。

5-6.使用运行 Shell 命令。执行

问题

您希望从 Node.js 应用中直接执行一个 shell 命令作为子进程。

解决办法

在上一节中,您看到了如何通过使用child_process模块轻松地产生子进程。这可能是一个长时间运行的流程,您希望访问流程中可用的stdio流。与此类似的是child_process.exec法。这两种方法的区别在于。spawn 方法将以流的形式返回所有数据,而。exec 方法将数据作为缓冲区返回。使用这种方法,您可以直接从 Node.js 应用中执行 shell 命令,在 Windows 中执行cmd.exe,或者在其他地方执行/bin/sh。使用本节中的解决方案,您将列出一组文件,并将该操作的结果记录到控制台。然后,您将在系统中搜索包含单词 node 的所有正在运行的进程,再次将输出记录到您的控制台。

清单 5-17 。。高级管理人员

/**
* Running Shell commands with .exec
*/

var exec = require('child_process').exec;

exec('ls -g', function(error, stdout, stderr) {
        if (error) console.log(error);

        console.log(stdout);
});

exec('ps ax | grep node', function(error, stdout, stderr) {
        if (error) console.log(error);

        console.log(stdout);
});

它是如何工作的

这个解决方案通过利用child_process.spawn方法和child_process.execFile方法的各个方面来工作,您将在下一节中研究这两个方法。本质上,当您告诉child_process使用exec方法时,您是在告诉它运行一个将/bin/sh文件(Windows 上的cmd.exe)作为可执行文件的进程。

清单 5-18 。子进程。执行功能

exports.exec = function(command /*, options, callback */) {
  var file, args, options, callback;

  if (typeof arguments[1] === 'function') {
    options = undefined;
    callback = arguments[1];
  } else {
    options = arguments[1];
    callback = arguments[2];
  }

  if (process.platform === 'win32') {
    file = 'cmd.exe';
    args = ['/s', '/c', '"' + command + '"'];
    // Make a shallow copy before patching so we don't clobber the user's
    // options object.
    options = util._extend({}, options);
    options.windowsVerbatimArguments = true;
  } else {
    file = '/bin/sh';
    args = ['-c', command];
  }
  return exports.execFile(file, args, options, callback);
};

事实上,这个函数调用了execFile方法。您将在下一节中看到,这意味着该进程是根据传递给函数的文件参数生成的。

var
child
=
spawn(file, args, {
  cwd
:
options.cwd,
  env
:
options.env,
  windowsVerbatimArguments
: !!
options.windowsVerbatimArguments
});

这意味着您想在命令行中运行的任何东西,都可以通过exec来运行。这就是为什么当您试图以ps ax | grep node的身份运行exec函数来识别所有包含单词 node 的正在运行的进程时,您会看到stdout结果,就像在 shell 中运行它一样。

17774 s001  S+     0:00.06 node 5-6-1.js
17776 s001  S+     0:00.00 /bin/sh -c ps ax | grep node
17778 s001  S+     0:00.00 grep node
11503 s002  S+     0:00.07 node

5-7.使用执行外壳文件。execFile

问题

在您的应用中,您需要将一个文件作为 Node.js 进程的子进程来执行。

解决办法

您已经对child_process模块的这个方法有些熟悉了。在这个解决方案中,您有一个 shell 脚本,其中包含您希望从 Node.js 应用中执行的几个步骤。这些可以在 Node.js 中直接完成,要么生成它们,要么调用.exec方法。然而,通过将它们作为一个文件调用一次,你可以将它们组合在一起,并且仍然可以将它们的组合输出缓冲到execFile的回调函数中。您可以在接下来的两个清单中看到示例 Node.js 应用和将要执行的文件。

清单 5-19 。使用。execFile

/**
* execFile
*/

var execFile = require('child_process').execFile;

execFile('./5-7-1.sh', function(error, stdout, stderr) {
        console.log(stdout);
        console.log(stderr);
        console.log(error);
});

清单 5-20 。要执行的外壳文件

#!/bin/sh
echo "running this shell script from child_process.execFile"
# run another node process
node 5-6-1.js
# and another
node 5-5-1.js

ps ax | grep node

它是如何工作的

当您开始研究execFile方法如何工作时,您会很快意识到它是.spawn方法的衍生物。这个方法非常复杂,为了执行一个文件要做很多事情。首先,execFile函数将接受四个参数。第一个是文件,它是查找要执行的文件和路径所必需的。

第二个是 args 数组,它将把参数传递给要执行的文件;第三个由在衍生进程上设置的特定选项组成;第四个是回调。如你所见,这些选项默认为通用设置,如 utf8 编码,超时设置为零,以及其他如清单 5-21 所示的设置。这个回调就像来自child_process.exec的回调一样,它将一组缓冲的errorstdoutstderr传递给函数,您可以直接从回调中使用这些流。

清单 5-21 。设置 execFile 的文件、参数和选项

exports.execFile = function(file /* args, options, callback */) {
  var args, optionArg, callback;
  var options = {
    encoding: 'utf8',
    timeout: 0,
    maxBuffer: 200 * 1024,
    killSignal: 'SIGTERM',
    cwd: null,
    env: null
  };

  // Parse the parameters.

  if (typeof arguments[arguments.length - 1] === 'function') {
    callback = arguments[arguments.length - 1];
  }

  if (Array.isArray(arguments[1])) {
    args = arguments[1];
    options = util._extend(options, arguments[2]);
  } else {
    args = [];
    options = util._extend(options, arguments[1]);
  }

Node.js 现在通过传入 options 对象来产生子进程。然后,产生的子 Node 通过各种事件监听器和回调函数传递,以便在返回子 Node 本身之前将stdio流聚合到提供给execFile方法的回调函数中,如清单 5-22 所示。这与。spawn 方法将直接返回stdoutstderr流。这里使用。exec方法返回一个从stdoutstderr流创建的缓冲区。

清单 5-22 。正在生成 execFile

  var child = spawn(file, args, {
    cwd: options.cwd,
    env: options.env,
    windowsVerbatimArguments: !!options.windowsVerbatimArguments
  });

  var stdout = '';
  var stderr = '';
  var killed = false;
  var exited = false;
  var timeoutId;

  var err;

  function exithandler(code, signal) {
    if (exited) return;
    exited = true;

    if (timeoutId) {
      clearTimeout(timeoutId);
      timeoutId = null;
    }

    if (!callback) return;

    if (err) {
      callback(err, stdout, stderr);
    } else if (code === 0 && signal === null) {
      callback(null, stdout, stderr);
    } else {
      var e = new Error('Command failed: ' + stderr);
      e.killed = child.killed || killed;
      e.code = code;
      e.signal = signal;
      callback(e, stdout, stderr);
    }
  }

  function errorhandler(e) {
    err = e;
    child.stdout.destroy();
    child.stderr.destroy();
    exithandler();
  }

  function kill() {
    child.stdout.destroy();
    child.stderr.destroy();

    killed = true;
    try {
      child.kill(options.killSignal);
    } catch (e) {
      err = e;
      exithandler();
    }
  }

  if (options.timeout > 0) {
    timeoutId = setTimeout(function() {
      kill();
      timeoutId = null;
    }, options.timeout);
  }

  child.stdout.setEncoding(options.encoding);
  child.stderr.setEncoding(options.encoding);

  child.stdout.addListener('data', function(chunk) {
    stdout += chunk;
    if (stdout.length > options.maxBuffer) {
      err = new Error('stdout maxBuffer exceeded.');
      kill();
    }
  });

  child.stderr.addListener('data', function(chunk) {
    stderr += chunk;
    if (stderr.length > options.maxBuffer) {
      err = new Error('stderr maxBuffer exceeded.');
      kill();
    }
  });

  child.addListener('close', exithandler);
  child.addListener('error', errorhandler);

  return child;
};

5-8.使用。fork 用于进程间通信

问题

您需要在 Node.js 中创建一个子流程,但是您还需要能够在这些子流程之间轻松地进行通信。

解决办法

使用 fork 方法在进程间通信的解决方案非常简单。您将构建一个创建 HTTP 服务器的主进程。该流程还将派生一个子流程,并将服务器对象传递给该子流程。即使服务器不是在子进程上创建的,子进程也能够处理来自该服务器的请求。服务器对象和所有消息都通过。send()法。

清单 5-23 。父进程

/**
* .fork main
*/

var cp = require('child_process');
                http = require('http');

var child = cp.fork('5-8-2.js');

var server = http.createServer(function(req, res) {
        res.end('hello');
}).listen(8080);

child.send('hello');
child.send('server', server);

清单 5-24 。分叉过程

/**
* forked process
*/

process.on('message', function(msg, hndl) {
        console.log(msg);

        if (msg === 'server') {
                hndl.on('connection', function() {
                console.log('connected on the child');
        });
        }

});

它是如何工作的

正如你在第 5-5 节看到的,创建一个分叉的进程和创建一个衍生的进程几乎是一样的。主要区别是通过child.send方法实现的跨进程通信。

这个send事件发送一个消息字符串和一个可选的句柄。手柄可以是五种类型之一:net.Socket, net.Server, net.Native, dgram.Socket, or dgram.Native。乍一看,要适应这些不同类型的方法可能令人望而生畏。幸运的是,Node.js 会为您转换句柄类型。这种处理也适用于衍生进程的响应。

消息发送到子流程时发生的事件是'message'事件。在这个解决方案中,您看到'message'事件包含消息的命名类型。首先,您发送了一条问候消息。接下来,您发送了一个服务器对象。一旦事件被确定为服务器,这个对象就被绑定到“connection”事件。然后,您可以像在单个流程模块中一样处理连接。

摘要

在本章中,您研究并实现了 Node.js 固有的两个重要模块的解决方案:事件和子流程。

在 events 模块中,您首先创建了一个自定义事件,然后解决了如何使用侦听器绑定到该事件。之后,您研究了当您只需要一个绑定时添加一次性事件侦听器的特殊情况。最后,您可以看到如何利用 Node.js 事件模块,通过使用事件来驱动功能,可以非常明显地减少回调的数量。

在本章的第二部分,您检查了子流程模块。您首先看到了如何生成一个子进程来运行主进程之外的命令。然后您看到了如何通过使用execexecFile方法直接运行 shell 命令和文件。这些都是从 spawn 进程派生出来的,正如.fork()进程一样,spawn 是 spawn 的一个特例,它允许简单的进程间通信,为多进程 Node.js 应用提供了无限的可能性。