[😈万字长文警告👿]NodeJs基础☞🀁

发布于:2024-04-30 ⋅ 阅读:(23) ⋅ 点赞:(0)

gratisography-surfing-dog-free-stock-photo.jpg

简介

Node.js是一个基于 Chrome V8 引擎JavaScript 运行时环境。

在浏览器外运⾏ V8 JavaScript 引擎(Google Chrome 的内核),利⽤事件驱动⾮阻塞I/O异步输⼊输出模型等技术提⾼性能,可以理解为 Node.js 就是⼀个服务器端的、⾮阻塞式I/O的、事件驱动的 JavaScript 运⾏环境

以下是 Node.js 的一些主要特点和优势:

  1. 事件驱动:Node.js 构建在事件驱动的基础上,使用了事件循环来处理并发请求,通过回调函数实现异步操作,从而提高了应用程序的性能和吞吐量。
  2. 非阻塞 I/O:Node.js 使用了非阻塞 I/O 模型,使得在执行 I/O 操作时不会阻塞后续代码的执行,从而实现了高效的并发处理。
  3. 单线程:虽然 Node.js 是单线程的,但通过事件循环和异步操作,可以处理大量并发请求,同时保持响应速度。
  4. 跨平台:Node.js 可以在多种操作系统上运行,包括 Windows、Linux、macOS 等,使得开发者能够轻松地在不同环境中部署应用程序。
  5. 模块化:Node.js 支持 CommonJS 模块规范,开发者可以通过 require 和 module.exports 来进行模块的导入和导出,从而实现代码的模块化和复用。
  6. 丰富的生态系统:Node.js 拥有丰富的第三方模块和工具,如 Express.js、、npm 等,使得开发者能够快速构建各种类型的应用程序,并且能够通过 npm 包管理器轻松地管理依赖项。
  7. 轻量高效:Node.js 本身的设计简单、轻量,启动快速,适合构建实时、高性能的网络应用程序。

Node.js 的主要应用场景包括 Web 服务器API 服务实时聊天应用物联网应用命令行工具等。它广泛应用于企业级应用程序开发、微服务架构、实时数据处理等领域。随着 JavaScript 的普及和 Node.js 生态系统的不断完善,Node.js 已经成为一种强大的服务器端技术。

事件驱动

事件驱动就是当进来⼀个新的请求的时,请求将会被压⼊⼀个事件队列中,然后通过⼀个循环来检测队列中的事件状态变化,如果检测到有状态变化的事件,那么就执⾏该事件对应的处理代码,⼀般都是回调函数

⽐如读取⼀个⽂件,⽂件读取完毕后,就会触发对应的状态,然后通过对应的回调函数来进⾏处理

Node.js 作为一个事件驱动的运行时环境,其事件驱动架构的优缺点具体如下:

优点:

  1. 高并发性: Node.js 的事件驱动模型使得它能够以非阻塞的方式处理大量并发请求,从而提高应用程序的吞吐量。
  2. 快速响应: Node.js 应用程序能够快速响应用户请求,因为事件处理不会阻塞主线程。
  3. 适合 I/O 密集型应用: Node.js 的事件驱动架构非常适合 I/O 密集型的应用程序,如 Web 服务器、实时聊天应用等。

缺点:

  1. CPU 密集型任务: Node.js 的单线程事件循环模型不太适合处理 CPU 密集型的任务,因为它会阻塞主线程,降低应用程序的响应性。
  2. 单线程限制 Node.js 是单线程的,虽然通过事件循环实现了并发处理,但某些 CPU 密集型任务可能会导致性能瓶颈。
  3. 可靠性低 ⼀旦代码某个环节崩溃,整个系统都崩溃

总的来说,Node.js 的事件驱动架构非常适合 I/O 密集型的应用程序,但对于 CPU 密集型任务或需要复杂并发控制的应用程序,可能需要采用其他的编程模型或技术栈。

⾮阻塞异步

