持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情
本文立足于代码,通过实现自定义事件、使用流读写数据以及导入导出模块,抛砖引玉,希望能对大家理解事件驱动结构、流和模块这几个Node.js
中的核心概念有所帮助
事件驱动架构
是什么
Node.js
中的许多模块,如http
模块,fs
模块等,都是基于事件驱动架构所创建的。类似于JavaScript
中的观察者模式
有什么用
- 模块之间更加独立
- 可以对同一个事件多次响应
代码说明
-
自定义事件
const EventEmitter = require("events"); // events模块 // 继承 class Sales extends EventEmitter { constructor() { super(); } } // 创建实例 const myEmitter = new Sales(); // listener1 myEmitter.on("newSale", () => { console.log("There was a new sale"); }); // listener2 myEmitter.on("newSale", (store) => { console.log(`There are now ${store} items`); }); // emitter myEmitter.emit("newSale", 10); 复制代码
-
使用内置的
http
模块const http = require("http"); // 创建实例 const server = http.createServer(); // listener1 server.on("request", (req, res) => { console.log("Request received!"); // 显示在控制台 res.end("Request received!"); // 显示在浏览器 }); // listener2 server.on("request", (req, res) => { console.log("Another request received!"); }); // emitter(启动服务器) server.listen(8000, "127.0.0.1", () => { console.log("Waiting for request..."); }); 复制代码
需要注意的地方
- 设置多个事件监听器时,其执行顺序的规则与同步代码一样
- 编写自定义事件时,注意
emitter
和listener
的顺序
流
是什么
官网的描述是:
A stream is an abstract interface for working with streaming data in Node.js.
也就是说,流是用于处理流式数据的抽象接口???好家伙,用自己解释自己!!!
那就不管了,先把概念封装起来,直接看API
-
Node.js
中的流有四种流 作用 案例 重要事件 重要函数 ⭐可读流 读取数据 http
请求,fs
读取文件data
,end
pipe()
,read()
⭐可写流 写入数据 http
响应,fs
写入文件drain
,finish
write()
,end()
双工流 即可读又可写 net
模块的web socket
/ / 转换流 在读写的时候转换数据 zlib
模块的Gzip creation
/ / Node.js
中的Stream
本质上是EventEmitter
的实例,所以也可以发射和监听事件
有什么用
- 用于读取或修改数据/文件片段,从而不需要在内存中保存全部资源
- 适合用于处理海量的数据,比如流媒体
代码说明
首先来设想一个需求:从磁盘读取文件,然后发给客户端
so easy,直接调用fs
模块和http
模块,一顿操作:
// 从磁盘读取text文件,发给客户端
const fs = require("fs");
const server = require("http").createServer();
server.on("request", (req, res) => {
fs.readFile("test-file.txt", (err, data) => {
if (err) console.log(err);
res.end(data);
});
});
server.listen(8000, "127.0.0.1", () => {
console.log("Listening request...");
});
复制代码
所读取的test-file.txt
文件的内容是:100万行“Node.js is the best!”
启动Node
程序,并在浏览器输入127.0.0.1:8000
,如期看到文本内容:
看到这里,相信大家也发现问题了:
- 使用上面的代码读取的是全部的文本内容,但有时候我们只需要获取部分数据就能满足需求
- 使用
fs.readFile()
读取文件时,本质上是先将文件的内容存在变量data
中,等全部读取完再发送给客户端,这就给Node
应用带来了内存压力,当文件很大时程序就崩溃了
👉使用流可以解决以上问题,代码如下:
const fs = require("fs");
const server = require("http").createServer();
server.on("request", (requ, res) => {
// 创建可读流实例
const readable = fs.createReadStream("test-file.txt");
// 读取文件时
readable.on("data", (chunk) => {
res.write(chunk); // 使用可写流的write()方法, chunk为每次传输的数据
});
// 文件读取完毕
readable.on("end", () => {
res.end();
});
// 读取出错
readable.on("error", (err) => {
console.log(err);
res.statusCode(500);
res.end("File not found!");
});
});
server.listen(8000, "127.0.0.1", () => {
console.log("Listening request...");
});
复制代码
使用流读取数据的思路是:readable
是一个可读流实例,在readable.on()
中监听data
事件,当不断读取数据时,data
事件就会一直被触发,此时将读取的数据,不借助变量直接写入res
中,文件全部读取完后便会触发end
事件。
所以可以简单地将流
理解为分段传输数据
但上面的思路其实存在一个由读写的速度带来漏洞,如果可读流从磁盘读入数据的速度,远远大于写入res
的速度,即接收到的这一波数据还来不及发送出去,下一波就来了,这就会造成内存溢出,也就是背压
👉可读流的pipe()
方法可以解决这个问题,它可以自动处理数据的读写速度:
const fs = require("fs");
const server = require("http").createServer();
server.on("request", (requ, res) => {
const readable = fs.createReadStream("test-file.txt");
readable.pipe(res); // 优雅永不过时
});
server.listen(8000, "127.0.0.1", () => {
console.log("Listening request...");
});
复制代码
模块
是什么
Node.js
中,每个.js
文件都是一个模块Node.js
模块的导入导出使用的是CommonJS
规范require()
:引入模块exports
:导出多个变量;只能为模块添加属性module.exports
:导出单一变量,比如类或函数;可以为模块添加属性或赋值到新对象
require('module')
的过程
graph LR
加载模块 --> 封装模块--> 执行模块--> 返回--> 缓存
-
加载:
require
加载模块的优先级:- 核心模块:
require('http')
- 自定义模块:
require('./utils')
- 第三方模块(
npm
安装):require('express')
完整的过程是:
- 首先按照名称查找有无对应的核心模块
- 如果以
./
或../
开头,就按照路径查找自定义的模块;没找到就去找同名文件夹下的index.js
文件 - 如果按照名称没有找到核心模块,就去
node_modules
目录下找;所以出现重名,就会优先加载核心模块
- 核心模块:
-
封装:
Node.js
封装模块时使用的函数封装器:(function(exports, require, module, __filename, __dirname) { // 模块的代码实际在这里 }); // require:加载模块 // exports:从模块中导出对象 // module:当前模块文件 // __filename:当前模块文件的绝对路径 // __dirname:当前模块文件据所在目录的绝对路径 复制代码
事实上,在任何一个
.js
文件中,输入console.log(arguments)
就可以看到当前模块的上述参数值;此外,Node.js
中有个核心模块就叫module
,我们同样可以使用它来查看上面的函数封装器// index.js console.log(arguments); console.log('------------------------') console.log(require("module").wrapper); 复制代码
控制台的输出结果如下:
[Arguments] { '0': {}, // require '1': // exports { [Function: require] resolve: { [Function: resolve] paths: [Function: paths] }, main: Module { id: '.', exports: {}, parent: null, filename: 'E:\\index.js', loaded: false, children: [], paths: [Array] }, extensions: [Object: null prototype] { '.js': [Function], '.json': [Function], '.node': [Function] }, cache: [Object: null prototype] { 'E:\\index.js': [Module] } }, '2': // module Module { id: '.', exports: {}, parent: null, filename: 'E:\\index.js', loaded: false, children: [], paths: [ 'E:\\node_modules' ] }, '3': 'E:\\index.js', // __filename '4': 'E:\\' } // __dirname ------------------------ [ '(function (exports, require, module, __filename, __dirname) { ', '\n});' ] 复制代码
-
⭐ 返回:
require
返回的是模块的导出,这些导出存储在module.exports
这个对象中,而exports
只是指向mudule.exports
代码说明
在module.js
中
-
使用
module.exports
导入test-module-1.js
// module.js // 使用module.exports const C = require("./test-module-1"); const calc1 = new C(); console.log(calc1.add(3, 4)); // 使用exports const { add, print } = require("./test-module-2"); // 导出的是对象(函数封装器中的exports),所以可以用解构赋值 console.log(add(5, 5)); print(); 复制代码
// test-module-1.js module.exports = class { add(a, b) { return a + b; } multiply(a, b) { return a * b; } divide(a, b) { return a / b; } }; 复制代码
-
使用
exports
导入test-module-2.js
// test-module-2.js exports.add = (a, b) => a + b; exports.multiply = (a, b) => a * b; exports.divide = (a, b) => a / b; module.exports.print = () => { console.log("--------"); }; 复制代码
-
下面做些有意思的事情
在
test-module-2.js
添加一行代码,然后直接运行node test-module-2.js
// test-module-2.js exports.add = (a, b) => a + b; exports.multiply = (a, b) => a * b; exports.divide = (a, b) => a / b; module.exports.print = () => { console.log("--------"); }; console.log(arguments); 复制代码
这里截取控制台输出的一小段
[Arguments] { '0': // require { add: [Function], multiply: [Function], divide: [Function], print: [Function] }, ...... '2': Module { id: 'E:\\Resourses\\Node\\complete-node-bootcamp\\2-how-node-works\\starter\\test-module-2.js', exports: // module.exports { add: [Function], multiply: [Function], divide: [Function], print: [Function] }, parent: ...... } 复制代码
我们已经知道这一项表示的是
require
,下面在module.js
中也增加一行代码console.log(require('test-module-2.js'))
,对应的输出结果如下:{ add: [Function], multiply: [Function], divide: [Function], print: [Function] } 复制代码
现在就比较清晰了,也验证了我们在上一节中的描述,即
require
返回的其实就是Node.js
内部module.exports
这个对象
总结
http
模块,fs
模块等,都是基于事件驱动架构所创建的- 使用可读流读取数据可以不经过变量,直接将读取到的数据发送给客户端,减小了
Node.js
的内存压力 - 可读流的
pipe()
方法可以解决背压问题:可读流实例.pipe(可写流实例)
exports
用于导出多个变量,但只能为模块添加属性,不能赋值到新对象;module.exports
用于导出单一变量,比如类或函数;也可以为模块添加属性或赋值到新对象;名称相同时,优先使用module.exports
参考资料
深入Node.js的模块加载机制,手写require函数 - 掘金 (juejin.cn)
详谈CommonJS模块化 - 掘金 (juejin.cn)