跳至主要内容

Node.js从零开始——事件、系统和流

毕竟不是一个真正的教程,这里主要还是以普及和介绍为主,所以这一部分就是 Node.js 的其他部分介绍了,主要也就是事件触发、操作系统以及流的知识。


1 事件触发器

因为我们之前在浏览器中使用 JavaScript,所以知道 JS 通过事件处理了许多用户的交互:鼠标的单击、键盘按钮的按下、对鼠标移动的反应等等。

在后端,Node.js 也提供了使用 events 模块 构建类似系统的选项。

具体上,此模块提供了 EventEmitter 类,用于处理事件。

使用以下代码进行初始化:

const EventEmitter = require('events');

const eventEmitter = new EventEmitter();

该对象公开了 onemit 方法:

  • emit 用于触发事件
  • on 用于添加回调函数(会在事件被触发时执行)

例如,创建 start 事件,并提供一个示例,通过记录到控制台进行交互:

eventEmitter.on('start', () => {
  console.log('开始');
});

当运行以下代码时:

eventEmitter.emit('start');

事件处理函数会被触发,且获得控制台日志。

可以通过将参数作为额外参数传给 emit() 来将参数传给事件处理程序:

eventEmitter.on('start', number => {
  console.log(`开始 ${number}`);
});
eventEmitter.emit('start', 23);

多个参数:

eventEmitter.on('start', (start, end) => {
  console.log(`从 ${start}${end}`);
});
eventEmitter.emit('start', 1, 100);

EventEmitter 对象还公开了其他几个与事件进行交互的方法,例如:

  • once():添加单次监听器
  • removeListener() / off():从事件中移除事件监听器
  • removeAllListeners():移除事件的所有监听器

下面详细的介绍一下事件模块:

2 事件模块

events 模块为提供了 EventEmitter 类,这是在 Node.js 中处理事件的关键,如上文简单的引入 events 即可使用,这里是另一个例子:

const EventEmitter = require('events');
const door = new EventEmitter();

事件监听器返回及使用以下事件:

  • 当监听器被添加时返回 newListener
  • 当监听器被移除时返回 removeListener

以下是最常用的方法的详细说明:

2.1 emitter.addListener()

emitter.on() 的别名,这是为了和 DOM API 保持一定的一致([DOM].addEventListener())。

2.2 emitter.emit()

触发事件, 按照事件被注册的顺序同步地调用每个事件监听器:

door.emit("slam"); // 触发 "slam" 事件。

2.3 emitter.eventNames()

返回字符串(表示在当前 EventEmitter 对象上注册的事件)数组:

door.eventNames();

2.4 emitter.getMaxListeners()

获取可以添加到 EventEmitter 对象的监听器的最大数量(默认为 10,但可以使用 setMaxListeners() 进行增加或减少)。

door.getMaxListeners();

2.5 emitter.listenerCount()

获取作为参数传入的事件监听器的计数:

door.listenerCount('open');

2.6 emitter.listeners()

获取作为参数传入的事件监听器的数组:

door.listeners('open');

2.7 emitter.off()

emitter.removeListener() 的别名,新增于 Node.js 10;这是为了和 emitter.on() 形成对应,简化记忆。

2.8 emitter.on()

添加当事件被触发时调用的回调函数,用法:

door.on('open', () => { 
  console.log('打开');
});

2.9 emitter.once()

添加当事件在注册之后首次被触发时调用的回调函数, 该回调只会被调用一次,不会再被调用:

const EventEmitter = require('events');
const ee = new EventEmitter();

ee.once('my-event', () => {
  //只调用一次回调函数
});

2.10 emitter.prependListener()

当使用 onaddListener 添加监听器时,监听器会被添加到监听器队列中的最后一个,并且最后一个被调用; 使用 prependListener 则可以在其他监听器之前添加并调用。

2.11 emitter.prependOnceListener()

当使用 once 添加监听器时,监听器会被添加到监听器队列中的最后一个,并且最后一个被调用; 使用 prependOnceListener 则可以在其他监听器之前添加并调用。

