本文立足于代码,通过
实现自定义事件、使用流读写数据以及导入导出模块
,抛砖引玉,希望能对大家理解事件驱动结构、流和模块这几个
Node.js
中的核心概念有所帮助
事件驱动架构
是什么
有什么用
- 模块之间更加独立
- 可以对同一个事件多次响应
代码说明
-
自定义事件
const EventEmitter = require("events"); // events模块// 继承class Sales extends EventEmitter {constructor() {super();}}// 创建实例const myEmitter = new Sales();// listener1myEmitter.on("newSale", () => {console.log("There was a new sale");});// listener2myEmitter.on("newSale", (store) => {console.log(`There are now ${store} items`);});// emittermyEmitter.emit("newSale", 10);
* 使用内置的
http
模块
const http = require("http");// 创建实例const server = http.createServer();// listener1server.on("request", (req, res) => {console.log("Request received!"); // 显示在控制台res.end("Request received!"); // 显示在浏览器});// listener2server.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')
的过程
require('module')
graph LR
加载模块 --> 封装模块--> 执行模块--> 返回--> 缓存
-
加载
:
require
加载模块的优先级:1.核心模块:
require('http')
2.自定义模块:
require('./utils')
3.第三方模块(
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.jsconsole.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': // moduleModule {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.exportsconst C = require("./test-module-1");const calc1 = new C();console.log(calc1.add(3, 4));// 使用exportsconst { add, print } = require("./test-module-2"); // 导出的是对象(函数封装器中的exports),所以可以用解构赋值console.log(add(5, 5)); print(); ``````// test-module-1.jsmodule.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.jsexports.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.jsexports.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
最后
最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。
有需要的小伙伴,可以点击下方卡片领取,无偿分享