[😈万字长文警告👿]NodeJs基础☞🀁
简介
Node.js
是一个基于Chrome V8 引擎
的JavaScript
运行时环境。
在浏览器外运⾏ V8 JavaScript 引擎
(Google Chrome 的内核),利⽤事件驱动
、⾮阻塞I/O
和异步输⼊输出
模型等技术提⾼性能,可以理解为 Node.js 就是⼀个服务器端的、⾮阻塞式I/O
的、事件驱动
的 JavaScript 运⾏环境
以下是 Node.js 的一些主要特点和优势:
事件驱动
:Node.js 构建在事件驱动的基础上,使用了事件循环来处理并发请求,通过回调函数实现异步操作,从而提高了应用程序的性能和吞吐量。非阻塞 I/O
:Node.js 使用了非阻塞 I/O 模型,使得在执行 I/O 操作时不会阻塞后续代码的执行,从而实现了高效的并发处理。单线程
:虽然 Node.js 是单线程的,但通过事件循环和异步操作,可以处理大量并发请求,同时保持响应速度。跨平台
:Node.js 可以在多种操作系统上运行,包括 Windows、Linux、macOS 等,使得开发者能够轻松地在不同环境中部署应用程序。模块化
:Node.js 支持 CommonJS 模块规范,开发者可以通过 require 和 module.exports 来进行模块的导入和导出,从而实现代码的模块化和复用。丰富的生态系统
:Node.js 拥有丰富的第三方模块和工具,如 Express.js、Socket.IO、npm 等,使得开发者能够快速构建各种类型的应用程序,并且能够通过 npm 包管理器轻松地管理依赖项。轻量高效
:Node.js 本身的设计简单、轻量,启动快速,适合构建实时、高性能的网络应用程序。
Node.js 的主要应用场景包括 Web 服务器
、API 服务
、实时聊天应用
、物联网应用
、命令行工具
等。它广泛应用于企业级应用程序开发、微服务架构、实时数据处理等领域。随着 JavaScript 的普及和 Node.js 生态系统的不断完善,Node.js 已经成为一种强大的服务器端技术。
事件驱动
事件驱动
就是当进来⼀个新的请求的时,请求将会被压⼊⼀个事件队列中,然后通过⼀个循环来检测队列中的事件状态变化,如果检测到有状态变化的事件,那么就执⾏该事件对应的处理代码,⼀般都是回调函数
。
⽐如读取⼀个⽂件,⽂件读取完毕后,就会触发对应的状态,然后通过对应的回调函数来进⾏处理
Node.js 作为一个事件驱动的运行时环境,其事件驱动架构的优缺点具体如下:
优点
:
高并发性
: Node.js 的事件驱动模型使得它能够以非阻塞的方式处理大量并发请求,从而提高应用程序的吞吐量。快速响应
: Node.js 应用程序能够快速响应用户请求,因为事件处理不会阻塞主线程。适合 I/O 密集型应用
: Node.js 的事件驱动架构非常适合 I/O 密集型的应用程序,如 Web 服务器、实时聊天应用等。
缺点
:
CPU 密集型任务
: Node.js 的单线程事件循环模型不太适合处理 CPU 密集型的任务,因为它会阻塞主线程,降低应用程序的响应性。单线程限制
: Node.js 是单线程的,虽然通过事件循环实现了并发处理,但某些 CPU 密集型任务可能会导致性能瓶颈。可靠性低
: ⼀旦代码某个环节崩溃,整个系统都崩溃
总的来说,Node.js 的事件驱动架构非常适合 I/O 密集型的应用程序,但对于 CPU 密集型任务或需要复杂并发控制的应用程序,可能需要采用其他的编程模型或技术栈。
⾮阻塞异步
Nodejs
采⽤了⾮阻塞型 I/O
机制,在做 I/O 操作的时候不会造成任何的阻塞,当完成之后,以时间的形式通知执⾏操作。
在非阻塞式 I/O 中,当程序发起一个 I/O 操作(比如读取文件、发送网络请求等),它会立即返回并继续执行后续代码,而不会等待 I/O 操作完成。程序可以通过轮询或事件通知的方式来检查 I/O 操作是否已经完成,一旦完成,程序会得到通知并处理相应的结果。
Buffer 缓冲区
- Buffer 的
结构与数组类似
,操作方法也与数组类似
- 数组不能存储二进制文件,Buffer 是
专门存储二进制数据
的 - Buffer 存储的是二进制数据,显示时以 16 进制的形式显示
- Buffer 每一个元素范围是 00
ff,即 0255、00000000~11111111 - 每一个元素占用一个字节内存
- Buffer 是对
底层内存的直接操作
,因此大小一旦确定就不能修改
Buffer 类在全局作⽤域
中,⽆须 require 导⼊
。
常用方法
创建 Buffer 对象
`Buffer.from()` 方法用于创建一个新的 Buffer 实例,其数据源可以是多种类型,包括数组、Buffer、字符串等
const buf1 = Buffer.from('hello world');// 从字符串创建
const buf2 = Buffer.from(buf1);// 从Buffer创建
const buf3 = Buffer.from([0x68, 0x65, 0x6c, 0x6c, 0x6f]);// 从数组创建
创建指定大小的 Buffer
Buffer.alloc(size[, fill[, encoding]])
创建一个指定大小的新 Buffer 实例。如果提供了 fill
参数,则会用填充物填充 Buffer;如果提供了 encoding
参数,则会根据指定的编码对填充物进行编码
。
const buf = Buffer.alloc(10); // 创建⼀个⼤⼩为 10 个字节的缓冲区
buf.write('abcdefgh', 2, 4); // 从索引 2 开始写入 4 个字节
console.log(buf.toString()); // 输出: ab efgh
Buffer.allocUnsafe(size)
是 Node.js 中用于创建一个指定大小的新 Buffer 实例的方法。该方法不会初始化新 Buffer 实例的内容,这意味着新创建的 Buffer 实例可能包含敏感数据
,因此需要格外小心。
Buffer.allocUnsafe(size)
参数说明:
size
: 必需参数,表示新 Buffer 实例的大小(以字节为单位)。
示例:
const buf = Buffer.allocUnsafe(10);
console.log(buf);
// 输出: <Buffer 00 00 00 00 00 00 00 00 00 00>(内容不确定)
// 清空内容
buf.fill(0);
console.log(buf);
// 输出: <Buffer 00 00 00 00 00 00 00 00 00 00>
虽然 Buffer.allocUnsafe()
方法比 Buffer.alloc()
方法更快,但由于不会对内存进行清空或初始化
,因此在使用时需要格外小心确保不会泄露敏感数据
。
与 Buffer.alloc()
方法不同, Buffer.allocUnsafe()
方法不会初始化新 Buffer 实例的内容,而是直接分配内存空间。这意味着它的性能比 Buffer.alloc()
方法更好,但同时也意味着新创建的 Buffer 实例可能包含未初始化的数据,这可能会导致安全隐患。
因此,在大多数情况下,我们建议使用 Buffer.alloc()
方法来创建新的 Buffer 实例,以确保数据安全。只有在对性能要求较高的情况下,才考虑使用 Buffer.allocUnsafe()
方法。使用 Buffer.allocUnsafe()
方法时,务必要确保在使用新创建的 Buffer 实例之前,对其进行适当的初始化操作。
读取数据
:
const buf = Buffer.from('hello world');
console.log(buf.toString()); // 输出: hello world
console.log(buf.toString('utf8', 0, 5)); // 输出: hello
以上只是 Buffer 对象常用方法的一些示例,实际使用时可根据具体需求选择合适的方法。Buffer 对象提供了丰富的 API,可以帮助开发者高效地处理二进制数据。
应用场景
Buffer 在 Node.js 中有许多应用场景,主要包括以下几个方面:
文件 I/O
: 在读取和写入文件时,通常需要使用 Buffer 来处理二进制数据。例如,使用fs.readFile()
和fs.writeFile()
方法时,就需要使用 Buffer 来存储和传输数据。网络通信
: 在网络通信中,数据通常以二进制形式传输。使用 Buffer 可以方便地处理这些二进制数据,例如在 HTTP 请求和响应中使用 Buffer 来传输数据。数据编码和解码
: 在处理字符串时,通常需要将其编码为二进制数据,或者从二进制数据中解码出字符串。Buffer 提供了方便的 API,如toString()
和write()
方法,来实现这些功能。二进制数据操作
: Buffer 可用于对二进制数据进行各种操作,如切片、连接、比较等。这在一些需要处理二进制数据的场景中非常有用,例如图像处理、加密/解密等。数据压缩和解压缩
: 在处理大量数据时,数据压缩和解压缩是一个常见的需求。Buffer 可以用于存储和操作压缩后的二进制数据。
总的来说,Buffer 在 Node.js 中扮演着非常重要的角色,它为开发者提供了一种高效、灵活的方式来处理二进制数据。无论是在文件 I/O、网络通信、数据编解码还是其他涉及二进制数据的场景中,Buffer 都是一个非常有用的工具。
fs 文件系统模块
- fs 模块中所有的操作都有两种形式可供选择:
同步和异步
同步文件系统会阻塞程序的执行
,也就是除非操作完毕,否则不会向下执行代码,无回调函数
。异步文件系统不会阻塞程序的执行
,而是在操作完成时,通过回调函数将结果返回- 实际开发很少用同步方式,因此只介绍异步步方式
打开模式:
模式 | 说明 |
---|---|
r | 读取文件,文件不存在抛异常 |
r+ | 读写文件,文件不存在抛异常 |
rs | 同步模式下打开文件用于读取 |
rs+ | 同步模式下打开文件用于读写 |
w | 写文件,不存在则创建,存在则覆盖原有内容 |
wx | 写文件,文件存在打开失败 |
w+ | 读写文件,不存在创建,存在截断 |
wx+ | 读写,存在打开失败 |
a | 追加,不存在创建 |
ax | 追加,存在失败 |
a+ | 追加和读取,不存在创建 |
ax+ | 追加和读取,存在失败 |
常用方法
文件读取
-
fs.readFile(path[, options], callback)
: 异步读取文件内容。path
:要读取的文件的路径。options
:可选参数,可以是一个对象,用于指定读取文件的选项,例如编码方式等。callback
:回调函数,当读取操作完成时调用。回调函数有两个参数err
和data
,分别表示可能的错误信息和读取到的文件内容。
-
fs.readFileSync(path[, options])
: 同步读取文件内容。
const fs = require('fs');
const path = require('path');
// 异步读取文件
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error('读取文件时出错:', err);
return;
}
console.log('文件内容:', data);
});
// 拼接路径
const filePath = path.join(__diranem, 'example.txt');
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error('读取文件时出错:', err);
return;
}
console.log('文件内容:', data);
});
路径动态拼接问题 __dirname
- 在使用 fs 模块操作文件时,如果提供的操作路径是以
./
或../
开头的相对路径时,容易出现路径动态拼接错误
的问题 - 原因:代码在运行的时候,会以执行 node 命令时所处的目录,动态拼接出被操作文件的完整路径
- 解决方案:在使用 fs 模块操作文件时,直接提供完整的路径,从而防止路径动态拼接的问题
__dirname
获取文件所处的绝对路径
在Node.js中,__dirname
是一个全局变量,表示当前模块的目录名
。它通常用于构建文件路径,以确保路径的准确性和可移植性。例如,你可以将它与其他路径拼接来创建完整的文件路径,如下所示:
const path = require('path');
const filePath = path.join(__dirname, 'subfolder', 'filename.txt');
这将创建一个包含当前模块所在目录的路径,并在其后附加'subfolder'和'filename.txt',以构建完整的文件路径。
文件写入
-
fs.writeFile(file, data[, options], callback)
: 异步写入文件。file
:要写入的文件的路径。data
:要写入的数据。options
:可选参数,可以是一个对象,用于指定写入文件的选项,例如编码方式等。callback
:回调函数,当写入操作完成时调用。回调函数有一个参数err
,表示可能的错误信息。
-
fs.writeFileSync(file, data[, options])
: 同步写入文件。
const fs = require('fs');
const path = require('path');
// 要写入的文件名
const fileName = 'example.txt';
// 要写入的数据
const data = 'Hello, world!';
// 拼接文件路径
const filePath = path.join(__dirname, fileName);
// 将数据写入文件
fs.writeFile(filePath, data, (err) => {
if (err) {
console.error('写入文件时出错:', err);
} else {
console.log('文件写入成功:', filePath);
}
});
文件追加
fs.appendFile(path, data[, options], callback)
: 异步追加数据到文件。(参数与fs.writeFile相同)fs.appendFileSync(path, data[, options])
: 同步追加数据到文件。
const fs = require('fs');
const path = require('path');
// 要追加数据的文件名
const fileName = 'example.txt';
// 要追加的数据
const additionalData = '\nThis is additional data.';
// 拼接文件路径
const filePath = path.join(__dirname, fileName);
// 追加数据到文件
fs.appendFile(filePath, additionalData, (err) => {
if (err) {
console.error('追加数据到文件时出错:', err);
} else {
console.log('数据追加成功:', filePath);
}
});
需要注意的是:
- 如果文件不存在,
fs.appendFile()
会自动创建
该文件。 fs.appendFile()
会将数据追加到文件的末尾
,而不是覆盖原有内容
。- 如果出现错误,
fs.appendFile()
会在回调函数的第一个参数中返回错误对象,开发者需要在回调函数中进行适当的错误处理。
删除文件
-
fs.unlink(path, callback)
: 异步删除文件。path
:要删除的文件或符号链接的路径。callback
:回调函数,当删除操作完成时调用。回调函数有一个参数err
,表示可能的错误信息。
-
fs.unlinkSync(path)
: 同步删除文件。
const fs = require('fs');
const path = require('path');
// 要删除的文件名
const fileName = 'example.txt';
// 拼接文件路径
const filePath = path.join(__dirname, fileName);
// 删除文件
fs.unlink(filePath, (err) => {
if (err) {
console.error('删除文件时出错:', err);
} else {
console.log('文件删除成功:', filePath);
}
});
需要注意的是:
fs.unlink()
只能删除文件,不能删除目录。如果要删除目录,可以使用fs.rmdir()
方法。- 如果指定的文件不存在,
fs.unlink()
将会返回一个错误。 - 如果出现错误,
fs.unlink()
会在回调函数的第一个参数中返回错误对象,开发者需要在回调函数中进行适当的错误处理。
使用 fs.unlink()
方法可以方便地删除文件,在需要清理文件系统或管理文件生命周期的场景中非常有用。
创建目录
-
fs.mkdir(path[, options], callback)
: 异步创建目录。path
:要创建的目录的路径。options
:可选参数,可以是一个对象,用于指定创建目录的选项,例如权限等。callback
:回调函数,当创建操作完成时调用。回调函数有一个参数err
,表示可能的错误信息。
-
fs.mkdirSync(path[, options])
: 同步创建目录。
const fs = require('fs');
const path = require('path');
// 创建单个目录
fs.mkdir('new-directory', (err) => {
if (err) {
console.error('创建目录时出错:', err);
return;
}
console.log('目录创建成功!');
});
// 创建多级目录
fs.mkdir('parent/child/grandchild', { recursive: true }, (err) => {
if (err) {
console.error('创建目录时出错:', err);
return;
}
console.log('目录创建成功!');
});
// 使用路径拼接创建目录
const directoryPath = path.join(__dirname, 'new-directory');
fs.mkdir(directoryPath, (err) => {
if (err) {
console.error('创建目录时出错:', err);
return;
}
console.log('目录创建成功!');
});
需要注意的是:
- 如果指定的目录
已经存在
,fs.mkdir()
将会返回一个错误。 - 如果设置了
recursive
选项为true
,则fs.mkdir()
会创建任何必需的中间目录。这在创建多级目录时非常有用。 mode
选项用于指定新目录的权限模式。它的值可以是一个八进制整数或一个字符串。- 如果出现错误,
fs.mkdir()
会在回调函数的第一个参数中返回错误对象,开发者需要在回调函数中进行适当的错误处理。
使用 fs.mkdir()
方法可以方便地创建新的目录,特别是在需要创建多级目录的情况下。这在文件系统管理和组织中非常有用。
删除目录
-
fs.rmdir(path[, options], callback)
: 异步删除目录。path
:要删除的目录的路径。options
:可选参数,可以是一个对象,用于指定删除目录的选项,例如递归删除等。callback
:回调函数,当删除操作完成时调用。回调函数有一个参数err
,表示可能的错误信息。
-
fs.rmdirSync(path[, options])
: 同步删除目录。
const fs = require('fs');
// 删除单个目录
fs.rmdir('example_dir', (err) => {
if (err) {
console.error('删除目录时出错:', err);
return;
}
console.log('目录删除成功!');
});
// 递归删除目录及其所有内容
fs.rmdir('example_dir', { recursive: true }, (err) => {
if (err) {
console.error('删除目录时出错:', err);
return;
}
console.log('目录及其内容删除成功!');
});
需要注意的是:
fs.rmdir()
只能删除空目录
。如果目录中包含文件或其他子目录,删除操作将会失败,除非设置options.recursive
为true
。- 如果指定的目录不存在,
fs.rmdir()
将会返回一个错误。 - 如果出现错误,
fs.rmdir()
会在回调函数的第一个参数中返回错误对象,开发者需要在回调函数中进行适当的错误处理。 - 使用
options.recursive
时,如果目录包含许多文件和子目录,删除操作可能会比较慢,因此可以考虑使用options.maxRetries
和options.retryDelay
来优化删除过程。
使用 fs.rmdir()
方法可以方便地删除目录,在需要清理文件系统或管理文件生命周期的场景中非常有用。
重命名文件和目录
-
fs.rename(oldPath, newPath, callback)
oldPath
:原始文件或目录的路径。newPath
:重命名后的文件或目录的新路径。callback
:回调函数,当重命名操作完成时调用。回调函数有一个参数err
,表示可能的错误信息。
-
fs.renameSync(oldPath, newPath)
const fs = require('fs');
const path = require('path');
// 要重命名的文件或目录的旧路径
const oldPath = path.join(__dirname, 'old_name.txt');
// 新路径
const newPath = path.join(__dirname, 'new_name.txt');
// 重命名文件或目录
fs.rename(oldPath, newPath, (err) => {
if (err) {
console.error('重命名文件或目录时出错:', err);
} else {
console.log('文件或目录重命名成功:', newPath);
}
});
需要注意的是:
fs.rename()
可以用于重命名文件或目录
。- 如果
newPath
指定的文件或目录已经存在,则会被覆盖
。 - 如果指定的
oldPath
不存在,fs.rename()
将会返回一个错误。 - 如果出现错误,
fs.rename()
会在回调函数的第一个参数中返回错误对象,开发者需要在回调函数中进行适当的错误处理。
使用 fs.rename()
方法可以方便地重命名文件或目录,在需要管理文件系统或重构应用程序时很有用。需要注意的是,如果需要跨设备或文件系统重命名文件或目录,可能需要使用其他的 API,如 fs.copyFile()
和 fs.unlink()
。
其他
验证路径是否存在:
fs.exists(path, callback)
fs.existsSync(path)
获取文件信息:
fs.stat(path, callback)
fs.stat(path)
列出文件:
fs.readdir(path[,options], callback)
fs.readdirSync(path[, options])
截断文件:
fs.truncate(path, len, callback)
fs.truncateSync(path, len)
监视文件更改:
fs.watchFile(filename[, options], listener)
path 路径模块
path
模块是用于处理文件和目录路径
的工具。它提供了一些常用的方法和属性,可以帮助开发者更方便地处理路径相关的操作。
下面我们来介绍一下 path
模块的常用功能:
-
path.join(...) : 用于拼接多个路径片段,并正确处理不同操作系统下的路径分隔符。
const path = require('path') const fs = require('fs') // 注意 ../ 会抵消前面的路径 // ./ 会被忽略 const pathStr = path.join('/a', '/b/c', '../../', './d', 'e') console.log(pathStr) // \a\d\e fs.readFile(path.join(__dirname, './files/1.txt'), 'utf8', function (err, dataStr) { if (err) { return console.log(err.message) } console.log(dataStr) })
-
path.resolve(...) : 用于将相对路径转换为绝对路径。它会从右到左解析路径片段,直到构造出一个绝对路径。
const path = require('path'); const filePath = path.resolve(__dirname, 'files', 'example.txt'); // 输出: '/path/to/project/files/example.txt'
-
path.dirname(path) : 返回一个路径的目录名部分。
const path = require('path'); const dirPath = path.dirname('/path/to/project/files/example.txt'); // 输出: '/path/to/project/files'
-
path.basename(path[, ext]) : 返回一个路径的文件名部分。可以指定排除文件扩展名。
const path = require('path'); const fileName = path.basename('/path/to/project/files/example.txt'); // 输出: 'example.txt' const fileNameWithoutExt = path.basename('/path/to/project/files/example.txt', '.txt'); // 输出: 'example'
-
path.extname(path) : 返回一个路径的扩展名部分。
const path = require('path'); const fileExtension = path.extname('/path/to/project/files/example.txt'); // 输出: '.txt'
-
path.parse(path) : 将一个路径字符串解析为一个对象,包含
dir
、root
、base
、name
和ext
属性。const path = require('path'); const pathObj = path.parse('/path/to/project/files/example.txt'); // 输出: // { // root: '/', // dir: '/path/to/project/files', // base: 'example.txt', // ext: '.txt', // name: 'example' // }
-
path.format(pathObject) : 将一个路径对象转换为一个路径字符串。
const path = require('path'); const pathObj = { dir: '/path/to/project', base: 'example.txt' }; const formattedPath = path.format(pathObj); // 输出: '/path/to/project/example.txt'
这些是 path
模块中一些常用的方法和属性。它们可以帮助我们更方便地处理文件和目录路径,特别是在跨平台开发中非常有用。
http 模块
http 模块是 Node.js 官方提供的、用来
创建 web 服务器
的模块。
通过 http 模块提供的 http.createServer()
方法,就能方便的把一台普通的电脑,变成一台 Web 服务器,从而对外提供 Web 资源服务。
在 Node.js 中,不需要使用 IIS、Apache(针对php) 等第三方 web 服务器软件(普通的电脑常常安装这些),而是基于 Node.js 提供的 http 模块,通过几行简单的代码,就能轻松的手写一个服务器软件,从而对外提供 web 服务
在 Node.js 中创建一个基本的 Web 服务器
的步骤如下:
-
导入
http
模块: 首先,我们需要导入 Node.js 内置的http
模块,它提供了创建 HTTP 服务器和客户端的功能。const http = require('http');
-
创建 HTTP 服务器: 使用
http.createServer()
方法创建一个 HTTP 服务器。这个方法接受一个回调函数,该函数会在每次收到 HTTP 请求时被调用。const server = http.createServer();
-
为服务器实例绑定
request
事件,监听客户端的请求: 我们可以使用server.on()
方法为服务器实例绑定request
事件,在每次收到客户端请求时执行相应的处理逻辑。// 3. 为服务器实例绑定 request 事件,监听客户端的请求 server.on('request', function (req, res) { const url = req.url const method = req.method const str = `Your request url is ${url}, and request method is ${method}` console.log(str) // 设置 Content-Type 响应头,解决中文乱码的问题 res.setHeader('Content-Type', 'text/html; charset=utf-8') // 向客户端响应内容 res.end(str) })
回调函数接受两个参数:
req
: 代表 HTTP 请求的对象,包含了请求的信息,如 URL、方法、头部等。res
: 代表 HTTP 响应的对象,用于设置响应的状态码、头部和内容。如果想访问与服务器相关的数据或属性,通过res.end(data)
方法响应。
-
启动服务器: 最后,我们使用
server.listen()
方法启动服务器,让它开始监听指定的端口,等待客户端的连接。const port = 3000; server.listen(port, () => { console.log(`Server running at http://localhost:${port}/`); });
在这个例子中,我们设置了端口号为 3000,并在服务器启动时打印了一条日志消息。
创建一个基本的 HTTP 服务器:
const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
// 1. 获取请求的 url 地址
const url = req.url
// 2. 设置默认的响应内容为 404 Not found
let content = '<h1>404 Not found!</h1>'
// 3. 判断用户请求的是否为 / 或 /index.html 首页
// 4. 判断用户请求的是否为 /about.html 关于页面
if (url === '/' || url === '/index.html') {
content = '<h1>首页</h1>'
} else if (url === '/about.html') {
content = '<h1>关于页面</h1>'
}
// 5. 设置 Content-Type 响应头,防止中文乱码
res.setHeader('Content-Type', 'text/html; charset=utf-8')
// 6. 使用 res.end() 把内容响应给客户端
res.end(content)
})
server.listen(80, () => {
console.log('server running at http://127.0.0.1')
})
os 模块
os
模块提供了一些与操作系统相关的实用程序方法和属性。以下是一些常用的 os
模块的功能:
-
系统信息:
os.arch()
: 返回 Node.js 二进制文件的操作系统 CPU 架构。os.platform()
: 返回操作系统的平台。os.type()
: 返回操作系统名称。os.release()
: 返回操作系统的发行版本。
-
文件路径:
os.homedir()
: 返回用户的主目录。os.tmpdir()
: 返回系统默认的临时文件目录。os.EOL
: 返回当前操作系统的行末标志(换行符)。
-
CPU 信息:
os.cpus()
: 返回一个包含 CPU 信息的对象数组。os.loadavg()
: 返回系统的平均负载。
-
内存信息:
os.totalmem()
: 返回系统内存总量(单位为字节)。os.freemem()
: 返回操作系统空闲内存量(单位为字节)。
-
网络信息:
os.networkInterfaces()
: 返回操作系统网络接口的信息。
以下是一个简单的示例,演示如何使用 os
模块获取操作系统信息:
const os = require('os');
console.log('操作系统架构:', os.arch());
console.log('操作系统平台:', os.platform());
console.log('操作系统名称:', os.type());
console.log('操作系统版本:', os.release());
console.log('系统内存总量:', os.totalmem(), '字节');
console.log('系统可用内存:', os.freemem(), '字节');
console.log('CPU 信息:', os.cpus());
os
模块提供了丰富的操作系统相关信息,可以帮助我们更好地了解和管理运行 Node.js 应用程序的环境。
Stream流
流(Stream)
,是一种数据传输
手段,是端到端信息交换
的一种方式,是有顺序的,是逐块读取数据、处理内容,用于顺序读取输入或写入输出
在很多时候,流(Stream)是字节流(Byte Steram)的简称,也就是长长的一串字节
除了字节流(Byte Stream),流还可以是视频流、音频流、数据流等不同类型。流的类型取决于所传输的数据类型。
流的独特之处在于,它不像传统的程序那样一次将一个文件读入内存,而是逐块读取数据
、处理其内容,而不是将其全部保存在内存中
流可以分为三部分:
- 数据源(source) :产生数据的地方,可以是文件、网络连接、内存等。
- 数据目标(destination) :接收数据的地方,可以是文件、网络连接、内存等。
- 管道(pipe) :将数据从数据源流向数据目标的连接。
基本语法是 source.pipe(dest)
,它会将数据从源流向目标,实现数据的传输和处理。
-
流的优势:
内存效率高
:无需一次性加载整个数据到内存,可以分块读写。- 代码简洁:流提供了统一的接口,使得代码更加简洁和易于理解。
可组合性强
:不同类型的流可以通过管道(pipe)相互连接。
总之,流是 Node.js 中非常重要的概念,它提供了一种高效、灵活的数据传输方式
,广泛应用于文件 I/O
、网络通信
等场景。理解和掌握流的工作原理对于编写高性能的 Node.js 应用程序非常关键。
种类
流在Node.js中主要分为四种类型:
可读流
(Readable Streams) :用于从数据源读取数据的流。可读流提供了一种方法来读取数据,比如从文件、网络连接或其他数据源中读取数据,并且通常实现了一个或多个事件监听器来处理数据读取的过程。可写流
(Writable Streams) :用于向目标写入数据的流。可写流提供了一种方法来写入数据,比如向文件、网络连接或其他目标写入数据,并且通常实现了一个或多个方法来处理数据写入的过程。双工流
(Duplex Streams) :同时具有可读和可写功能的流。双工流允许读取数据同时也可以写入数据,通常用于网络套接字等场景。转换流
(Transform Streams) :是一种特殊的双工流,用于数据的转换和处理。转换流在数据传输过程中可以对数据进行处理、转换或过滤,比如压缩、加密、解密等操作。
这些流类型在Node.js中都有相应的模块和API来进行使用和操作。通过合理地选择和组合这些流类型,可以实现高效的数据处理和传输。
在NodeJS
中HTTP
服务器模块中,request
是可读流,response
是可写流。还有fs
模块,能同时处理可读和可写文件流
应用场景
stream
的应用场景主要就是处理IO
操作,而http
请求和文件操作都属于IO
操作
思考一下,如果一次IO
操作过大,硬件的开销就过大,而将此次大的IO
操作进行分段操作,让数据像水管一样流动,知道流动完成
常见的场景有:
get请求返回文件给客户端
文件操作
一些打包工具的底层操作
🍬示例1🍬:
假设我们有一个名为 file.txt
的文本文件,我们希望通过 GET 请求返回给客户端。我们可以使用 Readable Stream
来实现这个功能:
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
if (req.url === '/file') {
// 创建一个可读流,读取文件内容
const fileStream = fs.createReadStream('file.txt');
// 设置响应头,指定返回的文件类型
res.setHeader('Content-Type', 'text/plain');
// 将可读流通过管道(pipe)连接到响应对象
fileStream.pipe(res);
// 监听流的 'end' 事件,表示文件传输完成
fileStream.on('end', () => {
res.end();
});
} else {
res.statusCode = 404;
res.end('Not found');
}
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000/');
});
在这个示例中:
- 我们创建了一个 HTTP 服务器,监听 3000 端口。
- 当客户端发起
/file
的 GET 请求时,我们使用fs.createReadStream()
创建了一个可读流,读取file.txt
文件的内容。 - 我们设置响应头,指定返回的文件类型为
text/plain
。 - 然后,我们使用
fileStream.pipe(res)
将可读流通过管道(pipe)连接到响应对象res
🍬示例2🍬:
假设我们有一个名为 source.txt
的源文件,我们需要将其复制
到一个名为 target.txt
的目标文件。我们可以使用 Readable Stream
和 Writable Stream
来完成这个任务:
const fs = require('fs');
const sourceFile = 'source.txt';
const targetFile = 'target.txt';
// 创建可读流,读取源文件内容
const readStream = fs.createReadStream(sourceFile);
// 创建可写流,写入目标文件
const writeStream = fs.createWriteStream(targetFile);
// 将可读流通过管道(pipe)连接到可写流
readStream.pipe(writeStream);
// 监听可写流的 'finish' 事件,表示文件复制完成
writeStream.on('finish', () => {
console.log('File copy complete!');
});
// 监听可读流和可写流的 'error' 事件,处理错误
readStream.on('error', (err) => {
console.error('Error reading file:', err);
});
writeStream.on('error', (err) => {
console.error('Error writing file:', err);
});
在这个示例中:
- 我们使用
fs.createReadStream()
创建了一个可读流,读取source.txt
文件的内容。 - 使用
fs.createWriteStream()
创建了一个可写流,用于写入target.txt
文件。 - 然后,我们使用
readStream.pipe(writeStream)
将可读流通过管道(pipe)连接到可写流。这样,数据会从可读流逐块(chunk)地传输到可写流,从而完成文件的复制过程。 - 我们监听可写流的
'finish'
事件,在文件复制完成时打印一条消息。 - 同时,我们也监听了可读流和可写流的
'error'
事件,用于处理在读取或写入文件时可能发生的错误。
使用流进行文件复制的优点是:
- 内存利用率高,不需要一次性将整个文件加载到内存中。
- 处理大文件时性能更好,因为数据是逐块传输的。
- 代码更简洁,使用管道(pipe)连接可读流和可写流可以很方便地实现文件复制功能。
全局对象
global
在Node.js中,global
是一个全局对象,类似于浏览器环境中的window
对象。它包含了所有全局作用域中定义的变量、函数和模块。global
对象的属性和方法可以在任何地方访问,而不需要显式地导入或声明。
它包含了许多全局变量和方法,让我们一起来看看其中最重要的一些:
-
全局变量:
__filename
: 当前执行脚本的文件路径。__dirname
: 当前执行脚本所在的目录。process
: 代表当前 Node.js 进程的对象。console
: 提供了一组简单的控制台功能,用于输出信息、错误、警告等。
-
全局方法:
setTimeout(cb, ms)
,setInterval(cb, ms)
: 设置延迟执行的定时器。clearTimeout(timer)
,clearInterval(timer)
: 取消定时器。require(moduleName)
: 用于加载模块。exports
,module
: 与模块相关的对象。
需要注意的是,虽然global
对象中的属性和方法可以在任何地方直接使用,但为了保持代码的可读性和可维护性,最好避免滥用全局变量,而是尽可能使用模块化的方式来组织代码。
process
process
对象是 Node.js 中一个全局对象,它代表当前正在运行的 Node.js 进程。process
对象提供了许多常用的属性和方法,让我们一起来看看其中一些常用的:
-
属性:
process.env
: 包含所有的环境变量。例如通过 `process.env.NODE_ENV 获取不同环境项目配置信息process.argv
: 一个包含命令行参数的数组。process.argv[0]
为Node.js可执行文件的路径,process.argv[1]
为当前执行的脚本文件路径,之后的元素为传递给脚本的命令行参数。process.cwd()
: 返回当前工作目录。process.pid
: 返回当前进程的 ID。process.ppid
:当前进程对应的父进程process.platform
: 返回操作系统平台。process.uptime()
:返回Node.js进程的运行时间,单位为秒。process.title
:获取或设置当前进程的标题,通常用于在进程列表中标识进程。
-
方法:
process.exit([code])
: 以指定的退出码(默认为 0)退出当前进程。- 进程事件:
process.on(‘uncaughtException’,cb)
捕获异常信息、process.on(‘exit’,cb)
进程退出监听 process.nextTick(callback)
: 将回调函数添加到下一个事件循环队列。- 三个标准流:
process.stdout
标准输出、process.stdin
标准输入、process.stderr
标准错误输出
下面是一些示例:
// 访问环境变量
console.log(process.env.NODE_ENV); // 输出当前的 Node.js 环境
// 访问命令行参数
console.log(process.argv); // 输出包含所有命令行参数的数组
// 退出进程
process.exit(1); // 以非零退出码退出进程
// 监听进程事件
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
process.exit(1);
});
// 向标准输出和标准错误输出写入数据
process.stdout.write('Hello, World!\n');
process.stderr.write('Error occurred.\n');
console.log('Start');
process.nextTick(() => {
console.log('Next tick callback');
});
console.log('End');
process
对象是 Node.js 中非常重要的全局对象,它提供了许多与当前进程相关的信息和功能。通过使用 process
对象,我们可以更好地控制和管理 Node.js 应用程序的运行环境和行为。
EventEmitter类
EventEmitter
是Node.js核心模块events
中的唯一一个类,用于实现事件驱动的编程模式。
EventEmitter
是 Node.js 中实现发布-订阅模式的核心模块。它提供了一种机制,允许对象之间进行事件通信和事件处理。
在EventEmitter
的基础上,Node
几乎所有的模块都继承了这个类,这些模块拥有了自己的事件,可以绑定/触发监听器,实现了异步操作。
重要方法和属性
on(eventName, listener)
: 监听指定事件。当事件被触发时,相应的监听器函数会被调用。emit(eventName, [args])
: 发布指定事件,并可传递参数给事件处理函数。once(eventName, listener)
: 仅监听一次指定事件,事件被触发后,监听器会被移除。removeListener(eventName, listener)
: 移除指定事件的指定监听器。removeAllListeners([eventName])
: 移除指定事件的所有监听器,或移除所有事件的所有监听器。setMaxListeners(n)
: 设置单个事件的最大监听器数量。默认情况下,Node.js将打印警告消息,如果单个事件的监听器数量超过10个。
示例用法
const EventEmitter = require('events');
class Logger extends EventEmitter {
log(message) {
// 触发 'log' 事件
this.emit('log', message);
}
error(message) {
// 触发 'error' 事件
this.emit('error', new Error(message));
}
}
const logger = new Logger();
// 注册 'log' 事件监听器
logger.on('log', (message) => {
console.log(`Logged: ${message}`);
});
// 注册 'error' 事件监听器
logger.on('error', (err) => {
console.error(`Error: ${err.message}`);
});
// 触发 'log' 事件
logger.log('Hello, EventEmitter!');
// 输出: Logged: Hello, EventEm
事件循环机制
浏览器中的 JavaScript 事件循环机制是根据 HTML5 规范实现的,而 Node.js 中的事件循环是基于
libuv
库实现的。
libuv
是一个多平台的专注于异步IO的库,如下图最右侧所示:
上图EVENT_QUEUE
给人看起来只有一个队列,但EventLoop
存在6个阶段,每个阶段都有对应的一个先进先出的回调队列
流程
事件循环分成了六个阶段,对应如下:
- timers(定时器阶段) :处理定时器任务,包括setTimeout()和setInterval()等注册的回调函数。
- pending callbacks(待定回调阶段) :执行延迟到下一个循环迭代的I/O回调。
- idle, prepare(空闲、准备阶段) :仅在内部使用。
- poll(轮询阶段) :检索新的I/O事件,执行与I/O相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和setImmediate()调度的回调函数,poll阶段通常是最活跃的阶段)。
- check(检查阶段) :setImmediate()的回调函数在这个阶段被执行。
- close callbacks(关闭回调阶段) :一些关闭的回调函数,比如socket.on('close', ...)。
这六个阶段构成了 Node.js 事件循环的一个周期。事件循环会不断地在这六个阶段之间循环,直到没有更多的工作要执行。
这种分阶段的事件循环机制使 Node.js 能够高效地处理异步 I/O 操作,并避免了阻塞主线程的问题。不同类型的任务被分配到不同的队列中,事件循环会按照优先级依次执行它们。
除了上述6个阶段,还存在process.nextTick,其不属于事件循环的任何一个阶段,它属于该阶段与下阶段之间的过渡, 即本阶段执行结束, 进入下一个阶段前, 所要执行的回调,类似插队
流程图如下所示:
在Node
中,同样存在宏任务和微任务,与浏览器中的事件循环相似
微任务:
next tick queue
:process.nextTick()
other queue
: Promise 的then
回调、queueMicrotask()
宏任务:
timer queue
:setTimeout
、setInterval
poll queue
: I/O 事件check queue
:setImmediate
close queue
:close
事件
而它们的执行顺序为:
next tick microtask queue
other microtask queue
timer queue
poll queue
check queue
close queue
示例
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout0')
}, 0)
setTimeout(function () {
console.log('setTimeout2')
}, 300)
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick1'));
async1();
process.nextTick(() => console.log('nextTick2'));
new Promise(function (resolve) {
console.log('promise1')
resolve();
console.log('promise2')
}).then(function () {
console.log('promise3')
})
console.log('script end')
分析过程:
- 先找到同步任务,输出script start
- 遇到第一个 setTimeout,将里面的回调函数放到 timer 队列中
- 遇到第二个 setTimeout,300ms后将里面的回调函数放到 timer 队列中
- 遇到第一个setImmediate,将里面的回调函数放到 check 队列中
- 遇到第一个 nextTick,将其里面的回调函数放到本轮同步任务执行完毕后执行
- 执行 async1函数,输出 async1 start
- 执行 async2 函数,输出 async2,async2 后面的输出 async1 end进入微任务,等待下一轮的事件循环
- 遇到第二个,将其里面的回调函数放到本轮同步任务执行完毕后执行
- 遇到 new Promise,执行里面的立即执行函数,输出 promise1、promise2
- then里面的回调函数进入微任务队列
- 遇到同步任务,输出 script end
- 执行下一轮回到函数,先依次输出 nextTick 的函数,分别是 nextTick1、nextTick2
- 然后执行微任务队列,依次输出 async1 end、promise3
- 执行timer 队列,依次输出 setTimeout0
- 接着执行 check 队列,依次输出 setImmediate
- 300ms后,timer 队列存在任务,执行输出 setTimeout2
执行结果如下:
script start
async1 start
async2
promise1
promise2
script end
nextTick1
nextTick2
async1 end
promise3
setTimeout0
setImmediate
setTimeout2
最后有一道是关于setTimeout
与setImmediate
的输出顺序
setTimeout(() => {
console.log("setTimeout");
}, 0);
setImmediate(() => {
console.log("setImmediate");
});
输出情况如下:
情况一:
setTimeout
setImmediate
情况二:
setImmediate
setTimeout
分析下流程:
- 外层同步代码一次性全部执行完,遇到异步API就塞到对应的阶段
- 遇到
setTimeout
,虽然设置的是0毫秒触发,但实际上会被强制改成1ms,时间到了然后塞入times
阶段 - 遇到
setImmediate
塞入check
阶段 - 同步代码执行完毕,进入Event Loop
- 先进入
times
阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout
条件,执行回调,如果没过1毫秒,跳过 - 跳过空的阶段,进入check阶段,执行
setImmediate
回调
这里的关键在于这1ms,如果同步代码执行时间较长,进入Event Loop
的时候1毫秒已经过了,setTimeout
先执行,如果1毫秒还没到,就先执行了setImmediate
模块化
概念
- 模块化是指解决一个复杂问题时,自顶向下逐层把系统划分为若干模块的过程,模块是可组合、分解和更换的单元。
- 模块化可提高代码的复用性和可维护性,实现按需加载。
- 模块化规范是对代码进行模块化拆分和组合时需要遵守的规则,如使用何种语法格式引用模块和向外暴露成员。
模块分类
在Node.js中,模块可以分为三类:核心模块
、第三方模块
和自定义模块
。
核心模块
:Node.js提供了一些内置的核心模块,可以直接使用,无需安装。例如,fs
模块用于文件操作,http
模块用于创建HTTP服务器等。可以使用require
函数来导入核心模块。
const fs = require('fs');
const http = require('http');
第三方模块
:Node.js社区提供了大量的第三方模块,可以通过npm(Node.js包管理器)进行安装和使用。第三方模块通常由其他开发者编写,用于扩展Node.js的功能。例如,express
是一个常用的Web框架,lodash
提供了许多实用的工具函数。可以使用require
函数来导入第三方模块。
const express = require('express');
const _ = require('lodash');
自定义模块
:开发者可以根据自己的需求创建自定义模块,用于组织和复用代码。自定义模块可以是单个文件或者由多个文件组成的文件夹。可以使用module.exports
来导出模块,使用require
函数来导入模块。
// math.js
exports.add = function(a, b) {
return a + b;
};
// app.js
const math = require('./math');
const sum = math.add(2, 3);
console.log(sum); // 输出: 5
通过合理使用这三类模块,可以提高代码的可维护性和可复用性。核心模块提供了基本的功能支持,第三方模块提供了丰富的功能扩展,而自定义模块可以根据具体需求来组织和管理代码。
模块作用域
- 和函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域
- 防止全局变量污染
模块作用域的成员
- 自定义模块中都有一个
module
对象,存储了和当前模块有关的信息 - 在自定义模块中,可以使用
module.exports
对象,将模块内的成员共享出去,供外界使用。导入自定义模块时,得到的就是module.exports
指向的对象。 - 默认情况下,
exports
和module.exports
指向同一个对象。最终共享的结果,以module.exports
指向的对象为准。
CommonJS 模块化规范
- 每个模块内部,
module
变量代表当前模块 module
变量是一个对象,module.exports
是对外的接口- 加载某个模块即加载该模块的
module.exports
属性
模块加载机制
模块第一次加载后会被缓存,即多次调用 require()
不会导致模块的代码被执行多次,提高模块加载效率。
内置模块加载
内置模块加载优先级最高。
Node.js 中有一些内置的模块,比如 fs
、http
等,这些模块是直接通过模块名进行加载的,不需要指定路径。内置模块的加载优先级最高,Node.js 会先查找内置模块是否存在,如果存在则直接加载。
自定义模块加载
加载自定义模块时,路径要以 ./
或 ../
开头,否则会作为内置模块或第三方模块加载。
导入自定义模块时,若省略文件扩展名,则 Node.js 会按顺序尝试加载文件:
- 按确切的文件名加载
- 补全
.js
扩展名加载 - 补全
.json
扩展名加载 - 补全
.node
扩展名加载 - 报错
第三方模块加载
- 若导入第三方模块, Node.js 会从当前模块的父目录开始,尝试从
/node_modules
文件夹中加载第三方模块。 - 如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录。
例如,假设在 C:\Users\bruce\project\foo.js
文件里调用了 require('tools')
,则 Node.js 会按以下顺序查找:
C:\Users\bruce\project\node_modules\tools
C:\Users\bruce\node_modules\tools
C:\Users\node_modules\tools
C:\node_modules\tools
目录作为模块加载
当把目录作为模块标识符进行加载的时候,有三种加载方式:
- 在被加载的目录下查找
package.json
的文件,并寻找main
属性,作为require()
加载的入口 - 如果没有
package.json
文件,或者main
入口不存在或无法解析,则 Node.js 将会试图加载目录下的index.js
文件。 - 若失败则报错
参考文献
转载自:https://juejin.cn/post/7363078360786599975