2.12 emitter.removeAllListeners()

移除 EventEmitter 对象的所有监听特定事件的监听器:

door.removeAllListeners('open');

2.13 emitter.removeListener()

移除特定的监听器,可以通过将回调函数保存到变量中(当添加时),以便以后可以引用它:

const doSomething = () => {};

door.on('open', doSomething);
door.removeListener('open', doSomething);

2.14 emitter.setMaxListeners()

设置可以添加到 EventEmitter 对象的监听器的最大数量(默认为 10,但可以增加或减少):

door.setMaxListeners(50);

3 操作系统模块

为了能够真正的和操作系统交互,Node.js 同样提供了操作系统模块;该模块提供了许多函数,可用于从底层的操作系统和程序运行所在的计算机上检索信息并与其进行交互,引用方法保持一致:

const os = require('os');

有一些有用的属性可以告诉我们一些与处理文件有关的关键事项:

  • os.EOL 可给出行定界符序列:在 LinuxmacOS 上为 \n,在 Windows 上为 \r\n
  • os.constants.signals 可告知所有与处理过程信号相关的常量,例如 SIGHUPSIGKILL
  • os.constants.errno 可设置用于错误报告的常量,例如 EADDRINUSEEOVERFLOW
可以在 nodejs.cn/api/os.html# 上阅读所有的内容。

现在看一下 os 提供的主要方法:

3.1 os.arch()

返回标识底层架构的字符串,例如 armx64arm64

3.2 os.cpus()

返回关于系统上可用的 CPU 的信息。

例如:

3.3 os.endianness()

根据是使用大端序或小端序编译 Node.js,返回 BELE

3.4 os.freemem()

返回代表系统中可用内存的字节数:

3.5 os.homedir()

返回到当前用户的主目录的路径,例如:

3.6 os.hostname()

返回主机名:

3.7 os.loadavg()

返回操作系统对平均负载的计算,这仅在 LinuxmacOS 上返回有意义的值;Windows 因为对系统负载的计算方法不同,返回值为 0

例如:

3.8 os.networkInterfaces()

返回系统上可用的网络接口的详细信息,例如(因为是我真实的电脑,所以我把 mac 地址遮蔽了,见谅):

3.9 os.platform()

返回为 Node.js 编译的平台:

  • darwin
  • freebsd
  • linux
  • openbsd
  • win32
  • ...等

3.10 os.release()

返回标识操作系统版本号的字符串:

3.11 os.tmpdir()

返回指定的临时文件夹的路径:

3.12 os.totalmem()

返回表示系统中可用的总内存的字节数:

3.13 os.type()

标识操作系统:

  • Linux
  • macOS 上为 Darwin
  • Windows 上为 Windows_NT(这是因为非 NT 架构的 Windows 系统现在占有率太低了,比如 Win 95、98、Me,所以默认如此显示)

3.14 os.uptime()

返回自上次重新启动以来计算机持续运行的秒数:

3.15 os.userInfo()

当前登录用户的信息:

4 流

流是为 Node.js 应用程序提供动力的基本概念之一,这是一种以高效的方式处理读/写文件、网络通信、或任何类型的端到端的信息交换。

流不是 Node.js 特有的概念,它是几十年前在 Unix 操作系统中引入的,程序可以通过管道运算符(|)对流进行相互交互。

例如,在传统的方式中,当告诉程序读取文件时,这会将文件从头到尾读入内存,然后进行处理。

使用流,则可以逐个片段地读取并处理(而无需全部保存在内存中)。

Node.jsstream 模块 提供了构建所有流 API 的基础,所有的流都是 EventEmitter 的实例(这样讲就容易明白了吧)。

4.1 为什么使用流

相对于使用其他的数据处理方法,流基本上提供了两个主要优点:

  • 内存效率:无需加载大量的数据到内存中即可进行处理
  • 时间效率:当获得数据之后即可立即开始处理数据,这样所需的时间更少,而不必等到整个数据有效负载可用才开始