Nodejs采⽤了⾮阻塞型 I/O 机制,在做 I/O 操作的时候不会造成任何的阻塞,当完成之后,以时间的形式通知执⾏操作。

在非阻塞式 I/O 中,当程序发起一个 I/O 操作(比如读取文件、发送网络请求等),它会立即返回并继续执行后续代码,而不会等待 I/O 操作完成。程序可以通过轮询或事件通知的方式来检查 I/O 操作是否已经完成,一旦完成,程序会得到通知并处理相应的结果。

Buffer 缓冲区

  • Buffer 的结构与数组类似操作方法也与数组类似
  • 数组不能存储二进制文件,Buffer 是专门存储二进制数据
  • Buffer 存储的是二进制数据,显示时以 16 进制的形式显示
  • Buffer 每一个元素范围是 00ff,即 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 中有许多应用场景,主要包括以下几个方面:

  1. 文件 I/O: 在读取和写入文件时,通常需要使用 Buffer 来处理二进制数据。例如,使用 fs.readFile()fs.writeFile() 方法时,就需要使用 Buffer 来存储和传输数据。
  2. 网络通信: 在网络通信中,数据通常以二进制形式传输。使用 Buffer 可以方便地处理这些二进制数据,例如在 HTTP 请求和响应中使用 Buffer 来传输数据。
  3. 数据编码和解码: 在处理字符串时,通常需要将其编码为二进制数据,或者从二进制数据中解码出字符串。Buffer 提供了方便的 API,如 toString()write() 方法,来实现这些功能。
  4. 二进制数据操作: Buffer 可用于对二进制数据进行各种操作,如切片、连接、比较等。这在一些需要处理二进制数据的场景中非常有用,例如图像处理、加密/解密等。
  5. 数据压缩和解压缩: 在处理大量数据时,数据压缩和解压缩是一个常见的需求。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:回调函数,当读取操作完成时调用。回调函数有两个参数 errdata,分别表示可能的错误信息和读取到的文件内容。
  • 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(directoryPath, '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);
  }
});

需要注意的是:

  1. 如果文件不存在,fs.appendFile()自动创建该文件。
  2. fs.appendFile() 会将数据追加到文件的末尾,而不是覆盖原有内容
  3. 如果出现错误,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);
  }
});

需要注意的是:

  1. fs.unlink() 只能删除文件,不能删除目录。如果要删除目录,可以使用 fs.rmdir() 方法。
  2. 如果指定的文件不存在,fs.unlink() 将会返回一个错误。
  3. 如果出现错误,fs.unlink() 会在回调函数的第一个参数中返回错误对象,开发者需要在回调函数中进行适当的错误处理。

使用 fs.unlink() 方法可以方便地删除文件,在需要清理文件系统或管理文件生命周期的场景中非常有用。

创建目录

  • fs.mkdir(path[, options], callback): 异步创建目录。

    • path:要创建的目录的路径。
    • options:可选参数,可以是一个对象,用于指定创建目录的选项,例如权限等。
    • callback:回调函数,当创建操作完成时调用。回调函数有一个参数 err,表示可能的错误信息。
  • fs.mkdirSync(path[, options]): 同步创建目录。

const fs = require('fs');
​
// 创建单个目录
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('目录创建成功!');
});

需要注意的是:

  1. 如果指定的目录已经存在,fs.mkdir() 将会返回一个错误。
  2. 如果设置了 recursive 选项为 true,则 fs.mkdir() 会创建任何必需的中间目录。这在创建多级目录时非常有用。
  3. mode 选项用于指定新目录的权限模式。它的值可以是一个八进制整数或一个字符串。
  4. 如果出现错误,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('目录及其内容删除成功!');
});

需要注意的是:

  1. fs.rmdir() 只能删除空目录。如果目录中包含文件或其他子目录,删除操作将会失败,除非设置 options.recursivetrue
  2. 如果指定的目录不存在,fs.rmdir() 将会返回一个错误。
  3. 如果出现错误,fs.rmdir() 会在回调函数的第一个参数中返回错误对象,开发者需要在回调函数中进行适当的错误处理。
  4. 使用 options.recursive 时,如果目录包含许多文件和子目录,删除操作可能会比较慢,因此可以考虑使用 options.maxRetriesoptions.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);
  }
});

