前言:
因为以前学习Node.js并没有真正意义上的去学习它,而是粗略的学习了npm的常用命令和Node.js一些模块化的语法,因此昨天花了一天的时间看了《Node.js开发指南》一书。通过这本书倒是让我对Node.js的认识更为全面,但由于这本书出版时间过早,有些API已经发生了变化或已经被废弃,而对于学习Node.js来说,核心部分又是最为重要的一环,因此我配合官方文档对这本书的第四章-Node.js核心进行了总结与梳理,由于水平有限,如有疏漏与错误,请指正。
正文
核心模块是Node.js的心脏,主要是有一些精简高效的库组成(这方面和Python有很大的相似之处),为Node.js提供了基础的API。主要内容包括:
Node.js核心入门(一)
全局对象
常用工具
事件机制
Node.js核心入门(二)
文件系统访问
HTTP服务器与客户端
全局对象
全局对象我想学过JavaScript的都知道在浏览器是window,在程序的任何地方都可以访问到全局对象,而在Node.js中,这个全局对象换成了global,所有的全局变量(除了global本身)都是global对象的属性。而我们在Node.js中能够直接访问的对象通常都是global的属性,如:console,process等。
全局对象与全局变量
global最根本的作用就是作为全局变量的宿主。按照ECMAScript规范,满足以下条件的变量即为全局变量:
- 在最外层定义的变量(在Node.js中不存在,因为Node.js的代码在模块中执行,不存在在最外层定义变量)
- 全局对象的属性
- 隐式定义的变量(即未定义而直接进行赋值的变量)
当我们定义一个全局变量的时候,这个全局变量会自动成为全局变量的属性。
process
process 对象是一个全局变量,它提供当前 Node.js 进程的相关信息,以及控制当前 Node.js 进程。通常我们在写本地命令行程序的时候,少不了和它打交道。下面是它的最常用的成员方法:
1.process.argv
process.argv 属性返回一个数组,这个数组包含了启动Node.js进程时的命令行参数。第一个元素为process.execPath,第二个元素为当前执行的JavaScript文件路径,剩余的元素为其他命令行参数。
例如存储一个名为argv.js的文件:
// print process.argv
process.argv.forEach((val, index) => {
console.log(`${index}: ${val}`);
});
则命令行运行时这样的:
$ node process-args.js one two=three four
0: /usr/local/bin/node
1: /Users/mjr/work/node/process-args.js
2: one
3: two=three
4: four
2.process.stdout
process.stdout是标准输出流,通常我们使用的console.log()输出打印字符,而process.stdout.write()函数提供了更为底层的接口。
process.stdout.write('请输入num1的值:');
3.process.stdin
process.stdin是标准输入流,初始时它是暂停的,要想从标准输入读取数据,我们必须恢复流,并手动编写流的事件响应函数。
/*1:声明变量*/
var num1, num2;
/*2:向屏幕输出,提示信息,要求输入num1*/
process.stdout.write('请输入num1的值:');
/*3:监听用户的输入*/
process.stdin.on('data', function (chunk) {
if (!num1) {
num1 = Number(chunk);
/*4:向屏幕输出,提示信息,要求输入num2*/
process.stdout.write('请输入num2的值');
} else {
num2 = Number(chunk);
process.stdout.write('结果是:' + (num1 + num2));
}
});
4.process.nextTick(callback[, ...args])
...args
process.nextTick()方法将 callback 添加到"next tick 队列"。 一旦当前事件轮询队列的任务全部完成,在next tick队列中的所有callbacks会被依次调用。
这种方式不是setTimeout(fn, 0)的别名。它更加有效率,因此别用setTimeout去代替process.nextTick。事件轮询随后的ticks 调用,会在任何I/O事件(包括定时器)之前运行。
console.log('start');
process.nextTick(() => {
console.log('nextTick callback');
});
console.log('scheduled');
// start
// scheduled
// nextTick callback
4.console
console 模块提供了一个简单的调试控制台,类似于 Web 浏览器提供的 JavaScript 控制台。该模块导出了两个特定的组件:
- 一个 Console 类,包含 console.log() 、 console.error() 和 console.warn() 等方法,可以被用于写入到任何 Node.js 流。
- 一个全局的 console 实例,可被用于写入到 process.stdout 和 process.stderr。 全局的 console 使用时无需调用 require('console')。(注意:全局的 console 对象的方法既不总是同步的(如浏览器中类似的 API),也不总是异步的(如其他 Node.js 流)。
比如全局下的常见的console实例:
console.log('hello,world');
// hello,world
console.log('hello%s', 'world');
// helloworld
console.error(new Error('错误信息'));
// Error: 错误信息
const name = '描述';
console.warn(`警告${name}`);
// 警告描述
console.trace() // 向标准错误流输出当前的调用栈
常见的Console类:
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello,world');
// hello,world
myConsole.log('hello%s', 'world');
// helloworld
myConsole.error(new Error('错误信息'));
// Error: 错误信息
const name = '描述';
myConsole.warn(`警告${name}`);
//警告描述
常用工具 util
util 模块主要用于支持 Node.js 内部 API 的需求。 大部分实用工具也可用于应用程序与模块开发者,用于弥补核心JavaScript的功能的不足。它的可以这样调用:
const util = require('util');
1.util.inspect(object[, options])
util.inspect() 方法返回 object 的字符串表示,主要用于调试和错误输出。 附加的 options 可用于改变格式化字符串的某些方面。它至少接受一个参数objet,即要转换的参数,而option则是可选的,可选内容如下:
- showHidden
如果为 true,则 object 的不可枚举的符号与属性也会被包括在格式化后的结果中。 默认为 false。 - depth
指定格式化 object 时递归的次数。 这对查看大型复杂对象很有用。 默认为 2。 若要无限地递归则传入 null。 - colors
如果为 true,则输出样式使用 ANSI 颜色代码。 默认为 false,可自定义。 - customInspect
如果为 false,则 object 上自定义的 inspect(depth, opts) 函数不会被调用。 默认为 true。 - showProxy
如果为 true,则 Proxy 对象的对象和函数会展示它们的 target 和 handler 对象。 默认为 false。 - maxArrayLength
指定格式化时数组和 TypedArray 元素能包含的最大数量。 默认为 100。 设为 null 则显式全部数组元素。 设为 0 或负数则不显式数组元素。 - breakLength
一个对象的键被拆分成多行的长度。 设为 Infinity 则格式化一个对象为单行。 默认为 60。
例如,查看 util 对象的所有属性:
const util = require('util');
console.log(util.inspect(util, { showHidden: true, depth: null }));
2.util.callbackify(original)
util.callbackify(original)方法将 async 异步函数(或者一个返回值为 Promise 的函数)转换成遵循 Node.js 回调风格的函数。 在回调函数中, 第一个参数 err 为 Promise rejected 的原因 (如果 Promise 状态为 resolved , err为 null ),第二个参数则是 Promise 状态为 resolved 时的返回值。例如:
const util = require('util');
async function fn() {
return await Promise.resolve('hello world');
}
const callbackFunction = util.callbackify(fn);
callbackFunction((err, ret) => {
if (err) throw err;
console.log(ret);
});
// hello world
注意:
- 回调函数是异步执行的, 并且有异常堆栈错误追踪. 如果回调函数抛出一个异常, 进程会触发一个 'uncaughtException' 异常, 如果没有被捕获, 进程将会退出。
- null 在回调函数中作为一个参数有其特殊的意义, 如果回调函数的首个参数为 Promise rejected 的原因且带有返回值, 且值可以转换成布尔值 false, 这个值会被封装在 Error 对象里, 可以通过属性 reason 获取。
function fn() {
return Promise.reject(null);
}
const callbackFunction = util.callbackify(fn);
callbackFunction((err, ret) => {
// 当Promise的rejecte是null时,它的Error与原始值都会被存储在'reason'中。
err && err.hasOwnProperty('reason') && err.reason === null; // true
});
事件驱动 events
events是Node.js最重要的模块,原因是Node.js本身架构就是事件式的,大多数 Node.js 核心 API 都采用惯用的异步事件驱动架构,而它提供了唯一的接口,因此堪称Node.js事件编程的及时。events 模块不仅用于用户代码与 Node.js 下层事件循环的交互,还几乎被所有的模块依赖。
所有能触发事件的对象都是 EventEmitter 类的实例。 这些对象开放了一个 eventEmitter.on() 函数,允许将一个或多个函数绑定到会被对象触发的命名事件上。 事件名称通常是驼峰式的字符串,但也可以使用任何有效的 JavaScript 属性名。对于每个事件, EventEmitter支持若干个事件监听器。当事件发射时,注册到这个事件的事件监听器被依次调用,事件参数作
为回调函数参数传递。
例如:
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
// eventEmitter.on() 方法用于注册监听器
myEmitter.on('event', () => {
console.log('触发了一个事件!');
});
// eventEmitter.emit() 方法用于触发事件
myEmitter.emit('event');
下面让我们来看看EventEmitter最常用的API:
-EventEmitter.on(event, listener) 方法可以添加 listener 函数到名为 eventName 的事件的监听器数组的末尾。不会检查 listener 是否已被添加。多次调用并传入相同的 eventName 和 listener 会导致 listener 被添加与调用多次。
-emitter.prependListener(eventName, listener)方法可以添加 listener 函数到名为 eventName 的事件的监听器数组的开头。 不会检查 listener 是否已被添加。 多次调用并传入相同的 eventName 和 listener 会导致 listener 被添加与调用多次。
-eventEmitter.emit() 方法允许将任意参数传给监听器函数。当一个普通的监听器函数被 EventEmitter 调用时,标准的 this 关键词会被设置指向监听器所附加的 EventEmitter。
// 实例:
const myEE = new EventEmitter();
myEE.on('foo', () => console.log('a'));
myEE.prependListener('foo', () => console.log('b'));
myEE.emit('foo');
// 打印:
// b
// a
- EventEmitter.once(event, listener) 为指定事件注册一个单次监听器,即监听器最多只会触发一次,触发后立刻解除该监听器。
server.once('connection', (stream) => {
console.log('首次调用!');
});
- EventEmitter.removeListener(event, listener) 移除指定事件的某个监听器, listener 必须是该事件已经注册过的监听器。(注意:removeListener 最多只会从监听器数组里移除一个监听器实例。 如果任何单一的监听器被多次添加到指定 eventName 的监听器数组中,则必须多次调用 removeListener 才能移除每个实例。)
const callback = (stream) => {
console.log('有连接!');
};
server.on('connection', callback);
// ...
server.removeListener('connection', callback);
- EventEmitter.removeAllListeners([event]) 移除所有事件的所有监听器,如果指定 event ,则移除指定事件的所有监听器。
const callback = (stream) => {
console.log('有连接!');
};
server.on('connection', callback);
// ...
server.removeListener('connection', callback);
error 事件
EventEmitter 定义了一个特殊的事件 error ,它包含了“错误”的语义,我们在遇到异常的时候通常会发射 error 事件。当 error被发射时,EventEmitter规定如果没有响
应的监听器,Node.js 会把它当作异常,退出程序并打印调用栈。我们一般要为会发射 error 事件的对象设置监听器,避免遇到错误后整个程序崩溃。
var events = require('events');
var emitter = new events.EventEmitter();
emitter.emit('error');
继承EventEmitter
大多数情况下,我们不会直接使用EventEmitter,而是在对象中继承它,包括fs,http在内的只要是支持事件响应的核心模块都是EventEmitter的子类。这样做的原因有以下两个:
- 具有某个实体功能的对象实现事件符合语义,事件的监听和发射应该是一个对象的方法。
- JavaScript 的对象机制是基于原型的,支持部分多重继承,继承 EventEmitter 不会打乱对象原有的继承关系。