4.2 流的示例

一个典型的例子是从磁盘读取文件。

使用 Node.jsfs 模块,可以读取文件,并在与 HTTP 服务器建立新连接时通过 HTTP 提供文件:

const http = require('http');
const fs = require('fs');

const server = http.createServer(function(req, res) {
  fs.readFile(__dirname + '/data.txt', (err, data) => {
    res.end(data);
  });
});
server.listen(3000);

readFile() 读取文件的全部内容,并在完成时调用回调函数;而回调中的 res.end(data) 会返回文件的内容给 HTTP 客户端。

如果文件很大,则该操作会花费较多的时间,这个时候我们可以使用流,如下例:

const http = require('http');
const fs = require('fs');

const server = http.createServer((req, res) => {
  const stream = fs.createReadStream(__dirname + '/data.txt');
  stream.pipe(res);
})
server.listen(3000);

当要发送的数据块已获得时就立即开始将其流式传输到 HTTP 客户端,而不是等待直到文件被完全读取。

4.3 pipe()

上面的示例使用了 stream.pipe(res) 这行代码:在文件流上调用 pipe() 方法。

该代码的作用是获取来源流,并将其通过管道传输到目标流,在该示例中,文件流通过管道传输到 HTTP 响应。

pipe() 方法的返回值是目标流,它可以链接多个 pipe() 调用,从而非常方便,如下所示:

src.pipe(dest1).pipe(dest2);

此构造相对于:

src.pipe(dest1);
dest1.pipe(dest2);

要更容易理解和编写。

4.4 流驱动的 Node.js API

由于它的优点,许多 Node.js 核心模块提供了原生的流处理功能,最值得注意的有:

  • process.stdin 返回连接到 stdin 的流
  • process.stdout 返回连接到 stdout 的流
  • process.stderr 返回连接到 stderr 的流
  • fs.createReadStream() 创建文件的可读流
  • fs.createWriteStream() 创建到文件的可写流
  • net.connect() 启动基于流的连接
  • http.request() 返回 http.ClientRequest 类的实例,该实例是可写流
  • zlib.createGzip() 使用 gzip(压缩算法)将数据压缩到流中
  • zlib.createGunzip() 解压缩 gzip
  • zlib.createDeflate() 使用 deflate(压缩算法)将数据压缩到流中
  • zlib.createInflate() 解压缩 deflate

4.5 不同类型的流

流分为四类:

  • Readable:可以通过管道读取、但不能通过管道写入的流(可以接收数据,但不能向其发送数据);当推送数据到可读流中时,会对其进行缓冲,直到使用者开始读取数据为止
  • Writable:可以通过管道写入、但不能通过管道读取的流(可以发送数据,但不能从中接收数据)
  • Duplex:可以通过管道写入和读取的流,基本上相对于是可读流和可写流的组合
  • Transform:类似于双工流、但其输出是其输入的转换的转换流

4.6 如何创建可读流

stream 模块获取可读流,对其进行初始化并实现 readable._read() 方法。

首先创建流对象:

const Stream = require('stream');
const readableStream = new Stream.Readable();

然后实现 _read

readableStream._read = () => {};

也可以使用 read 选项实现 _read

const readableStream = new Stream.Readable({
  read() {}
});

现在,流已初始化,可以向其发送数据了:

readableStream.push('hi!');
readableStream.push('ho!');

4.7 如何创建可写流

若要创建可写流,需要继承基本的 Writable 对象,并实现其 _write() 方法。

首先创建流对象:

const Stream = require('stream');
const writableStream = new Stream.Writable();

然后实现 _write

writableStream._write = (chunk, encoding, next) => {
  console.log(chunk.toString());
  next();
};

现在,可以通过以下方式传输可读流:

process.stdin.pipe(writableStream);

4.8 如何从可读流中获取数据

使用可写流:

const Stream = require('stream');

const readableStream = new Stream.Readable({
  read() {}
});
const writableStream = new Stream.Writable();