需要注意的是:

  1. fs.rename() 可以用于重命名文件或目录
  2. 如果 newPath 指定的文件或目录已经存在,则会被覆盖
  3. 如果指定的 oldPath 不存在,fs.rename() 将会返回一个错误。
  4. 如果出现错误,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 模块的常用功能:

  1. 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)
    })
    
  2. path.resolve(...) : 用于将相对路径转换为绝对路径。它会从右到左解析路径片段,直到构造出一个绝对路径。

    const path = require('path');
    const filePath = path.resolve(__dirname, 'files', 'example.txt');
    // 输出: '/path/to/project/files/example.txt'
    
  3. path.dirname(path) : 返回一个路径的目录名部分。

    const path = require('path');
    const dirPath = path.dirname('/path/to/project/files/example.txt');
    // 输出: '/path/to/project/files'
    
  4. 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'
    
  5. path.extname(path) : 返回一个路径的扩展名部分。

    const path = require('path');
    const fileExtension = path.extname('/path/to/project/files/example.txt');
    // 输出: '.txt'
    
  6. path.parse(path) : 将一个路径字符串解析为一个对象,包含 dirrootbasenameext 属性。

    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'
    // }
    
  7. 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 服务器的步骤如下:

  1. 导入 http 模块: 首先,我们需要导入 Node.js 内置的 http 模块,它提供了创建 HTTP 服务器和客户端的功能。

    const http = require('http');
    
  2. 创建 HTTP 服务器: 使用 http.createServer() 方法创建一个 HTTP 服务器。这个方法接受一个回调函数,该函数会在每次收到 HTTP 请求时被调用。

    const server = http.createServer();
    
  3. 为服务器实例绑定 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) 方法响应。
  4. 启动服务器: 最后,我们使用 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 模块的功能:

  1. 系统信息:

    • os.arch(): 返回 Node.js 二进制文件的操作系统 CPU 架构。
    • os.platform(): 返回操作系统的平台。
    • os.type(): 返回操作系统名称。
    • os.release(): 返回操作系统的发行版本。
  2. 文件路径:

    • os.homedir(): 返回用户的主目录。
    • os.tmpdir(): 返回系统默认的临时文件目录。
    • os.EOL: 返回当前操作系统的行末标志(换行符)。
  3. CPU 信息:

    • os.cpus(): 返回一个包含 CPU 信息的对象数组。
    • os.loadavg(): 返回系统的平均负载。
  4. 内存信息:

    • os.totalmem(): 返回系统内存总量(单位为字节)。
    • os.freemem(): 返回操作系统空闲内存量(单位为字节)。
  5. 网络信息:

    • 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),流还可以是视频流、音频流、数据流等不同类型。流的类型取决于所传输的数据类型。

流的独特之处在于,它不像传统的程序那样一次将一个文件读入内存,而是逐块读取数据、处理其内容,而不是将其全部保存在内存中

流可以分为三部分:

  1. 数据源(source) :产生数据的地方,可以是文件、网络连接、内存等。
  2. 数据目标(destination) :接收数据的地方,可以是文件、网络连接、内存等。
  3. 管道(pipe) :将数据从数据源流向数据目标的连接。

基本语法是 source.pipe(dest),它会将数据从源流向目标,实现数据的传输和处理。

  1. 流的优势:

    • 内存效率高:无需一次性加载整个数据到内存,可以分块读写。
    • 代码简洁:流提供了统一的接口,使得代码更加简洁和易于理解。
    • 可组合性强:不同类型的流可以通过管道(pipe)相互连接。

总之,流是 Node.js 中非常重要的概念,它提供了一种高效、灵活的数据传输方式,广泛应用于文件 I/O网络通信等场景。理解和掌握流的工作原理对于编写高性能的 Node.js 应用程序非常关键。

种类