writableStream._write = (chunk, encoding, next) => {
  console.log(chunk.toString());
  next();
};

readableStream.pipe(writableStream);

readableStream.push('hi!');
readableStream.push('ho!');

也可以使用 readable 事件直接获取可读流数据:

readableStream.on('readable', () => {
  console.log(readableStream.read());
});

4.9 如何发送数据到可写流

使用流的 write() 方法:

writableStream.write('hey!\n');

4.10 使用信号通知已结束写入的可写流

使用 end() 方法:

const Stream = require('stream');

const readableStream = new Stream.Readable({
  read() {}
});
const writableStream = new Stream.Writable();

writableStream._write = (chunk, encoding, next) => {
  console.log(chunk.toString());
  next();
};

readableStream.pipe(writableStream);

readableStream.push('hi!');
readableStream.push('ho!');

writableStream.end();

眼看着基础的 Node.js 快要结束了,其实如果不算第三方的模块,它自带的东西的确就是这么多,我们的重点还是要有模块化开发的思路和方法论,或者大量的练习,毕竟熟能生巧么。



from 知乎专栏-全栈开发从零开始 https://ift.tt/2GIOit3
via IFTTT

评论

此博客中的热门博文

Node.js从零开始——HTTP 服务器

其实 Node.js 最初的目的,就是实现一个完全可以由 JavaScript 来进行开发的服务器端,所以归根到底,它的后端能力之一就是实现一个 HTTP 服务器,这里我们来看看它。 1 搭建 HTTP 服务器 其实前面我们已经看过了一个例子,不过这里再来看一个 HTTP web 服务器的例子: const http = require ( 'http' ); const port = 3000 ; const server = http . createServer (( req , res ) => { res . statusCode = 200 ; res . setHeader ( 'Content-Type' , 'text/plain' ); res . end ( '你好世界\n' ); }) server . listen ( port , () => { console . log ( `服务器运行在 http:// ${ hostname } : ${ port } /` ); }); 简要分析一下: 这里引入了 ref=" http:// nodejs.cn/api/http.html ">http 模块:使用该模块来创建 HTTP 服务器 服务器被设置为在指定的 3000 端口上进行监听, 当服务器就绪时,则 listen 回调函数会被调用 传入的回调函数会在每次接收到请求时被执行, 每当接收到新的请求时, "http://nodejs.cn/api/http.html#http_event_request">request 事件 会被调用,并提供两个对象:一个请求( http.IncomingMessage 对象)和一个响应( http.ServerResponse 对象) request 提供了请求的详细信息, 通过它可以访问请求头和请求的数据, response 用于构造要返回给客户端的数据;在此示例中: res . statusCode = 200 ; 设置 status

Web API从零开始——SVG

SVG 是我基本没有用过的知识块,所以这里也是边分享边学习,尽量在我自己理解的基础上来分享。 1 概念 SVG 是一种基于 XML 语法的图像格式,全称是可缩放矢量图(Scalable Vector Graphics);其他图像格式都是基于像素处理的, SVG 则是属于对图像的形状描述,所以它本质上是文本文件,体积较小,且不管放大多少倍都不会失真。 SVG 文件可以直接插入网页,成为 DOM 的一部分,然后用 JavaScript 和 CSS 进行操作。 上面是 SVG 代码直接插入网页的例子。 SVG 代码也可以写在一个独立文件中,然后用 、 、 、 等标签插入网页: < img src = "circle.svg" > < object id = "object" data = "circle.svg" type = "image/svg+xml" ></</span> object > < embed id = "embed" src = "icon.svg" type = "image/svg+xml" > < iframe id = "iframe" src = "icon.svg" ></</span> iframe > CSS 也可以使用 SVG 文件: . logo { background : url ( icon.svg ); } SVG 文件还可以转为 BASE64 编码,然后作为 Data URI 写入网页: < img src = "data:image/svg+xml;base64,[data]" > 2 语法 2.1 标签 我们可以把 SVG 代码都放在顶层标签 之中,下面是一个例子: < svg width = "100%" height = "100%" >