流在Node.js中主要分为四种类型:

  1. 可读流(Readable Streams) :用于从数据源读取数据的流。可读流提供了一种方法来读取数据,比如从文件、网络连接或其他数据源中读取数据,并且通常实现了一个或多个事件监听器来处理数据读取的过程。
  2. 可写流(Writable Streams) :用于向目标写入数据的流。可写流提供了一种方法来写入数据,比如向文件、网络连接或其他目标写入数据,并且通常实现了一个或多个方法来处理数据写入的过程。
  3. 双工流(Duplex Streams) :同时具有可读和可写功能的流。双工流允许读取数据同时也可以写入数据,通常用于网络套接字等场景。
  4. 转换流(Transform Streams) :是一种特殊的双工流,用于数据的转换和处理。转换流在数据传输过程中可以对数据进行处理、转换或过滤,比如压缩、加密、解密等操作。

这些流类型在Node.js中都有相应的模块和API来进行使用和操作。通过合理地选择和组合这些流类型,可以实现高效的数据处理和传输。

NodeJSHTTP服务器模块中,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/');
});

在这个示例中:

  1. 我们创建了一个 HTTP 服务器,监听 3000 端口。
  2. 当客户端发起 /file 的 GET 请求时,我们使用 fs.createReadStream() 创建了一个可读流,读取 file.txt 文件的内容。
  3. 我们设置响应头,指定返回的文件类型为 text/plain
  4. 然后,我们使用 fileStream.pipe(res) 将可读流通过管道(pipe)连接到响应对象 res

🍬示例2🍬:

假设我们有一个名为 source.txt 的源文件,我们需要将其复制到一个名为 target.txt 的目标文件。我们可以使用 Readable StreamWritable 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);
});

在这个示例中:

  1. 我们使用 fs.createReadStream() 创建了一个可读流,读取 source.txt 文件的内容。
  2. 使用 fs.createWriteStream() 创建了一个可写流,用于写入 target.txt 文件。
  3. 然后,我们使用 readStream.pipe(writeStream) 将可读流通过管道(pipe)连接到可写流。这样,数据会从可读流逐块(chunk)地传输到可写流,从而完成文件的复制过程。
  4. 我们监听可写流的 'finish' 事件,在文件复制完成时打印一条消息。
  5. 同时,我们也监听了可读流和可写流的 'error' 事件,用于处理在读取或写入文件时可能发生的错误。

使用流进行文件复制的优点是:

  1. 内存利用率高,不需要一次性将整个文件加载到内存中。
  2. 处理大文件时性能更好,因为数据是逐块传输的。
  3. 代码更简洁,使用管道(pipe)连接可读流和可写流可以很方便地实现文件复制功能。

全局对象

global

在Node.js中,global是一个全局对象,类似于浏览器环境中的window对象。它包含了所有全局作用域中定义的变量、函数和模块。global对象的属性和方法可以在任何地方访问,而不需要显式地导入或声明。

它包含了许多全局变量和方法,让我们一起来看看其中最重要的一些:

  1. 全局变量:

    • __filename: 当前执行脚本的文件路径。
    • __dirname: 当前执行脚本所在的目录。
    • process: 代表当前 Node.js 进程的对象。
    • console: 提供了一组简单的控制台功能,用于输出信息、错误、警告等。
  2. 全局方法:

    • setTimeout(cb, ms), setInterval(cb, ms): 设置延迟执行的定时器。
    • clearTimeout(timer), clearInterval(timer): 取消定时器。
    • require(moduleName): 用于加载模块。
    • exports, module: 与模块相关的对象。

需要注意的是,虽然global对象中的属性和方法可以在任何地方直接使用,但为了保持代码的可读性和可维护性,最好避免滥用全局变量,而是尽可能使用模块化的方式来组织代码。

process

process 对象是 Node.js 中一个全局对象,它代表当前正在运行的 Node.js 进程。process 对象提供了许多常用的属性和方法,让我们一起来看看其中一些常用的:

  1. 属性:

    • 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:获取或设置当前进程的标题,通常用于在进程列表中标识进程。
  2. 方法:

    • 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的库,如下图最右侧所示:

1161361-20210615163956733-1487025398.png

上图EVENT_QUEUE 给人看起来只有一个队列,但EventLoop存在6个阶段,每个阶段都有对应的一个先进先出的回调队列

流程

事件循环分成了六个阶段,对应如下:

1161361-20210615164006197-1660686614.png

  1. timers(定时器阶段) :处理定时器任务,包括setTimeout()和setInterval()等注册的回调函数。
  2. pending callbacks(待定回调阶段) :执行延迟到下一个循环迭代的I/O回调。
  3. idle, prepare(空闲、准备阶段) :仅在内部使用。
  4. poll(轮询阶段) :检索新的I/O事件,执行与I/O相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和setImmediate()调度的回调函数,poll阶段通常是最活跃的阶段)。
  5. check(检查阶段) :setImmediate()的回调函数在这个阶段被执行。
  6. close callbacks(关闭回调阶段) :一些关闭的回调函数,比如socket.on('close', ...)。

这六个阶段构成了 Node.js 事件循环的一个周期。事件循环会不断地在这六个阶段之间循环,直到没有更多的工作要执行。

这种分阶段的事件循环机制使 Node.js 能够高效地处理异步 I/O 操作,并避免了阻塞主线程的问题。不同类型的任务被分配到不同的队列中,事件循环会按照优先级依次执行它们。

除了上述6个阶段,还存在process.nextTick,其不属于事件循环的任何一个阶段,它属于该阶段与下阶段之间的过渡, 即本阶段执行结束, 进入下一个阶段前, 所要执行的回调,类似插队

流程图如下所示:

1161361-20210615164024786-902965115.png

Node中,同样存在宏任务和微任务,与浏览器中的事件循环相似

微任务:

  • next tick queue: process.nextTick()
  • other queue: Promise 的 then 回调、queueMicrotask()

宏任务:

  • timer queue: setTimeoutsetInterval
  • poll queue: I/O 事件
  • check queue: setImmediate
  • close queue: close 事件

而它们的执行顺序为:

  1. next tick microtask queue
  2. other microtask queue
  3. timer queue
  4. poll queue
  5. check queue
  6. 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

最后有一道是关于setTimeoutsetImmediate的输出顺序

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中,模块可以分为三类:核心模块、第三方模块和自定义模块。

  1. 核心模块:Node.js提供了一些内置的核心模块,可以直接使用,无需安装。例如,fs模块用于文件操作,http模块用于创建HTTP服务器等。可以使用require函数来导入核心模块。
const fs = require('fs');
const http = require('http');
  1. 第三方模块:Node.js社区提供了大量的第三方模块,可以通过npm(Node.js包管理器)进行安装和使用。第三方模块通常由其他开发者编写,用于扩展Node.js的功能。例如,express是一个常用的Web框架,lodash提供了许多实用的工具函数。可以使用require函数来导入第三方模块。
const express = require('express');
const _ = require('lodash');
  1. 自定义模块:开发者可以根据自己的需求创建自定义模块,用于组织和复用代码。自定义模块可以是单个文件或者由多个文件组成的文件夹。可以使用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 指向的对象。
  • 默认情况下,exportsmodule.exports 指向同一个对象。最终共享的结果,以 module.exports 指向的对象为准。

CommonJS 模块化规范

  • 每个模块内部,module 变量代表当前模块
  • module 变量是一个对象,module.exports 是对外的接口
  • 加载某个模块即加载该模块的 module.exports 属性

模块加载机制

模块第一次加载后会被缓存,即多次调用 require() 不会导致模块的代码被执行多次,提高模块加载效率。

内置模块加载

内置模块加载优先级最高。

Node.js 中有一些内置的模块,比如 fshttp 等,这些模块是直接通过模块名进行加载的,不需要指定路径。内置模块的加载优先级最高,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 文件。
  • 若失败则报错

参考文献