书籍目录
1- 课程介绍
2- Node.js简介
3- Node.js的安装和配置
4- 使用Node进行编程
5- 全局对象
6- CommonJs
7- 模块
8- 包和NPM
9- 异步I/O
10- 事件
11- 异步编程
12- util
13- 文件系统 fs
14- 路径模块 Path
15- Buffer
16- Stream
17- 系统 os
18- url和queryString
19- dns
20- 其他模块
21- net
22- http
23- https
24- WebSocket
25- 进程和子进程
26- 数据库访问
27- ORM操作数据库
28- RESTful API设计与实现
29- Express框架
30- 中间件
31- 页面渲染与模板引擎
32- 用户认证与授权
33- 错误处理与断言
34- 测试
35- 使用TypeScript
36- 项目工程化
37- 性能优化和安全性
38- 日志
39- 监控
40- 部署和扩展
123
Node.js开发 入门与进阶
限时免费
共40小节
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时,用于构建高性能、可扩展的网络应用程序。它提供了丰富的库和模块,使得开发者能够轻松地构建服务器端应用、命令行工具和其他类型的应用。 本教程将为你提供 Node.js 开发的入门与进阶指南,帮助你快速掌握 Node.js 的核心概念和常用技术。
离线

Mixixi

私信

前言

随着互联网技术的不断发展,Web应用程序的需求也随之不断增加。作为一名开发者,我们需要掌握各种技术来满足不同的需求。在服务器端开发领域,Node.js 已经成为了一种非常流行和强大的工具。

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,它让我们可以使用 JavaScript 语言进行服务器端编程,从而实现高性能、可扩展的网络应用和后端服务。相较于传统的服务器端开发语言,Node.js 拥有更好的性能、更高的效率和更良好的开发体验。

本书的目标是帮助读者全面了解 Node.js 的基本原理、核心模块、常用框架和实际应用,从而能够掌握 Node.js 的核心概念和技术,并运用它们构建高性能、可靠和可扩展的Web应用程序和后端服务。

本书内容介绍

下面将对本书的内容做一个整体的介绍,这样可以对书中的知识体系有一个全面的了解。

第一部分:基础知识和工具

这一部分将介绍 Node.js 的基本概念和原理,包括 Node.js 的起源和发展、安装和配置、工具的使用以及基础知识的介绍。通过学习这些内容,您将对 Node.js 有一个全面的认识,并能够开始进行基础的开发工作。

第二部分:核心模块和常用工具

这一部分主要介绍 Node.js 的核心模块和常用工具。它包括 Node.js 的核心模块、异步编程、模块和包管理等知识内容。这些核心模块和常用工具在 Node.js 开发中非常重要,它们提供了丰富的功能和工具,方便开发者进行文件操作、事件处理、工具函数调用等任务。

第三部分:网络编程和Web开发

这部分主要介绍如何使用 Node.js 构建 TCP、UDP、HTTP、Websocket 服务和进程以及网络服务和安全等,还有如何使用 Node.js 去进行一些Web开发。

第四部分:数据库和Web框架的使用

这部分介绍了如何使用 Node.js 去连接和访问数据库,并通过 Express 框架构建Web服务。这部分包含了数据库访问、ORM、Restful API、EXpress框架、中间件和模版引擎渲染等技术。通过这部分的学习,可以快速开发一个现代化、高性能的Web应用服务器。

第五部分:Node.js进阶

Node.js 的进阶部分涵盖了测试、项目产品化、日志监控、安全性、性能优化和部署扩展等主题。掌握这些知识将使开发者能够更好地组织和管理代码、保障应用程序的安全性和提升应用程序的性能以及项目的运维部署。

读者建议

本书适合那些具备一定 JavaScript 编程基础和服务器端开发经验的读者。如果您已经掌握了 JavaScript 语言、Web开发以及服务器端开发的基础知识,那么这本书将对您进一步提升技能和拓宽视野非常有帮助。如果您还没有这些基础,建议先学习相关知识,以充分理解本书的内容。

建议多动手实践,建议手动尝试运行本书中的代码,以便能够帮助您快速的掌握本书中的内容。最好能够自己动手去做一个 Node.js 的项目,这样会更大限度的加深对 Node.js 的理解。

购买须知

  1. 购买后可获得永久阅读本书的权限。
  2. 本书版权归陕西云端源想有限公司所有,只可自己阅读学习,禁止私自转载、链接、转贴、或用于任何商业用途。如有违反,将追究法律责任。
  3. 本书为虚拟内容,如果购买成功概不退款,敬请见谅。
  4. 由于篇幅限制和时间仓促,在书中难免会有疏漏和错误,如有任何问题可随时联系或者私信@我,笔者非常感谢能收到您的反馈和意见。

1-Node.js简介

Node.js 现在已经是非常火热的技术了,从本节课程开始我们将会带大家了解更多的内容。

1.1 Node.js的发展史

Node.js 的发展史可以追溯到2009年,当时Ryan Dahl发布了第一个 Node.js 版本。Node.js 最初是基于V8引擎和libuv库开发的,旨在使 JavaScript 能够在服务器端执行,实现高性能、可扩展和实时应用程序的构建。

在2010年Ryan Dahl加入了Joyent公司,随后由Ryan Dahl全职负责 Node.js。随着 Node.js 开始受到越来越多的关注和支持,也使得它成为当时最受欢迎的开源项目之一。大量的社区贡献者加入了 Node.js 的开发,提供了大量的模块和工具,帮助开发者快速构建具有各种功能的应用程序。此外,Node.js 的出现也推动了前端和后端技术的融合,使得前端开发者能够使用熟悉的 JavaScript 语言进行全栈开发。

随着 Node.js 的普及,越来越多的企业和组织开始将其用于生产环境中的应用程序。Node.js 也不断地更新和改进,增加了更多的特性和功能,同时也在性能和安全方面得到了更好的保障。目前,Node.js 已经成为全球最流行的服务器端 JavaScript 环境之一,被广泛地应用于Web开发、移动应用、物联网等领域。

1.2 什么是Node.js

Node.js 它既不是语言,也不是一个 JavaScript 框架,更不是一个浏览器端的库。Node.js 是一个能够让 JavaScript 运行在服务端的开发平台,这使得它已经能够和PHP、Python、Perl等平起平坐。

Node.js 的出现极大地拓展了 JavaScript 的应用范围,不再局限于浏览器端的脚本语言。通过 Node.js,开发人员可以构建高性能、可伸缩的网络应用程序,处理大量并发请求,以及执行I/O密集型任务。

图1 Chrome和Node组件组成

Node.js 采用了事件驱动、非阻塞I/O模型,使得它能够高效地处理并发请求。相比传统的同步I/O模型,Node.js 的异步非阻塞特性使得应用程序能够更快地响应请求,提供更好的性能和可扩展性。

除了作为服务器端的运行时环境,Node.js 还提供了丰富的模块和工具生态系统,使得开发人员能够轻松构建Web应用程序、命令行工具、API服务等各种类型的应用。通过npm(Node Package Manager),开发人员可以方便地安装、管理和共享代码库。

Node.js 在云计算、大数据处理、实时通信等领域广泛应用。许多知名公司和开源项目都使用 Node.js 作为其核心技术栈,如Netflix、LinkedIn、Uber等。

总之,Node.js 是一个强大的服务器端运行时环境,允许开发人员使用 JavaScript 语言构建高性能、可伸缩的应用程序,拓展了 JavaScript 的应用范围,并在Web开发领域取得了广泛的应用和认可。

1.3 Node.js和JavaScript

JavaScript 是一种高级、解释型的编程语言,最初被设计用于在Web浏览器中实现动态交互和用户界面。它具有类似C语言的基本语法结构,支持动态类型和面向对象编程特性。随着 Node.js 的出现,JavaScript 也成为了一种流行的服务器端编程语言。JavaScript 还可以用于全栈开发,利用同一种编程语言在前端和后端进行开发。JavaScrip t拥有丰富的生态系统和第三方库,提供了丰富的功能和API。Node.js 中的 JavaScript 指的是 Core JavaScript 或者是 ECMAScript 的一个实现,它不包含DOM、BOM或者 Client JavaScript。Node.js 使得 JavaScript 可以运行在浏览器之外的平台。它有了文件系统、模块、操作系统API、网络通信等 Core JavaScript 所没有的功能。

Node.js 是一个基于Chrome V8 JavaScript引擎的运行时环境。V8 引擎可以说是世界上最快的 JavaScript 引擎,它的执行速度已经接近本地代码的执行速度。Node.js 实现了前后端编程环境的统一,可以大大降低前端转后端所付出的代价。

1.4 Node.js的特点

Node.js 作为 JavaScript的运行平台,主要用于服务器端编程。下面是 Node.js 的几个主要特点:

  1. 事件驱动和非阻塞I/O模型:Node.js 采用事件驱动的模型,所有的I/O操作都是异步的,不会阻塞程序的执行。这使得 Node.js 非常适合高并发、I/O密集型的应用场景。

  2. 单线程和多进程:Node.js 使用单线程模型来实现高并发。在单线程模型下,所有的I/O操作都是异步的,因此不会阻塞程序的执行。同时,Node.js 也支持多进程模型,通过子进程方式实现多进程,可以充分利用多核CPU的性能。

  3. 高效的内存管理:Node.js 使用了V8引擎来解析和执行 JavaScript 代码,V8引擎具有高效的内存管理机制,可以自动回收不再使用的内存。同时,Node.js 也提供了一些内存管理工具,如 Heapdump 和 profiler 等,方便开发人员进行内存优化和调试。

  4. 模块化:Node.js 使用 CommonJS 规范来实现模块化,所有的代码都是模块化的。开发人员可以使用 require 函数来引入其他模块的代码,使用 module.exportsexports 来导出自己的代码。这样可以有效地组织、共享和重用代码。

  5. 社区生态的丰富性:Node.js 拥有一个庞大的社区,有许多优秀的第三方模块和工具可供使用,如Express、Socket.io、Mongoose等。这些模块和工具可以极大地提高开发效率,加速应用程序的开发和部署。

  6. 跨平台性:Node.js 可以在各种操作系统下运行,如Windows、Linux、MacOS等。这使得 Node.js 非常适合开发跨平台的应用程序。

1.5 Node.js能做什么

Node.js 作为一种服务器端 JavaScript 运行时环境 你可以做以下几个方面的事情:

  1. 构建Web应用程序:Node.js 提供了HTTP模块,可以轻松地创建Web服务器。同时,Node.js 还有丰富的第三方模块和框架,如Express、Koa等,可以帮助开发人员快速构建高质量的Web应用程序。

  2. 实现API服务:Node.js 可以用来实现RESTful API服务,处理和响应HTTP请求。同时,Node.js 还可以与各种数据库进行交互,如MySQL、MongoDB等,实现数据的读写操作。

  3. 开发命令行工具:Node.js 可以用来开发命令行工具,如Webpack、Gulp等。这些工具可以自动化执行一些重复性的任务,提高开发效率。

  4. 实现网络编程:Node.js 可以用来实现网络编程,如构建TCP、UDP服务器和客户端等。同时,Node.js 还可以使用WebSocket协议,实现实时通信功能。

  5. 处理文件系统:Node.js 可以用来处理文件系统,如读写文件、创建和删除目录等。同时,Node.js 还可以监视文件和目录的变化,支持文件流的方式进行读写操作。

  6. 带图形的本地应用程序:可以使用 Node.js 与其他工具和框架结合(例如:Electron、Qt等),来创建带有图形界面的本地应用程序。

Node.js 具有丰富的应用场景和功能,可以用于各种类型的应用程序开发。通过丰富的第三方模块和工具,开发人员可以轻松地构建高质量的应用程序。

1.6 有哪些公司在用

Node.js 在业界得到了广泛的应用和采用,许多知名公司都在使用 Node.js 来构建他们的应用程序。以下是一些使用 Node.js 的知名公司:

  1. Netflix:Netflix是全球领先的在线流媒体服务提供商,在其服务器端应用程序中广泛使用 Node.js 。Netflix利用 Node.js 的高并发和非阻塞I/O模型来处理大量的并发请求,提供快速且稳定的流媒体服务。

  2. Uber:Uber是一家全球知名的打车服务公司,他们的后端系统中也使用了 Node.js。Uber的实时应用程序需要处理大量的并发请求和实时数据交换,Node.js 的事件驱动和非阻塞I/O模型非常适合这种场景。

  3. LinkedIn:LinkedIn是全球最大的职业社交网络平台,他们的服务端应用程序中也采用了 Node.js。LinkedIn使用 Node.js 来构建实时通信、数据推送等功能,通过 Node.js 的高性能和轻量级特性,实现了快速响应和高并发的需求。

  4. PayPal:PayPal是一家全球领先的在线支付解决方案提供商,他们的一些服务和工具中也使用了Node.js。PayPal利用 Node.js 构建了一些内部工具和API服务,以提高开发效率和系统性能。

  5. Walmart:Walmart是全球最大的零售公司之一,他们的一些后端系统中也采用了 Node.js。Walmart利用 Node.js 构建了一些实时数据分析和监控系统,通过 Node.js 的高效性能和灵活性,实现了对商业数据的及时处理和分析。

除了以上这些公司,还有许多其他知名公司也在使用 Node.js,如Yahoo、NASA、IBM等。Node.js 的高性能、丰富的生态系统和广泛的社区支持使得它成为许多公司选择的首选技术之一。

总结

Node.js 以其高性能、可扩展性和丰富的生态系统成为开发者喜爱的选择,为构建现代化的网络应用程序提供了强大的支持。Node.js 无论是构建Web应用、处理实时数据还是开发命令行工具等等,都是一个强大的选择。

2-Node.js的安装和配置

从2009年诞生至今,Node.js 一直处于快速发展的阶段,很多方法和功能都会被新的技术所取代,因此我们需要注意的是对于 Node.js 的版本没必要追求最新。由于 Node 的安装方式有很多,我们主要介绍从官网的途径去获取并安装。下面我们将介绍如何在Windows、Mac和Linux系统上安装Node环境。

2.1 安装前的准备

官网 下载安装,推荐使用 LTS 版本(长期支持版本)进行安装。

2.1.1 下载并安装

点击官网的 Other Downloads 然后根据自己的操作系统选择对应的安装包下载进行安装。

图1 官网

注意事项:
1. 点击官网下载页面上的 Previous Releases 栏可以选择往期版本进行安装。
2. 需要注意自己的操作系统,32位操作系统下载32位安装版,64位操作系统下载64位安装包,不能选错。
3. 注意有些计算机的处理器是arm架构(如:Mac M1、M2),需要下载ARM64的安装包。

图2 历史版本 (1)

图3 历史版本 (2)

图4 历史版本 (3)

2.1.2 Windows上安装Node.js

安装

  1. 找到并点击下载好的安装包,点击 Next 开始进行安装。
图5 安装 (1)

  1. 勾选 接受,继续安装。
图6 安装 (2)

  1. 根据自己的喜好,可以将 Node.js 安装到你选择的磁盘目录下,点击 Next

    点击 change 可以选择 Node.js 安装到你选择的磁盘目录。

图7 安装 (3)

图8 安装 (4)

  1. 保持默认,点击 Next
图9 安装 (5)

  1. 继续点击 Next
图10 安装 (6)

  1. 点击 install 开始安装,最后等待进度条结束完成安装。
图11 安装 (7)

  1. 在 Node.js 的安装目录下创建 node_globalnode_cache 文件夹。
图12 安装 (8)

  1. 管理员身份打开cmd,运行下面命令:
npm config set prefix "D:\NodeJS\node_global"
npm config set prefix "D:\NodeJS\node_cache"

环境变量配置

  • Windows操作系统下,通过 系统 > 高级系统设置 > 环境变量 来进行环境变量设置。

    图13 环境变量 (1)

    图14 环境变量 (2)

    图15 环境变量 (3)

  • 在系统变量 path > 编辑 里添加 D:\NodeJS

    图16 环境变量 (4)

    图17 环境变量 (5)

  • 在系统变量中新建变量名 NODE_PATH,变量值 D:\NodeJS\node\_global\node_modules

    图18 环境变量 (6)

    然后在系统变量 path > 编辑 里添加 %NODE_PATH%

    图19 环境变量 (图7)

    图20 环境变量 (8)

  • 在用户变量 path > 编辑,新增 D:\NodeJS\node_global

    图21 环境变量 (9)

2.1.3 Mac上安装Node.js

  1. 点击下载好的安装包,点击 继续 开始进行安装。

    图22 Mac上安装Node.js (1)

  2. 在软件许可协议阶段点击 继续,然后点击同意。

    图23 Mac上安装Node.js (2)

  3. 根据自己喜好,点击 更改安装位置,可以修改 Node.js 的安装磁盘位置,完成后点击继续等待进度条结束完成安装。

    图24 Mac上安装Node.js (3)

    图25 Mac上安装Node.js (4)

2.1.4 Linux上安装Node.js

安装

进入到 /usr/local 下载压缩包并解压。

# 进入到/usr/lcoal目录下
cd /usr/lcoal
# 创建app文件夹
mkdir app
# 使用wget工具下载Node.js
wget https://nodejs.org/download/release/v18.19.0/node-v18.19.0-linux-x64.tar.gz
# 解压
tar -zxvf node-v18.19.0-linux-x64.tar.gz -C /usr/local/app

环境变量配置

将node-v18.19.0-linux-x64下的 bin 目录加入环境变量中,修改 /etc/profile 或者 $HOME/.profile 文件。
在 profile 文件末尾追加配置内容。

vim /etc/profile

vim $HOME/.profile

英文输入法状态下输入 i 进入编辑状态,并末尾追加下面的内容:

export NODE_HOME=/usr/local/app/node-v18.19.0-linux-x64
export PATH=$PATH:$NODE_HOME/bin

最后输入 :wq 退出编辑状态并保存。

2.1.5 验证是否安装成功

  • 查看node版本:node -v

    图26 验证 (1)

  • 查看npm版本:npm -v

    图27 验证 (2)


如果嫌安装配置环境麻烦,我们还可以使用远端源想的在线编程工具,直接创建项目编写并运行代码,后面我们会介绍。

2.1.6 设置镜像地址

在 cmd或终端 中输入以下命令:

# 更换npm源为淘宝镜像
npm config set registry https://registry.npm.taobao.org
# 设置 npm官方镜像仓库 (如果你想使用官方镜像地址就执行这条命令)
npm config set registry https://registry.npmjs.org

查看是否配置成功,在cmd中输入 npm config get registry 是否能成功输出淘宝的镜像地址。

# 查看npm源地址
npm config get registry

注意: 如果遇到 npm ERR: request to https://registry.npm.taobao.org failed, reason: certificate has expired错误,这是因为证书已经过期了;淘宝镜像站已经切换成新的域名。需要将 https://registry.npm.taobao.org 换成 https://registry.npmmirror.com

2.2 多版本管理器

Node.js 更新速度非常快,有可能旧版本的方法和API会被新版本所替代,这就会造成代码不能正常的向下兼容。如果你想尝试新版本带来的新特性,又想保持目前稳定的开发环境。这时候就需要多版本管理器了。Node.js 的多版本管理器主要包括nvm、nvm-windows和n。它们可以帮助开发者在同一台机器上同时安装和切换不同版本的 Node.js。

  • nvm(Node Version Manager):nvm是一个跨平台的 Node.js 版本管理工具,适用于 macOS 和 Linux 系统。它允许用户在同一台机器上安装和管理多个 Node.js 版本,并且可以轻松切换使用不同的版本。nvm 还支持在不同的项目中使用不同的 Node.js 版本,这对于处理依赖关系和保持项目的一致性非常有帮助。

  • nvm-windows:nvm-windows 是为 Windows 操作系统设计的 Node.js 版本管理工具。类似于 nvm,在 Windows 上使用 nvm-windows 可以方便地安装和切换不同版本的 Node.js。nvm-windows 提供了一个交互式的命令行界面,用户可以从命令行界面中选择要安装和使用的 Node.js 版本。

  • n:n 是另一个简单实用的 Node.js 版本管理工具,适用于macOS和Linux系统。与 nvm 不同,n 不提供交互式的命令行界面,而是通过命令行命令来安装和切换 Node.js 版本。n 还提供了一些其他有用的功能,例如列出可用版本、安装指定版本、切换默认版本等。

如果已经正确安装了 Node.js 环境,那么就可以直接使用 npm install -g n 命令来管理 Node.js,安装好 n,可以使用 n --help查看它的帮助命令。

注意:
1. n不支持Windows系统,Windows系统下需要使用 nvm-windows
2. n只能管理通过n安装的 Node.js。
2. 多版本管理器根据需要安装,Node.js 学习阶段不是必须的。

总结

这章节我们介绍了 Node.js 环境的安装以及环境变量配置等内容,请确保 Node.js 的开发环境能够正确安装,这将为我们后面的学习开发等知识内容做好良好的铺垫。

3-使用Node进行编程

通过前面的介绍和学习,相信你已经成功安装了 Node.js,接下来就让我们开始正式学习如何在 Node.js 上编程并运行代码。

3.1 Node.js交互式环境REPL

REPL (Read Eval Print Loop),它是 Node.js 提供的交互式环境,允许用户直接在命令行中输入 JavaScript 代码并立即得到执行结果。这个功能类似于 Python 的交互式环境或者 Ruby 的 IRB。

通过在命令行中输入 node 命令,即可进入 Node.js 的 REPL 环境。在 REPL 环境中,用户可以输入任意的 JavaScript 代码,并且 Node.js 会立即对这些代码进行解析、求值和输出结果。例如,用户可以输入简单的数学运算、定义变量、调用函数等操作,并且可以立即得到执行结果。这使得开发者可以快速测试和验证一些想法,而不必编写完整的程序。

除了基本的 JavaScript 语法外,Node.js 的 REPL 还提供了一些额外的功能,例如可以使用下划线 _ 变量来引用最近一次表达式的结果,可以使用.help命令来获取帮助信息,以及可以使用 .exit 或者按下 Ctrl+C 两次来退出 REPL 环境。

Node.js 的 REPL 是一个非常有用的工具,可以帮助开发者快速验证和调试 JavaScript 代码,以及学习和探索新的语言特性和API。它为开发者提供了一个方便的交互式环境,可以在其中进行实时的代码实验和交互式学习。

3.1.1 REPL的基础命令

当你进入 Node.js 的 REPL 环境后,可以使用以下基础命令来进行交互:

  • .help:显示 REPL 环境的帮助信息,其中包含了可用命令的列表和说明。

    ydcq@ydcqdeMac-mini ~ % node
    Welcome to Node.js v18.17.1.
    Type ".help" for more information.
    > .help
    .break    Sometimes you get stuck, this gets you out
    .clear    Alias for .break
    .editor   Enter editor mode
    .exit     Exit the REPL
    .help     Print this help message
    .load     Load JS from a file into the REPL session
    .save     Save all evaluated commands in this REPL session to a file
    
    Press Ctrl+C to abort current expression, Ctrl+D to exit the REPL
    > 
    
    
  • .break(或按下两次 Ctrl+C ):如果你输入了一个多行代码并且想要取消输入,可以使用 .break 命令。

    > a = [1, 2, 3];
    [ 1, 2, 3 ]
    > a.forEach(function(x) {
    ... console.log(x);
    ... }
    ... .break
    > 
    

    注意:如果是一段代码没有写分号 ; 或者是一段表达式没有写完就会发生多行分次输入,直到代码或者表达式写完。

  • .clear:清除 REPL 环境中的所有内容,将其重置为初始状态。

    注意:通过 .help 我们可以看到 .clear 的解释是Alias for .break; 它是 .break 的别名;意思就是它的作用和 .break 命令是一样的。

  • .save [filename]:将当前 REPL 会话中输入的所有代码保存到指定的文件中。

    > a
    [ 1, 2, 3 ]
    > msg = "hello";
    > .save /Users/ydcq/Desktop/web/node_demo/demo.js
    Session saved to: /Users/ydcq/Desktop/web/node_demo/demo.js
    > 
    

    如果保存路径正确,我们就可以看到已经保存的文件了。

    图1 .save命令

  • .load [filename]:从指定的文件中加载 JavaScript 代码并执行。

    图2 .load命令

  • .exit(或按下两次Ctrl + C):退出 Node.js 的 REPL 环境。

    图3 .exit命令

  • .editor:进入编辑器模式,以便你可以多行输入和编辑代码。按下 Ctrl+D 或者输入 .exit 退出编辑器模式并执行代码。

    图4 .editor命令

  • tab :当在空白行按下时,显示全局和本地作用域内的变量。当在输入时按下,显示相关的自动补全选项。

    图5 tab补全

3.1.2 REPL中使用下划线 _

在 Node.js 的 REPL 环境中,下划线符号 _ 代表最近一次表达式的结果。当你在 REPL 中输入一个表达式并按下回车键后,Node.js 会显示表达式的结果,并将该结果存储到下划线 _ 变量中。你可以在随后的表达式中使用下划线 _ 变量来引用先前的结果,而不必重新计算或重新输入相同的代码。

例如,假设你在 REPL 中输入以下两个表达式:

> 2 + 2
4

此时,Node.js 会将表达式 2 + 2 的结果存储到下划线 _ 变量中,并将结果打印到屏幕上。现在,如果你想使用先前的结果来执行另一个操作,例如乘以2,你可以输入以下表达式:

> _ * 2
8

这将使用下划线 _ 变量引用先前的结果,并将其乘以2。这样,你可以在 REPL 环境中轻松地构建和测试复杂的表达式,同时避免重复输入相同的代码或手动存储中间结果。

注意:下划线 `_` 变量只存储最近一个表达式的结果。如果你在 REPL 中输入多个表达式,每个表达式的结果都会覆盖下划线 `_` 变量中的内容。因此,如果你需要在后续操作中引用多个结果,最好将它们存储在具有描述性名称的变量中。

这些基础命令可以帮助你控制和管理 Node.js 的 REPL 环境。你可以使用 .help 命令获取更多详细的信息,并根据需要进行实际操作。此外,你还可以使用常规的 JavaScript 语法和命令,如变量赋值、函数调用等,以及访问全局对象和已加载的模块。REPL 环境允许你以交互方式编写和测试代码,非常适合快速验证想法和进行实验。

3.2 加载Node.js脚本

在 Node.js 环境中,可以通过 node 文件路径 加载和执行 JavaScript 脚本文件。例如我们创建一个 node_demo 文件夹,进入该文件夹,再创建一个 helloworld.js 文件,用文本编辑器打开,并输入 console.log("Hello World!"); 这段代码保存并退出。

在 终端/cmd 中输入 node helloworld.js 并按回车键,终端/cmd 将会输出 Hello World!

ydcq@ydcqdeMac-mini node_demo % node helloworld.js
Hello Word!

3.3 VSCode

Visual Studio Code(简称 VSCode)是一个免费开源的跨平台代码编辑器,由 Microsoft 开发和维护。它提供了丰富的功能和扩展性,适用于各种编程语言和开发场景。VSCode 还内置了智能代码补全、代码导航和代码重构等功能,可以大大提高开发效率。

3.3.1 VSCode安装

  1. VSCode 官网 ,根据自己的操作系统下载对应的安装包。

    图6 VSCode官网

  2. 双击下载好的安装包并勾选同意按钮,点击下一步进行安装。

    图7 VSCode安装

  3. 根据自己的喜好设置安装到磁盘的位置。

    图8 VSCode选择磁盘位置

  4. 勾选对应的选项点击下一步继续安装等待安装。

    图9 VSCode安装继续

  5. 最后点击完成。

    图10 VSCode完成安装

安装完成后的界面。

图11 VSCode安装完成展示

3.3.2 设置简体中文

我们发现刚才安装的 VSCode 还是英文的,接下来我们就要设置它的中文界面。

点击下面的图标,然后输入 chinese,选择第一个,然后点击 install 进行安装。

图12 简体中文插件

完成后根据提示重启 VSCode 就能显示中文了,接下来就可以在VSCode里面进行编程了。

图13 简体中文界面

3.4 在线编程工具

我们也可以使用云端源想在线编程工具去运行 Node.js 的项目,在线编程工具已经集成了 Node.js 的环境,可以省去很多安装和配置的步骤,真正做到了开箱即用。下面就让我们来看一看具体的操作吧。

  1. 进入官网首页https://www.ydcode.cn/ ,点击开始进入

    图14 云端源想官网首页

  2. 在课程首页点击在线编程

    图15 点击在线编程

  3. 开始创建新的项目

    图16 新建项目

  4. 完成项目创建,我们只使用 Node.js 环境,选择Vue项目足够我们使用了。

    图17 完成创建项目

  5. 打开创建的项目,等待项目加载并正常打开。

    图18 打开创建的项目

  6. 我们在在线编程下面的终端输入 node -vnpm -v,我们可以看到已经能够正常输出版本号了,是不是很方便。

    图19 运行node命令

  7. 我们创建一个 demo.js,并在里面填入 console.log("Hello World!"); 这段代码。

    图20 新建文件

    图21 创建demo.js

    图22 编写代码

  8. 然后我们在下面的终端输入 node demo.js,命令。

    图23 运行demo.js

注意:
1. 输入命令请在英文输入法下进行。
2. 在线编程长时间不操作会断开连接,你需要点击左上角返回然后再重新进入项目才能继续操作。

使用云端源想的在线编程工具,我们还可以搭配 智能AI工具 来协助我们进行编程开发等,下面就让我们简单介绍一下吧。

首先先进到智能工具页面,然后我们点击页面上的 智能AI问答

图24 智能AI工具

然后输入我们想要问的问题,稍等片刻,就可以得到问题解答。

图25 AI问答

如果还有一些学习中或者实际开发当中遇到问题,欢迎大家到远端源想的 论坛交流 板块发帖求助。

3.5 调试

Node.js 的调试器使用 Chrome 开发团队开发的 V8 调试协议作为底层协议,可以通过命令行或图形界面进行调试。

3.5.1 命令行调试

node inspect your-script.js

其中 your-script.js 是你要调试的脚本文件名。这将启动 Node.js 的调试器,并暂停脚本的执行,等你输入调试命令。

以下是一些常见的调试命令:

  • c:继续执行程序直到遇到下一个断点或程序结束。

  • n:执行下一行代码。

  • s:进入当前函数。

  • o:跳出当前函数。

  • setBreakpoint():在当前行设置断点。

  • setBreakpoint(line):在指定行设置断点。

  • setBreakpoint(‘fn()’'):在指定的fn方法设置断点。

  • setBreakpoint(filename, line):在filename文件的line行设置断点。

  • clearBreakpoint(breakpointId):清除指定 ID 的断点。

  • listBreakpoints():列出所有设置的断点。

  • repl:进入 REPL 模式,在该模式下可以查看和修改变量的值。

  • watch(‘variable’):监视一个变量的值。

  • unwatch(‘variable’):取消监视变量。

  • kill:终止当前执行的脚本。

你还可以使用 Ctrl+C 两次来退出调试器。

例如,假设你有一个名为 demo.js 的脚本文件:

function add(a, b) {
  return a + b;
}

const result = add(1, 2);
console.log(result);

如果你想要在 add 函数内进行调试,可以在代码中添加一个断点:

function add(a, b) {
  debugger;
  return a + b;
}

然后使用以下命令启动调试器:

node inspect demo.js

这将会在 debugger; 处暂停程序的执行,等待你的调试命令。在调试器中,你可以使用 c 命令来继续执行程序,或使用 s 命令进入 add 函数内部进行调试。

在实际应用中,你可能需要更多的高级调试功能,例如条件断点、捕获异常、远程调试等,Node.js 调试器还提供了这些功能,你可以查阅 Node.js 文档获取更详细的信息和示例。

3.5.2 VSCode调试

下面我们来讲解一下如何在 VSCode 中开始代码调试。

  1. 使用 VSCode 打开你的项目,然后点击红色框出来的调试按钮。

    图26 点击调试按钮

  2. 点击创建 launch.json 文件。

    图27 项目创建launch.json

  3. 点击选择 Node.js。

    图28 选择Node.js

  4. 接着就创建好了 launch.json 文件。

    图29 launch.json

  5. 点击调试按钮,并在代码的行号前面点击标出断点。

    图30 标注断点开始调试

  6. 点击运行程序,代码就会停在有断点的地方,现在你就可以进行调试了。

    图31 点击启动程序开始调试

总结

这一节我们讲解了 Node 的 REPL 运行环境,以及如何在 REPL 环境中编写运行代码等,还讲解了其他几种代码编写和运行工具的使用等。根据你的喜好选择适合自己的方式可以大大提高编写效率。

4-全局对象

4.1 全局对象

Node.js 中有一些全局对象,在任何模块中都可以访问它们,而无需导入或引入。在 JavaScript 中 window 是全局对象,而 Node.js 中的全局对象是 global,除了 global,其他的全局对象都是 global 的属性。通过使用全局对象,你可以轻松地访问和操作各种 Node.js 运行时环境中提供的功能和信息。

下面是一些常见的 Node.js 全局对象:

  1. global:global 是 Node.js 的全局对象,类似于浏览器环境中的window对象。你可以在任何模块中使用global来定义全局变量。但是,在编写 Node.js 应用程序时,最好避免在全局范围内定义太多全局变量。

  2. process:process 对象提供了有关当前 Node.js 进程的信息,并允许与进程进行交互。例如,你可以使用 process.argv 来获取命令行参数,使用 process.env 来访问环境变量,使用 process.exit() 来退出当前进程等。

  3. console:console 对象是一个全局对象,提供了一组用于将输出消息写入控制台的方法,如前面所述。

  4. require:require 函数是一个用于加载模块的全局函数。通过require函数,你可以在一个模块中引入其他模块,以便在当前模块中使用其导出的功能。

  5. module:module 对象代表当前模块,在每个模块中都是唯一的。它包含有关当前模块的信息和状态,并且可以使用 module.exports 导出模块的功能。

  6. exports:exports 对象是 module.exports 的一个引用,它被用于导出当前模块的功能。你可以将要导出的内容添加到 exports 对象上,以便其他模块可以使用 require 函数引入。

4.1.1 console

Node.js 中的 console 是一个全局对象,提供了一组用于将输出消息写入标准输出流 (stdout) 或标准错误流 (stderr) 的方法。你可以使用这些方法在命令行窗口、终端或日志文件中记录调试信息、警告和错误等信息。

console.log方法

console.log() 是 Node.js 中用于向标准输出流 (stdout) 输出消息的方法。它是 console对象提供的一个常用方法。

console.log() 可以接受一个或多个参数,并将它们打印到控制台。每个参数将使用空格分隔,并以换行符结尾。下面是一些使用 console.log() 的示例:

console.log('Hello, World!'); // 输出: Hello, World!

const name = 'Alice';
const age = 25;
console.log('Name:', name, 'Age:', age); // 输出: Name: Alice Age: 25

const fruits = ['apple', 'banana', 'orange'];
console.log('Fruits:', fruits); // 输出: Fruits: [ 'apple', 'banana', 'orange' ]

注意:console.log() 还支持像C语言的print函数的 %s%d 的占位符。

const name = 'Alice';
const age = 25;
console.log('Name:%s Age:%d', name, age); // 输出: Name: Alice Age: 25

console.error方法

console.error() 是 Node.js 中用于向标准错误流 (stderr) 输出错误消息的方法。它是 console 对象提供的一个常用方法。

console.log() 不同,console.error() 将消息写入标准错误流,并以红色字体显示,以突出显示错误性质。这样可以更容易地区分错误消息和普通消息。

console.error() 的用法与 console.log() 类似,可以接受一个或多个参数,并将它们打印到控制台。每个参数将使用空格分隔,并以换行符结尾。下面是一些使用 console.error() 的示例:

const error = new Error('Something went wrong!');
console.error(error); // 输出 Error: Something went wrong!

const name = 'Alice';
const age = 25;
console.error('Name:', name, 'Age:', age); // 输出 Error: Name: Alice Age: 25

console.trace方法

console.trace() 是 Node.js 中的一个方法,用于在控制台输出当前函数的调用堆栈(stack trace)。它是 console 对象提供的一个常用方法。

当你调用 console.trace() 时,它会打印当前函数或代码段的调用堆栈信息,显示函数的调用关系和位置。这对于调试和追踪代码执行路径非常有用,特别是在定位错误和查找代码路径时。

下面是使用 console.trace() 的示例:

function foo() {
  bar();
}

function bar() {
  baz();
}

function baz() {
  console.trace();
}

foo();

运行上述代码将输出以下内容:

Trace
    at baz (/Users/ydcq/Desktop/web/node_demo/demo.js:10:11)
    at bar (/Users/ydcq/Desktop/web/node_demo/demo.js:6:3)
    at foo (/Users/ydcq/Desktop/web/node_demo/demo.js:2:3)
    at Object.<anonymous> (/Users/ydcq/Desktop/web/node_demo/demo.js:13:1)
    at Module._compile (node:internal/modules/cjs/loader:1256:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Module.load (node:internal/modules/cjs/loader:1119:32)
    at Module._load (node:internal/modules/cjs/loader:960:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:23:47

这些信息的每一行都代表调用堆栈中的一个函数,并显示了函数的名称、文件名、行号和列号。从上到下,可以看到函数的调用顺序,最后是当前文件的入口点。

console.trace() 在调试和排查问题时非常有用,可以帮助你了解代码是如何被调用的,以及它们之间的关系。通过追踪函数的调用路径,你可以更好地理解程序的执行流程,并找出可能的问题所在。

console.time 方法和 console.timeEnd 方法

console.time()console.timeEnd() 是Node.js中用于测量代码执行时间的方法,它们是console对象提供的一对常用方法。

console.time(label) 用于开始计时器,并将其与一个标签 (label) 相关联。标签可以是任何字符串,用于标识计时器。你可以同时启动多个计时器,只需为每个计时器使用不同的标签。

console.timeEnd(label) 用于停止计时器,并输出经过的时间。它会根据与计时器关联的标签 (label) 找到相应的计时器,并计算出从 console.time()console.timeEnd() 之间经过的时间。

下面是使用 console.time()console.timeEnd() 的示例:

console.time('myTimer'); // 启动计时器

// 执行一些耗时操作
for (let i = 0; i < 1000000; i++) {
  // do something
}

console.timeEnd('myTimer'); // 停止计时器并输出时间

在这个示例中,我们使用 console.time('myTimer') 开始计时器,然后执行一些耗时操作,最后使用 console.timeEnd('myTimer') 停止计时器并输出经过的时间。输出显示计时器标签和经过的时间(以毫秒为单位)。

通过使用 console.time()console.timeEnd(),你可以测量代码中特定部分的执行时间,以便更好地了解程序的性能和优化需求。这在优化代码、比较不同实现方式的性能等方面非常有用。

4.1.2 process

process 是 Node.js 中的一个全局对象,它提供了与当前 Node.js 进程相关的信息和控制能力,包括进程环境、命令行参数、标准输入输出、事件通知等。你可以使用 process 对象来访问这些信息和功能。

下面是一些常用的 process 对象属性和方法:

  • process.argv:包含命令行参数的数组,第一个元素是 Node.js 执行程序的路径,第二个元素是被执行的 JavaScript 文件的路径,后续元素是传递给程序的命令行参数。
  • process.env:包含进程环境变量的对象。
  • process.stdin:标准输入流的可读流对象。
  • process.stdout:标准输出流的可写流对象。
  • process.stderr:标准错误流的可写流对象。
  • process.cwd():返回当前工作目录的路径。
  • process.exit([exitcode]):结束进程并返回指定的退出码(exitcode),默认为0。

除了上述方法和属性外,process 对象还提供了许多其他有用的功能,如事件通知机制、信号处理、进程崩溃检测等。

下面是一个使用 process 对象的示例:

// 打印命令行参数
console.log('命令行参数:', process.argv);

// 打印环境变量
console.log('环境变量:', process.env);

// 监听SIGINT事件,在接收到Ctrl+C时打印信息并退出
process.on('SIGINT', () => {
  console.log('接收到SIGINT信号,即将退出');
  process.exit(0);
});

运行上述代码将输出命令行参数和环境变量信息,并监听 SIGINT 事件。在接收到 SIGINT 信号(例如按下Ctrl+C)时,程序将打印消息并使用 process.exit() 退出进程。

process 对象提供了许多有用的功能,可以帮助你更好地控制和管理 Node.js 进程。使用它,你可以访问进程相关的信息、事件和功能,并对其进行操作和控制。

4.2 全局函数

Node.js 中还定义了一些全局函数,接下来我们将介绍这些全局函数。

4.2.1 setTimeout 函数和 clearTimeout 函数

当在 Node.js 中使用 JavaScript 编写异步代码时,可能会经常用到 setTimeoutclearTimeout 函数。这两个函数用于处理定时器相关的操作。

setTimeout 函数用于在一定的时间延迟后执行指定的任务。它接受两个参数:要执行的任务(可以是函数或字符串)和延迟的时间(以毫秒为单位)。下面是一个示例:

setTimeout(() => {
  console.log('定时器已触发');
}, 3000);

上述代码将在3秒后输出 “定时器已触发”。

clearTimeout 函数用于取消之前通过 setTimeout 创建的定时器。它接受一个参数,即要取消的定时器对象。下面是一个示例:

const timer = setTimeout(() => {
  console.log('这个定时器将被取消');
}, 2000);

clearTimeout(timer);

上述代码中,通过 setTimeout 创建了一个定时器,并将其赋值给了变量 timer。然后在2秒之前,通过 clearTimeout 取消了这个定时器,因此任务不会被执行。

需要注意的是,`setTimeout` 函数返回一个定时器对象,可以通过这个对象来取消定时器。如果不取消定时器,定时器会在延迟时间到达后自动执行任务。

4.2.2 setInterval 函数和 clearInterval 函数

setIntervalclearInterval 是两个用于处理定时器的函数。setInterval 函数用于在指定的时间间隔内重复执行指定的任务。它接受两个参数:要执行的任务(可以是函数或字符串)和重复执行任务的时间间隔(以毫秒为单位)。

下面是 setInterval 一个示例:

setInterval(() => {
  console.log('这个任务将每隔2秒钟执行一次');
}, 2000);

上述代码将每隔2秒输出一次"这个任务将每隔2秒钟执行一次"。

clearInterval 函数用于取消之前通过 setInterval 创建的定时器。它接受一个参数,即要取消的定时器对象。下面是一个示例:

const timer = setInterval(() => {
  console.log('这个定时器将被取消');
}, 2000);

clearInterval(timer);

上述代码中,通过 setInterval 创建了一个定时器,并将其赋值给了变量 timer。然后通过 clearInterval 取消了这个定时器,因此任务不会继续执行。

需要注意的是,`setInterval` 函数返回一个定时器对象,可以通过这个对象来取消定时器。如果不取消定时器,定时器会在每个时间间隔到达后自动执行任务(会一直执行这个任务)。

4.2.3 定时器的ref函数和unref函数

在 Node.js 的定时器中,ref 函数和 unref 函数是用于控制定时器是否参与事件循环的函数。

ref 函数用于保持定时器活跃,使其参与事件循环。默认情况下,定时器是处于活跃状态的,即参与事件循环。但是,在某些情况下,我们可能希望临时暂停定时器,不让其参与事件循环,这时可以使用 unref 函数。下面是一个示例:

const timer = setInterval(() => {
  console.log('定时器任务');
}, 1000);

timer.ref(); // 将定时器设置为活跃状态

// 5秒后暂停定时器
setTimeout(() => {
  timer.unref(); // 暂停定时器,不参与事件循环
}, 5000);

上述代码中,通过 setInterval 创建了一个每秒执行一次的定时器。然后使用 timer.ref() 将定时器设置为活跃状态,确保它参与事件循环。在5秒后,使用 timer.unref() 将定时器暂停,不让其参与事件循环。

需要注意的是,refunref 函数只对 setIntervalsetTimeout 创建的定时器有效。对于其他类型的定时器,如 process.nextTicksetImmediate 创建的定时器,这些函数没有作用。

总结

通过使用这些全局对象,可以轻松地访问和操作各种 Node.js 运行时环境中提供的功能和信息。请注意,在编写 Node.js 应用程序时,尽量避免滥用全局对象,以维护代码的可读性和可维护性。

5-CommonJS

CommonJS 是一个 JavaScript 模块化规范,主要用于服务器端 JavaScript 编程。它提供了一套简单的API来定义和加载模块,使得 JavaScript 代码能够按照模块化的方式进行组织。

5.1 CommonJS规范

CommonJS 规范包括模块、包、系统、二进制、控制台、编码、文件系统、套接字单元测试等。Node.js 是 CommonJS 规范的一个实现,它采用了 CommonJS 规范作为其模块系统的基础。在 Node.js 中,每个 JavaScript 文件被视为一个单独的模块,可以通过 require 函数引入其他模块,同时使用 module.exportsexports 对象来导出模块的接口。

在 Node.js 中,当模块被第一次引入时,Node.js 会执行该模块的代码,并缓存模块的导出接口,之后再次引入相同的模块时,将直接返回已缓存的导出接口,而不会重新执行模块的代码。

5.2 模块化有什么作用?

  1. 代码组织和模块化:通过模块化可以将代码分解为相互独立的功能单元,每个模块负责实现特定的功能。这样可以使代码更具可读性和可维护性,便于开发者理解和修改代码。

  2. 依赖管理:在实际开发中,一个项目通常会依赖于大量的第三方库或框架,模块化可以帮助管理这些依赖关系,避免代码混乱和冲突。

  3. 代码复用:通过模块化可以将常用的功能封装为模块,然后在多个地方进行引用和复用,避免重复编写相似的代码。

  4. 性能优化:模块化还可以帮助提高应用程序的性能,因为模块可以被缓存,避免重复加载和执行。

  5. 作用域隔离:每个模块都拥有自己的作用域,可以避免全局变量污染和命名冲突。

Node.js 需要模块来帮助开发者更好地组织和管理代码,提高代码的可维护性和可复用性,同时也提高了应用程序的性能和可扩展性。

5.3 CommonJS的模块规范

Node.js 遵循 CommonJS 规范,它定义了一套模块化开发的标准,使得 JavaScript 代码可以被组织成独立的、可复用的模块。

CommonJS 是 Node.js 最早采用并广泛使用的模块规范,它的主要特点是同步加载模块。

CommonJS 规范对模块的定义主要包含以下三个部分:

模块引用(require):使用 require 函数加载模块。在 Node.js 中,每个文件都被视为一个独立的模块,通过 require 函数可以加载其他模块的代码。

模块定义(exportsmodule.exports):使用 exportsmodule.exports 对象暴露模块的接口。exports 对象是对 module.exports 的简单封装,用于导出模块的接口。

模块标识(module):每个模块都有一个 module 对象,它表示当前模块本身,包含模块的文件名、模块的导出对象等信息。

图1 模块的定义

下面是一个简单的示例,演示了如何使用 CommonJS 规范进行模块化开发:

hello.js

// 模块定义
const greeting = 'Hello,';

function sayHello(name) {
  console.log(greeting + name + '!');
}

// 暴露模块的接口
module.exports = {
  sayHello,
};

main.js

// 模块引用
const hello = require('./hello');

// 使用模块的接口
hello.sayHello('world'); // 输出: Hello,world

在上面的示例中,我们定义了一个 sayHello 函数,并将其作为模块的接口暴露出去。然后在另一个文件中,使用 require 函数加载该模块,并使用模块的接口调用 sayHello 函数。

总结起来,CommonJS 规范是 Node.js 中用于实现模块化开发的标准,它定义了模块的引用、定义和标识方式。这种模块化的机制使得 Node.js 具有良好的可扩展性和灵活性,便于开发大型和复杂的应用程序。

5.4 其他规范

当谈到 Node.js 的多种模块规范时,我们可以详细了解 CommonJS、ES6 Modules、CMD 和 AMD。CommonJS 是为了后端 JavaScript 而制定的规范,它并不适用于前端的使用场景。因此就诞生了 AMD 规范(Asynchronous Module Definition:异步模块定义),AMD 规范还有 CMD 规范(Common Module Definition:通用模块定义)。

这些规范对于模块的定义、导入和导出方式都有不同的特点和语法。让我们逐一来介绍它们:

5.4.1 AMD规范

AMD 是用于浏览器端 JavaScript 模块化的一种规范,允许异步加载模块,有利于提高应用程序的性能。

导出模块:

define(['dependency1', 'dependency2'], function(dep1, dep2) {
  // 模块内容
  return {
    func1: function() { /*...*/ },
    func2: function() { /*...*/ }
  };
});

导入模块:

require(['module1', 'module2'], function(m1, m2) {
  // 使用模块
});

5.4.2 CMD规范

CMD 是另一种用于浏览器端的模块化规范,它与 AMD 类似,但更加强调模块的延迟执行和依赖就近原则。

导出模块:

define(function(require, exports, module) {
  // 模块内容
  module.exports = {
    func1: function() { /*...*/ },
    func2: function() { /*...*/ }
  };
});

导入模块:

define(function(require, exports, module) {
  var dep1 = require('dependency1');
  var dep2 = require('dependency2');
  // 模块内容
  module.exports = {
    func1: function() { /*...*/ },
    func2: function() { /*...*/ }
  };
});

5.4.3 ESM规范

ESM 是 ECMAScript 2015 引入的官方 JavaScript 模块化规范,用于浏览器端和 Node.js。它提供了静态导入和导出的能力。

导出模块:

export default myFunction;

export { func1, func2 };

导入模块:

import moduleName from 'path/to/module';
import { func1, func2 } from 'path/to/module';

ESM 的语法比较简洁,且与其他规范有一些差异。ESM 还在不断发展中,但已经成为了现代 JavaScript 开发中的主流模块化规范。

需要注意的是,AMDCMD 主要用于浏览器端的模块化开发,而 CommonJSESM 则更多地用于 Node.js 和现代浏览器环境。在实际应用中,可以根据项目需求和开发环境选择合适的规范。同时,也可以使用工具(如 RequireJS、SystemJS 和 Babel)来实现不同规范之间的兼容和转换。

总结

CommonJS 规范定义了模块化的机制和规则,通过导入和导出模块的方式实现代码的组织、复用和共享。它具有简单、易用的特点,并且适用于服务器端的 JavaScript 开发。不同的规范提供了不同的特性和优势,开发者可以根据项目需求选择合适的模块化规范。

6-模块

通过模块系统,Node.js 实现了代码的模块化,它允许开发者将代码分割成小的、独立的模块,以提高代码的可维护性,使得代码更加清晰、并提供了良好的封装和复用性。

6.1 Node.js的模块实现

Node.js 的模块实现是基于 CommonJS 规范的,它提供了一种简单而有效的方式来组织、加载和复用代码。Node.js 的模块系统遵循以下几个重要原则:

  1. 每个文件都被视为一个独立的模块 (一个模块可以是一个 JavaScript 文件,也可以是一个JSON文件等。),每个模块都有自己的作用域。模块中定义的变量、函数和类默认情况下是私有的,只能在模块内部使用。

  2. 使用 module.exportsexports 对象将接口暴露给其他模块使用。这些导出的接口可以是变量、函数、类或对象。

  3. 使用 require() 函数加载其他模块。require() 函数接受一个模块标识符作为参数,并返回该模块的导出对象。

下面详细介绍 Node.js 的模块实现:

6.1.1 模块加载

当调用 require() 函数加载一个模块时,Node.js 会执行以下步骤:

  1. 解析模块路径:根据传入的模块标识符,Node.js 会解析出模块的绝对路径。模块标识符可以是相对路径(以./或…/开头)或者是一个模块名(如’http’或’lodash’)。

  2. 缓存检查:Node.js 会检查模块是否已经被缓存,如果已经被缓存,则直接返回缓存的模块对象,避免重复加载和解析。

  3. 模块编译:如果模块没有被缓存,则进行模块编译。Node.js 会根据模块的文件类型(.js.json.node)使用适当的编译器对模块进行编译,并生成一个模块对象。

  4. 模块包装:在模块编译完成后,Node.js 会将模块的代码包装在一个函数中,该函数接受五个参数:requiremoduleexports__filename__dirname

  5. 模块执行:包装函数被调用,模块的代码开始执行。在模块内部,可以通过 module.exportsexports 对象将接口暴露给其他模块使用。

  6. 模块缓存:模块的导出对象被缓存起来,以便下次加载时直接返回缓存的对象。

6.1.2 __filename和__dirname

__filename__dirname 是两个特殊的全局变量,用于获取当前模块文件的完整路径和所在目录的完整路径。

  • __filename

__filename 表示当前模块文件的完整路径,包括文件名。这个路径是绝对路径,可以通过它获取到当前模块文件的位置。

console.log(__filename);
// 输出:/Users/ydcq/Desktop/web/node_demo/demo.js
  • __dirname

__dirname 表示当前模块文件所在目录的完整路径。与 __filename 不同,__dirname 表示的是所在目录的路径,不包括文件名。

console.log(__dirname);
// 输出:/Users/ydcq/Desktop/web/node_demo

这两个全局变量经常用于构建文件路径或者加载其他模块时需要使用的路径信息。在模块化开发中,通常会用到这两个变量来构造文件的绝对路径,以便正确地加载模块或读取文件。

6.1.3 模块导出

模块中的接口可以通过 module.exportsexports 对象进行导出。

module.exports 是对 exports 对象的引用,它是导出的主要方式。可以将任何类型的值赋给 module.exports,例如一个对象、一个函数、一个类或一个原始数据类型。

exportsmodule.exports 的一个简便方式,它是一个空对象。可以通过给 exports 对象添加属性和方法来导出接口。

例如,在一个名为 foo.js 的模块中,可以定义一个私有变量和一个导出函数:

// 私有变量
var privateVariable = 'This is a private variable';

// 导出函数
exports.sayHello = function() {
  console.log('Hello, world!');
};

在另一个模块中,可以通过 require() 函数加载并使用 foo.js 模块的导出接口:

var foo = require('./foo');
foo.sayHello(); // 输出 "Hello, world!"

6.1.4 模块引入

在一个模块中,可以通过 require() 函数加载其他模块,并使用其导出接口。

require() 函数接受一个模块标识符作为参数,该标识符可以是相对路径或者是一个模块名。如果是相对路径,则表示加载当前目录下的模块文件;如果是模块名,则表示加载安装在 node_modules 目录下的模块。

例如,在一个名为 bar.js 的模块中,可以引入 foo.js 模块并调用其导出函数:

var foo = require('./foo');
foo.sayHello(); // 输出 "Hello, world!"

这里,我们使用相对路径 ./foo 来引入 foo.js 模块,并将其返回值保存在 foo 变量中。然后,我们调用了 foo 模块的 sayHello() 函数来输出一条消息。

6.1.5 模块缓存

Node.js 会对引入的模块进行缓存,以避免重复加载和解析模块。当第一次引入一个模块时, Node.js 会将该模块的导出对象缓存起来。当再次引入该模块时,Node.js 会直接返回缓存的导出对象,而不会重新加载和解析该模块。

例如,在一个名为 qux.js 的模块中,可以多次引入同一个模块:

var foo1 = require('./foo');
var foo2 = require('./foo');
console.log(foo1 === foo2); // 输出 true

这里,我们在两个不同的变量中引入了 foo.js 模块,然后检查它们是否相等。由于模块缓存的存在,这两个变量的值应该是相等的。

需要注意的是,如果一个模块被多个模块引入,则它的导出对象的改变会影响所有引入该模块的模块。因此,在编写模块时,应该避免修改导出对象的属性和方法。

6.1.6 自定义模块解析策略

在 Node.js 中,可以通过自定义模块解析策略来支持更复杂的模块依赖关系。例如,可以将模块路径映射到其他位置,或者根据环境变量来动态决定使用哪个模块。

要自定义模块解析策略,可以使用 require.resolve() 函数来获取模块的路径,然后修改该路径来实现自定义解析。例如,在一个名为 quux.js 的模块中,可以使用环境变量来决定使用哪个模块:

var moduleName = process.env.MODULE_NAME || './foo';
var modulePath = require.resolve(moduleName);
var module = require(modulePath);
module.sayHello(); // 输出 "Hello, world!"

这里,我们通过 process.env.MODULE_NAME 环境变量来决定使用哪个模块,默认情况下使用 ./foo 模块。然后,我们使用 require.resolve() 函数来获取该模块的路径,并将其返回值保存在 modulePath 变量中。最后,我们使用 require() 函数来加载该模块,并调用其导出函数来输出一条消息。

6.1.7 模块循环依赖

在 Node.js 的模块系统中,当存在循环依赖时,Node.js 会解决模块加载的问题。

循环依赖是指两个或多个模块之间相互引用,形成一个闭环的依赖关系。在这种情况下,Node.js 会返回已经部分加载的模块对象,并将其缓存起来。当循环依赖的模块被完全加载后,引用关系会得到解决,模块对象中的导出接口会被填充。

例如,假设存在两个模块 a.jsb.js,并且它们彼此相互引用:

a.js

var b = require('./b');
console.log('a:', b.variable);
exports.variable = 'This is a';

b.js

var a = require('./a');
console.log('b:', a.variable);
exports.variable = 'This is b';

当我们加载 a.js 模块时,a.js 会尝试加载 b.js 模块。然而,由于 b.js 又引用了 a.js 模块,Node.js会返回 a.js 的部分加载结果(即 module.exportsexports 对象),并将其缓存起来。然后,b.js 继续加载完成,并填充 a.js 模块中的导出接口。

在这种情况下,我们可以看到 a.jsb.js 模块互相引用了彼此的导出接口,但由于模块缓存的存在,它们不会陷入无限循环的加载过程。

Node.js 的模块实现基于 CommonJS 规范,并采用简单而灵活的方式来组织、加载和复用代码。模块可以通过 module.exportsexports 对象进行导出,通过 require() 函数进行引入。当存在循环依赖时,Node.js 会解决模块加载的问题并保证正确的导出和缓存。

6.2 模块对象的属性

在 Node.js 中,每个模块都有一个特殊的模块对象 module,它包含了当前模块的信息和属性。以下是 module 对象的一些常用属性:

  1. id:模块的标识符,通常是模块的绝对路径。
  2. exports:模块的导出接口,可以通过修改该属性来导出模块的功能。
  3. filename:模块的文件名,即模块的绝对路径。
  4. loaded:模块是否已加载完成的标志,值为true或false。
  5. parent:引用当前模块的模块对象。
  6. children:当前模块引用的其他模块对象的数组。

这些属性可以通过在模块中直接访问 module 对象来获取。例如,在一个模块中可以使用 module.id 来获取当前模块的标识符,使用 module.exports 来导出模块的功能。以下是一个示例:

// 模块定义
console.log('module id:', module.id);
console.log('module filename:', module.filename);

// 修改导出接口
module.exports = {
  foo: 'bar'
};

在上述示例中,我们首先输出了模块的标识符和文件名。然后,通过修改 module.exports 属性,将一个对象导出为模块的功能。

需要注意的是,`module` 对象是模块内部的一个局部对象,只能在模块内部使用。它提供了一些有用的属性和方法来操作当前模块的信息和导出接口。

除了以上常用属性,module 对象还具有一些其他的属性和方法:

  1. require(id):加载和返回指定模块的导出接口。如果模块已经缓存,则直接返回缓存的导出接口,否则将加载并执行模块,并将其导出接口返回。
  2. paths:一个数组,表示 Node.js 在查找模块时要搜索的路径列表。
  3. builtinModules:一个数组,包含了 Node.js 内置模块的名称。
  4. wrapper:一个字符串,表示模块代码的包装函数,默认值为 (function (exports, require, module, __filename, __dirname) { })。

6.3 核心模块

Node.js 的核心模块是一组原生的模块,提供了许多基本的功能和工具,可以在 Node.js 环境中直接使用,无需额外安装。以下是一些常用的 Node.js 核心模块及其功能:

  • fs(File System)模块:用于处理文件系统操作,包括读取、写入、修改、删除文件等。

  • http 模块:用于创建 HTTP 服务器和客户端,可以实现 Web 服务器的功能,处理 HTTP 请求和响应。

  • path 模块:用于处理文件路径,提供了一些方法来解析、拼接、规范化文件路径等。

  • os(Operating System)模块:用于获取操作系统相关的信息,如 CPU 架构、内存使用情况、操作系统平台等。

  • events 模块:用于实现事件驱动编程,提供了一些类和方法来处理事件的注册、触发和监听。

  • util 模块:提供了一些实用函数,包括继承、类型判断、错误处理等常用的工具函数。

  • crypto 模块:用于提供加密和解密功能,包括哈希、加密、解密、签名等安全相关的操作。

  • stream 模块:用于处理流式数据,可以实现数据的读取、写入和转换等操作,特别适用于大型数据的处理。

  • buffer 模块:用于处理二进制数据,提供了一些方法来创建、操作和转换二进制数据。

  • querystring 模块:用于处理 URL 查询字符串,可以解析、序列化和修改查询字符串。

除了以上列举的核心模块,Node.js 还有其他许多模块可供使用,如 child_processnetdnscrypto 等,它们提供了更多丰富的功能和工具,使得开发人员可以根据自己的需求进行更灵活的开发。同时,Node.js 也支持模块的扩展和第三方模块的使用,使得开发人员可以利用社区的资源来解决各种问题。

总结

Node.js 的模块实现基于 CommonJS 规范,并采用简单而灵活的方式来组织、加载和复用代码。模块可以通过 module.exportsexports 对象进行导出,通过 require() 函数进行引入。当存在循环依赖时,Node.js 会解决模块加载的问题并保证正确的导出和缓存。

Node.js 的模块化开发方式可以帮助开发人员有效组织和管理代码,提高代码的可重用性和可维护性。核心模块、第三方模块和自定义模块相互配合,为开发者提供了丰富的功能和工具。模块导入和导出机制以及模块解析规则使得模块之间的依赖关系清晰可见。

7-包和NPM

7.1 包

包(Package)是一种用于组织、管理和共享代码的机制。一个包可以包含多个模块,以及描述包信息的 package.json 文件。Node.js 通过包的机制实现了代码的模块化和复用,使得开发者可以轻松地组织和管理自己的代码,同时也能方便地使用其他人共享的代码。

CommonJS 包规范主要由包结构和包描述文件组成,包其实就是一个归档文件,即一个目录打包成 .zip 或者 .tar.gz 格式的文件。符合 CommonJS 规范的包的结构还应该包括一下几点:

  • package.json:包的描述文件。
  • bin:用于存放二进制可执行文件的目录。
  • lib:用于存放JavaScript代码的目录。
  • doc:用于存放文档的目录。
  • test:用于存放单元测试用例的代码。

注意:为了提高兼容性,建议你在制作包的时候,严格遵守 CommonJS 规范。

7.1.1 文件夹类型的模块

文件和模块一般是一一对应,文件可以是 JavaScript文件、JSON文件、二进制等,还可以是一个文件夹。最简单的包就是文件夹作为一个模块。例如:

新建一个 hello 的文件夹,并在 hello 文件夹下创建一个 index.js 文件,最后写入一下代码:

exports.hello = function() {
	console.log("hello")
}

然后在 hello 的文件夹之外再创建一个 demo.js,写入一下代码:

var pkg = require("./hello");

pkg.hello();

我们运行 node demo.js 就会输出 hello。我们把这种文件夹封装成为一个模块,也就是包。包实际上就是许多模块的集合,类似于 C/C++ 的函数库。通过 package.json,我们就可以创建更复杂、更符合规范的包,并用于发布和下载安装。

7.1.2 package.json

package.json 是 Node.js 项目中的一个重要文件,用于描述项目的元数据信息和配置信息。它以 JSON(JavaScript Object Notation)格式书写,位于项目的根目录下。

{
  "name": "myPackage",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

package.json 文件包含了以下几个常用字段:

  • name:项目的名称,通常为小写字母、连字符或下划线组成的字符串。它在 npm 仓库中唯一标识一个包。

  • version:项目的版本号,通常采用 "主版本号.次版本号.修订号" 的格式,例如:"1.0.0"。版本号的变化反映了项目的迭代更新。

  • description:项目的简短描述,用于介绍项目的特点和功能。

  • main:指定项目的入口文件,即在执行 require() 函数时加载的模块。默认值为 index.js

  • scripts:定义了一组脚本命令,可以在命令行中使用 npm run <script>来执行。常见的脚本命令包括starttest等。例如,可以定义一个start脚本来启动应用程序,如 "start": "node index.js"

  • dependencies:列出了项目的生产环境依赖包及其版本号。当使用 npm install 安装项目时,这些依赖包会自动下载和安装。

  • devDependencies:列出了项目的开发环境依赖包及其版本号。这些依赖包通常用于开发、测试和构建项目,不会在生产环境中使用。

  • keywords:关键字数组,用于描述项目的特点、功能或领域,便于搜索和分类。

  • author:项目的作者信息,可以是个人或组织的名称。

  • license:项目的许可证类型,用于定义其他人使用、修改和分发项目的规则。常见的许可证类型包括MIT、Apache-2.0等。

7.1.3 node_modules 目录

node_modules 目录通常是在 Node.js 项目中出现的一个文件夹,它用于存放项目依赖的第三方模块。当你使用 npm 或者 yarn 等包管理工具安装了某个 Node.js 模块时,这些模块会被下载并存放在 node_modules 目录下。

node_modules 目录中,每个安装的模块通常会有自己的文件夹,里面包含了该模块的代码文件、配置文件和其他必要的资源。这样做的好处是能够保持项目结构的清晰,并且方便地管理和维护依赖关系。

注意:
1. node_modules 目录是用来存放 Node.js 项目依赖的第三方模块的文件夹,它对于管理项目的依赖关系是非常重要的。
2. node_modules 目录非常容易变得庞大,因为项目可能会依赖很多第三方模块。为了避免在版本控制系统中提交过多的第三方模块代码,一般会将 node_modules 目录添加到 .gitignore 文件中,以防止将其提交到代码仓库中。

7.2 NPM包管理工具

NPM(Node Package Manager)是 Node.js 的包管理工具,用于安装、共享以及管理 Node.js 项目中的包依赖关系,你也可以使用 NPM 发布属于你自己的包。以下是关于 NPM 的详细介绍:

7.2.1 安装和使用:

  • NPM 是随同 Node.js 一起安装的,默认已经包含在 Node.js 的安装包中。

  • 可以在命令行中执行各种 NPM 命令,如安装、升级、删除包等。

  • 如果你不熟悉 NPM 命令,可以直接执行 npm 或者是 npm --help 查看帮助引导说明。

7.2.2 包的安装和管理:

  • 使用 npm install package-name 命令可以安装指定的包。

  • 包可以通过 -g 选项进行全局安装,也可以在项目目录下安装本地包。

  • 在项目的根目录下,可以通过 npm init 命令来创建一个新的 Node.js 项目,并生成一个初始的 package.json 文件。

  • package.json 文件中可以列出项目所依赖的其他包及其版本。通过 npm install 命令可以安装项目所依赖的所有包。

  • 可以使用 npm update 命令更新项目中的已安装包到最新版本。npm update package-name可以更新特定的包到最新版本。

  • 通过 npm uninstall package-name 命令可以从项目中删除指定的包。

  • 通过 npm outdated 检查哪些依赖项已经过时。

  • 通过 npm search package-name可以搜索npm仓库中的特定包。

  • npm run package-name 命令可以运行在 package.json 文件中定义的脚本。

  • npm test 命令可以运行在 package.json 文件中定义的测试脚本。

本地安装

对于一些没有发布到 NPM 官方源上的包,或者是一些其他原因无法直接安装的包,可以将这些包下载到本地,然后使用本地安装。本地安装需要为 NPM 指定 package.json 文件所在的位置;这个 package.json 可以是一个带有 package.json 的归档文件,或者是一个URL地址,再或者是一个目录下带有 package.json 文件的目录位置。

# 通过package.json文件路径安装依赖包
npm install <file>
# 通过URL地址安装依赖包
npm install <url>
# 通过文件夹路径安装依赖包
npm install <folder>

非官方源安装

在安装依赖包的时候,还可以通过景象安装的方式进行安装,你只需要在后面加上 --registry=http://xxx 就可以安装非官方源的依赖包了。

npm install xxx --registry=http://xxx

7.2.3 NPM scripts:

  • package.json 文件中的 scripts 字段允许开发者自定义一系列命令,用于在项目中执行各种脚本任务。

  • 这些脚本任务可以通过 npm run script-name 命令来运行。

7.2.4 版本管理:

  • NPM 使用语义化版本控制(Semantic Versioning)来管理包的版本。

  • package.json 文件中,可以通过指定特定的版本范围来管理包的依赖关系。

    "dependencies": {
      "axios": "^1.3.4",
    }
    

7.2.5 搜索和浏览包:

  • 可以使用 npm search package-name 命令搜索 NPM 仓库中的包。

  • 在 NPM 的网站上(https://www.npmjs.com/ )也可以浏览和搜索各种 Node.js 包。

7.2.6 包的发布:

要发布一个 npm 包,你需要按照以下步骤进行操作:

  1. 创建 package.json 文件:在你的项目根目录下,打开终端并运行 npm init 命令。按照提示填写项目的相关信息,比如包名称、版本、描述等,并确认生成的 package.json 文件。

    {
      "name": "node-demo",           // 包名,在NPM服务器上须要保持唯一
      "version": "1.0.0",            // 当前版本号
      "dependencies": {              // 三方包依赖,需要指定包名和版本号
    	"argv": "0.0.2"
      },
      "main": "./lib/main.js",       // 入口模块位置
      "bin" : {
    	"node-demo": "./bin/node-demo"      // 命令行程序名和主模块位置
      }
    }
    
  2. 编写代码和测试:开发你的包,并确保它具备良好的功能和稳定性。同时,你也可以编写一些测试用例,以确保包在各种环境下都能正常工作。

  3. 注册 npm 账号:如果你还没有 npm 账号,需要前往 npm 官网 注册一个账号。

  4. 登录 npm 账号:在终端中运行 npm login 命令,然后输入你在上一步注册的 npm 账号的用户名、密码和邮箱。

  5. 构建包:在项目根目录下,确保你的代码和 package.json 文件都已经准备好。运行 npm publish 命令,npm 将会自动构建并发布你的包到 npm 仓库中。

  6. 验证发布结果:访问 https://www.npmjs.com/package/你的包名称 检查你的包是否成功发布。你也可以运行 npm info 你的包名称 命令来检查包的详细信息。

请注意:发布 npm 包需要遵循一些规范和最佳实践。你可以查阅 npm 文档以获取更详细的发布指南和说明。

总结

NPM 不仅是一个工具,也是一个庞大的开源社区,其中有数以万计的开发者共享和贡献他们的包。通过 NPM,开发者可以方便地使用其他人的包,并且可以将自己的代码分享给全球开发者,促进了 Node.js 生态系统的繁荣和发展。

8-异步I/O

异步其实最先诞生于操作系统的底层,在底层系统中,异步通过信号量、消息等方式有广泛的应用。但在大多数高级编程语言中,异步并不多见,因为异步编程其实并不符合人的思维逻辑。异步其实早就已经诞生了,在Web 2.0的时候随着Ajax就已经在Web中变得非常流行了。前端技术员在Ajax技术产生就已经非常习惯异步应用场景了。

8.1 Node.js为什么需要异步I/O

异步I/O为什么在 Node.js 里如此重要,这其实与 Node.js 的网络设计有关,现在的Web应用已经不是以前的单一的服务器就能够胜任的,跨网并发已经是主流了,然而说到具体就要从下面两点说起:

8.1.1 用户体验

前端Web浏览器的处理UI和响应是处于停滞状态的,用户是不能进行其他任何操作的。如果浏览器的执行事件如果超过100毫秒,用户是会感觉到卡顿现象的,如果时间过长,用户就会认为网页已经停止了响应。只有后端能够足够快的处理响应资源,才能让前端UI和用户的体验变好。这也就是异步I/O为什么在 Node.js 为什么如此盛行。

8.1.2 资源分配

接下来我们从计算机资源分配的层面上来说一下异步 I/O 的重要性。计算机在发展过程中将组件抽象为 I/O 设备和计算机设备。假设有一组任务,现阶段的主流处理方式有以下两个:

  1. 单线程串行依次执行

    同步执行时,就会造成阻塞,必须等待前一个任务结束,才能继续执行下一个任务。当然现在计算机在多核的情况下可以并行完成,但是同步会导致I/O的进行会让后续的任务阻塞等待,这使得计算机的资源不能被有效的利用起来。

  2. 多线程并发执行

    在多线程下,操作系统会CPU的时间片分配给其他的线程继续执行,这也就使得多线程在多核CPU上能够有效的将资源利用起来。多线程并发其实也有一些弊端,多线程在创建和线程切换的时候开销比较大。如果创建线程的开销小如果小于并行执行那么多线程编程仍然是首选。如果在一些复杂的业务中,多线程还会有锁、状态同步等问题产生。

单线程同步编程会因为阻塞I/O导致系统的硬件资源不能被充分利用起来。而多线程也同样面临、锁、状态同步等诸多问题。

Node.js 已经在这两者之间给出了权衡利弊的方案;利用单线程可以避免多线程的锁和状态同步的问题。同时利用异步 I/O 也能让单线程远离阻塞,从而更好的利用系统资源。

为了弥补单线程无法利用多核CPU的问题,Node.js 还提供了类似前端浏览器的 Web Workers 的子进程,它可以充分高效的利用CPU和 I/O 等资源。

8.2 操作系统对异步I/O的支持

异步与非阻塞听起来好像是同一个意思,因为从实际效果而言它们都达到了我们并行 I/O 的目的。但是对于计算机内核 I/O 而言,它们又是另外一回事了。

8.2.1 I/O的阻塞与非阻塞

  • 阻塞 I/O 是指程序在执行 I/O 操作时,会一直等待直到操作完成才返回结果。在阻塞 I/O 的情况下,当一个线程发起 I/O 请求后,它会一直等待直到 I/O 操作完成,期间无法进行其他操作,程序会停在那里,等待 I/O 操作完成。这种方式的缺点是会造成资源浪费,使得系统的并发性能受到影响。

  • 非阻塞 I/O 指程序在执行 I/O 操作时,不会一直等待操作完成才返回结果。在非阻塞 I/O 的情况下,当一个线程发起 I/O 请求后,它会立即返回一个错误代码(比如 EAGAIN、EWOULDBLOCK),告诉调用者 I/O 操作尚未完成,然后该线程可以做其他事情,或者将控制权交给其他线程来处理其他任务。当 I/O 操作完成后,操作系统会通知程序,线程再次去读取数据。这种方式可以充分利用资源,提升系统的并发性能。

8.2.2 I/O的同步与异步

  • 同步I/O 是指程序发起一个 I/O 请求后,需要等待直到请求完成并返回结果,期间程序会一直阻塞。在同步 I/O 的情况下,程序发送一个 I/O 请求后,会一直等待直到数据准备就绪并完成数据传输,期间程序处于阻塞状态,无法执行其他任务。这意味着程序必须等待 I/O 操作完成才能继续执行后续的任务。

  • 异步 I/O 是指程序发起一个 I/O 请求后,不需要等待请求完成,可以立即执行其他任务。在异步 I/O 的情况下,程序发送一个 I/O 请求后,可以立即执行其他任务,不需要等待数据准备就绪或数据传输完成。当数据准备就绪或传输完成后,操作系统会通知程序,并处理相应的事件。

8.3 异步I/O与轮询技术

阻塞 I/O 会造成CPU的等待,而非阻塞 I/O 则会带来需要确认数据是否完全完成了获取的问题 (当进行非阻塞 I/O 操作时,也就需要读到完整的数据,那么程序就需要多次轮询,才能确保完整的读取数据。)。它会让CPU处理状态判断,造成CPU的资源浪费。下面就是现在常用的一些轮询技术:

  • read 是一个阻塞式 I/O 函数,用于从文件描述符中读取数据。当没有数据可读时,read 函数会一直阻塞等待,直到有数据到达或发生错误。如果需要进行非阻塞读取,可以使用 O_NONBLOCK 标志来设置文件描述符为非阻塞模式。它是最原始的,也是性能最低的一种方式,在得到数据以前,CPU资源一直用在了等待上。

  • select 是一种多路复用技术,用于监视多个文件描述符的状态,并在其中任何一个文件描述符准备好进行 I/O 操作时得到通知。通过调用 select 函数,程序可以等待多个文件描述符中的任意一个变为可读或可写状态,从而实现非阻塞式 I/O 操作。但是 select 有性能瓶颈,因为它每次都要遍历所有监视的文件描述符。

  • poll 也是一种多路复用技术,类似于 select,主要不同在于 poll 是基于链表实现的,因此可以处理更多的文件描述符。poll 函数会等待所有指定的文件描述符中任意一个可读、可写或出现错误时返回。pollselect 相比,对于大量文件描述符的情况,poll 性能更优。

  • epollLinux 内核提供的一种 I/O 多路复用机制,是最新的轮询技术。与 select 和 poll 相比,epoll` 可以处理更多的并发连接,并且具有更好的性能、更低的延迟和更少的资源消耗。epoll 通过注册感兴趣的文件描述符来监视 I/O 事件,当事件发生时,系统会触发回调函数,通知程序进行相应的操作。

  • kqueue 是一种在 BSD 系统中广泛使用的高性能 I/O 事件通知机制,它提供了一种可扩展的、高效的事件通知接口,用于监视文件描述符上的事件,并在事件发生时通知应用程序。kqueue 主要用于实现高并发的 I/O 多路复用,特别适用于网络编程和服务器开发。

8.3.1 理想的非阻塞异步I/O

理想的非异步 I/O 是应用层调用异步方法后,直接进行下一个任务处理,然后等数据完整拿到以后再通过回调传递给程序,如下图所示:

图1 理想的异步 I/O

在 Linux 系统下确实存在这种方式,它原生提供了一种异步非阻塞异步 I/O 方式 (AIO)。AIO是通过信号或者回调来传递数据的。只不过仅仅只有 Linux 系统有这么一个方案,而且它还存在缺陷和BUG(AIO仅支持内核I/O中的O_DIRECT方式读取,从而导致应用程序无法利用系统缓存带来的性能优势。)。

8.3.2 其他方式的异步I/O

还另一种异步 I/O 是采用多线程加阻塞 I/O,将 I/O 操作分到多个线程上,从而利用多线程之间的通信将 I/O 获得到的数据进行传递,这样也就实现了异步 I/O。如下图所示:

图2 异步 I/O

8.3.3 不同系统的异步I/O方案

在 Windows 系统中,主要使用 I/O 完成端口(I/O Completion Port)实现异步 I/O。它基于事件驱动的模型,通过将 I/O 操作请求关联到完成端口上,并在操作完成时通知应用程序,实现异步操作。IOCP的异步模型和Node的异步模型十分相似。

在 Linux 系统下采用了 libeio 配合 libve 的实现 I/O 部分,实现了异步 I/O。而在Node v0.9.3中,自行实现了线程池用来完成异步 I/O。

8.3.4 Node.js对于不同系统的兼容

由于各个系统之间对异步 I/O 都有不同的支持和实现,所以 Node.js 提供了 libuv 作为底层封装,这也使得不同的平台兼容性都能由这层来完成,这样保证 Node.js 和底层 libeio/libevIOCP 之间各自独立。Node.js 会在编译期间判断操作系统平台,然后选择性的编译unix目录或是win目录下的源文件到目标程序中。

图3 Node.js对于不同系统的兼容

8.4 Node.js的异步I/O

上面我们介绍了操作系统对异步 I/O 的支持,下面我们将介绍 Node.js 如何实现异步 I/O。

8.4.1 事件循环 (Event Loop)

在 Node.js 中,事件驱动指的是程序通过注册事件处理函数,并等待事件的触发来执行相应的代码逻辑。Node.js 使用了事件循环机制,它会不断地检查事件队列中是否有事件需要处理,当有事件被触发时,相应的回调函数就会被调用。在程序启动的时候,Node 会创建一个类似于 while(true) 的循环,每执行一次循环体的过程就被称为 Tick。每个 Tick 的过程就是查看是否有事件需要处理,如果有,那就取出事件及其相关的回调函数(如果存在与之关联的回调函数那就去执行该回调函数。)。然后进入到下一个循环,如果不存在需要处理的事件,那么就退出进程。

图4 Tick流程图

8.4.2 事件循环和观察者

像这种判断是否有事件需要处理的设计模式就是观察者模式。浏览器采用了类似的机制,事件可能来自用户的点击或者加载某些文件时产生,这些事件都有与之相对应的观察者。在 Node.js 中,事件主要来源于网络请求、文件 I/O 等。而这些事件与之的对应的观察者有网络 I/O 观察者和文件 I/O 观察者等,观察者也将事件进行了分类。

事件循环是一个典型的生产者和消费者模型,异步I/O和网络请求等相当于事件的生产者,源源不断地为Node提供不同类型的事件,这些事件最后被传递到对应的观察者那里,事件循环从观察者那里取出事件并处理。在Windows系统下这个循环是基于IOCP创建的,在*nix下是基于多线程创建的。

8.4.3 请求对象

普通的函数一般由开发者自己调用,回调函数则是由系统直接调用。我们发出了调用以后,再到回调函数执行,它们中间发生了什么?即从 JavaScript 发起调用到内核执行完 I/O 操作过程中,这中间的产物就是请求对象。

我们以一段代码为例,我看看看 Node.js 如何来对接这些操作系统从而实现异步 I/O:

const fs = require('fs');

fs.open('/msg.txt', function (err, data) {
  console.log(data); 
});
  1. JavaScript调用Node核心模块(fs.js)。
  2. Node核心模块调用C++的内建模块(node_file.cc),创建对应的文件I/O观察者对象。
  3. c++的内建模块通过Node的libuv根据不同的系统平台进行系统调用。
图5 调用流程

8.4.4 libuv调用过程

  1. 系统有 Windows 和 Linux 两个平台的实现,实际上是调用了这两个平台的 uv_fs_open() 方法。在 uv_fs_open() 方法调用的时候,我们创建了一个 FSReqWrap 请求对象。从 JavaScript 层传入的参数和当前的方法都被封装在了这个请求对象中,其中的回调函数被设置在了对象的 oncomplete_sym 属性上。

    req_wrap -> object_ -> Set(oncomplete_sym, callback);
    
  2. 对象包装完成以后,在 Windows 系统下,调用 QueueUserWorkItem() 方法将 FSReqWrap 对象推入线程池中等待执行:

    QueueUserWorkItem(&uv_fs_thread_proc, req, WT_EXECUTEDEFAULT);
    

    参数说明:第一个参数是将要执行方法的引用(uv_fs_thread_proc),第二个参数是第一个 uv_fs_thread_proc 方法运行时所需要的参数,第三个参数是执行标志。

到目前为止,JavaScript 的调用就直接返回了,我们的 JavaScript 代码继续往下执行,当前的 I/O 操作同时也会被放在线程池中执行,这也就完成了异步。

8.4.5 执行回调

封装好请求对象,放入 I/O 线程等待操作系统执行才是开始,接下来是通知回调。线程池中的 I/O 操作完毕后,会将获取到的结果存储到 req->result 属性上,然后调用 PostQueuedCompletionStatus() 通知IOCP,告知当前对象操作已经完成。

PostQueuedCompletionStatus((loop)->iocp, 0, 0, &(req)->overlapped)

每一个 Tick 当中都会调用 PostQueuedCompletionStatus 检查线程池中是否有执行完成的请求,如果有就会将请求对象加入到 I/O 观察者的队列上,然后再将它当作事件处理。PostQueuedCompletionStatus 的作用就是向IOCP提交状态,告诉它当前 I/O 已经完成了。

I/O 观察者回调函数的行为就是取出请求对象的result属性作为参数,取出 oncomplete_sym 作为方法,然后调用并执行,从而达到调用 JavaScript 中传入的回调函数的目的,至此也就完成了整 个I/O 过程。

Linux 则是通过epoll实现这个过程,FreeBSD则是通过kqueue实现,*nix系列下是由libuv自行实现的,只不过线程池是在Windows下由内核(IOCP)直接提供。

图6 异步I/O流程

事件循环、观察者、请其对象、I/O线程池共同构成了异步I/O模型,JavaScript 是单线程的,而 Node.js 本身是多线程的,只不过I/O线程使用的CPU资源较少。还有一个需要知道的就是,除了用户代码无法并行执行以外,所有的I/O(磁盘I/O和网络I/O等)都是可以并行执行的。

8.5 Node.js中非I/O的异步API

除了异步 I/O 以外,Node.js 中还存在一些与 I/O 无关的异步API,它们是设置超时定时器 setTimeout()、设置间隔定时器 setInterval()、设置马上执行定时器 setImmediate()process.nextTick()

setTimeout()setInterval() 这两个函数和浏览器中的API是一样的,setTimeout() 执行单次任务,而 setInterval() 执行多次任务。这些函数的原理和异步I/O相类似,不同的是不需要线程池的参与,调用setTimeout()setInterval() 这两个函数创建的定时器会被插入到定时器观察者内部的红黑树中去,每次执行Tick,都会从这个红黑树中迭代取出对应的定时器对象。setTimeout()函数的行为和 setInterval() 相类似,只不过 setInterval() 是重复的检测和执行。下面是 setTimeout() 行为的图示:

图7 setTimeout()流程

8.5.1 process.nextTick()

一般情况下使用异步都会想到 setTimeout() 这个函数,不过由于事件循环自身的特点,因此定时器的精确度不够。使用定时器需要使用红黑树,创建定时器对象并迭代,setTimeout()的方式就显得性能浪费了。而 process.nextTick() 的操作就显得非常轻量。

process.nextTick() 方法可以将回调函数放入微任务队列,等待当前阶段结束后立即执行。它的执行优先级比 setImmediate() 更高,会在下一个阶段之前立即执行。

process.nextTick = function(callback) {
  // on the way out, don't bother.
  // it won't get fired anyway
  if (process._exiting) {
	return;
  }
  if (tickDepth >= process.maxTickDepth) {
	maxTickWarn();
  }
	
  var tock = { callback: callback };
	
  if (process.domain) {
	tock.domain = process.domain;
  }
	
  nextTickQueue.push(tock);
	
  if (nextTickQueue.length) {
	process._needTickCallback();
  }
};

需要注意的是,process.nextTick() 的回调函数会在当前阶段中被递归调用,直到队列为空为止。因此,如果回调函数没有限制递归深度或者执行时间,可能会导致事件循环无法及时处理其他任务。

8.5.2 setImmediate()

setImmediate() 方法会将回调函数放入事件队列,等待下一轮事件循环执行。它的执行优先级较低,会在 I/O 操作和定时器之后执行。setImmediate()process.nextTick() 比较相似,在 Node.js v0.9.1之前setImmediate()还没有实现以前,都是使用 process.nextTick() 来完成异步的。

console.log('Start');

setImmediate(() => {
  console.log('Callback of setImmediate');
});

console.log('End');

但是,两者还是存在细微差别,process.nextTick() 的优先级会高一些,我们看一下示例代码:

setImmediate(() => {
  console.log('Callback of setImmediate');
});

process.nextTick(() => {
  console.log("Callback of nextTick")
})

使用 setImmediate() 可以避免某些情况下的死锁,例如递归调用一个函数或者在一个 I/O 操作的回调函数中再次发起 I/O 操作。在这种情况下,如果使用 setTimeout() 来调度回调函数,可能会因为定时器过早触发而导致死锁。

8.6 事件驱动与高性能服务器

Node.js 的异步 I/O 不只是用在了文件 I/O,还包括网络套接字,网络套接字上侦听到的请求都会形成事件,然后交给 I/O 观察者。事件循环会不停的处理这些网络 I/O 事件。利用 Node.js 构建的Web服务器就是建立在这样的基础之上,其设计理念和实现方式都与传统的多线程服务器模型有所不同。Node.js 的事件驱动机制和高性能服务器实现方式都基于非阻塞式的编程模型和异步 I/O 操作,在处理高并发、高吞吐量的网络应用和服务时具有较好的性能优势。

总结

Node.js 的异步 I/O 是通过libuv库实现的,它采用了非阻塞、事件驱动的模型来处理 I/O 操作。下面是对 Node.js 异步 I/O 的总结:

  • 非阻塞:Node.js 使用异步 I/O 模型,使得在进行 I/O 操作时不会阻塞主线程或其他任务,提高程序的性能和响应能力。

  • libuv库:Node.js 的异步 I/O 功能是由libuv库提供的,它负责管理 I/O 线程池、事件循环和事件队列等。

  • 事件驱动:异步 I/O 操作是通过事件驱动的方式进行的。当一个异步操作完成时,libuv会将结果保存起来,并将相应的回调函数添加到事件队列中,等待事件循环处理。

  • 回调函数:在 Node.js 中,回调函数是处理异步操作结果的常用方式。当异步操作完成后,事件循环会调用相应的回调函数,并将操作结果传递给它。

  • 异步控制流:为了处理多个异步操作的执行顺序和结果依赖关系,Node.js 提供了一些流程控制的工具,如回调嵌套、事件发布/订阅、Promise、async/await等。。

  • 错误处理:在异步 I/O 中,错误处理尤为重要。Node.js 提供了一些机制来处理和捕获异步操作中的错误,如使用try-catch捕获异常、传递错误给回调函数等。

Node.js 的异步I/O模型使得它适合处理高并发的网络请求、实时数据处理等场景。通过合理地利用异步 I/O,开发者可以编写出高性能、高效率的 Node.js 应用程序。但同时,也需要注意回调地狱、错误处理等问题,以保证代码的可读性和稳定性。

9-事件

Node.js 采用基于事件驱动的非阻塞 I/O 模型,实现高效的异步编程。Node.js 的事件模块(events)是一个核心模块,它提供了一些用于处理事件的基本工具。

Node.js 中的事件模块(events)是指 EventEmitter 类,它是 Node.js 核心模块 events 的一部分。事件模块提供了一种基于观察者模式的事件驱动编程机制,允许开发者轻松地创建、触发和监听事件,并在事件发生时执行相应的回调函数。

9.1 EventEmitter 类

在事件模块中,EventEmitter 类是核心部分。它提供了多个方法来注册、触发和处理事件。

Node.js 的事件模块广泛应用于实现各种功能,包括网络编程(如 HTTP 服务器和客户端)、文件系统操作、消息队列、用户界面交互等。通过使用 EventEmitter 类,开发者可以实现高效的异步编程,同时保持代码简洁和可维护性。

下面是 EventEmitter 类的一些常见的方法(event表示事件字符串,listener事件处理函数,中括号里面的参数是可选的。):

方法 描述
addListener(event, listener) 对指定的事件绑定事件处理函数(监听器),接受event字符串和listener回调函数。
on(event, listener) 对指定的事件注册一个监听函数。
once(event, listener) 对指定的事件绑定一个只执行一次的函数。
removeListener(event, listener) 对指定的事件移除绑定的事件处理函数。
removeAllListener(event, listener) 对指定的事件移除所有事件处理函数。
setMaxListeners(n) 指定事件处理函数的最大数量,n为整数。
lsteners(n) 获取指定事件的所有事件处理函数。
emit(event, [arg1], [arg2], […]) 按照参数的顺序执行每个处理函数,如果有注册监听返回true,否则返回false。

9.1.1 创建 EventEmitter 实例

要使用事件模块,首先需要引入 events 模块,并创建一个 EventEmitter 实例:

const EventEmitter = require('events');
// 这将创建一个名为 emitter 的 EventEmitter 实例。
const emitter = new EventEmitter();

9.1.2 注册事件监听器

可以使用 on() 方法或 addListener() 方法来注册事件监听器,以便在特定事件发生时执行相应的回调函数。

emitter.on('eventName', (arg1, arg2) => {
  // 处理事件
});

在上面的代码中,当 eventName 事件被触发时,注册的回调函数将被执行。

9.1.3 触发事件

使用 emit() 方法来触发特定的事件,并传递参数给注册的事件监听器。

emitter.emit('eventName', arg1, arg2);

在上面的代码中,emit() 方法将触发名为 eventName 的事件,并将 arg1arg2 作为参数传递给事件监听器。

9.1.4 一次性监听事件

除了 on() 方法之外,还可以使用 once() 方法一次性监听事件。这意味着事件只会被触发一次,而不是每次事件发生时都触发。

emitter.once('eventName', () => {
  // 处理事件
});

9.1.5 错误处理

EventEmitter 类还包含了处理错误的能力。当没有为 'error' 事件注册监听器时,EventEmitter 将会打印堆栈跟踪并退出程序。为了避免这种情况,建议始终为 'error' 事件注册至少一个监听器。

emitter.on('error', (err) => {
  console.error('Error occurred:', err);
});

9.2 继承 EventEmitter

通常情况下,我们都是在对象中去继承 EventEmitter,包括 fsnethttp 在内的支持事件响应的核心模块都是 EventEmitter 的子类。这是因为具有某个实体功能的对象实现事件符合语义,事件的监听和发生应该是一个对象的方法。再就是 JavaScript 的对象机制是基于原型的,支持部分多重继承,继承 EventEmitter 并不会打乱对象原有的继承关系。

总结

Node.js 的事件模型使得开发者可以基于事件驱动的方式编写高效的、非阻塞的程序。通过 EventEmitter,我们可以很方便地监听各种事件,当事件发生时,执行相应的代码逻辑,从而实现异步编程。

10-异步编程

Node.js 的异步编程是其高性能和高并发的关键。在 Node.js 中,异步编程采用了一系列机制,包括 回调函数事件循环Promiseasync/await 等。下面将对这些机制进行详细介绍。

10.1 回调函数

回调函数是 Node.js 异步编程模型的核心。当一个异步操作完成时,会触发相应的事件,并执行对应的回调函数来处理事件。通过回调函数,我们可以在异步 I/O 操作完成后继续执行程序,而不必等待 I/O 操作完成。

例如,下面的代码演示了使用回调函数处理异步文件读取操作:

const fs = require('fs');

fs.readFile('/Users/ydcq/Desktop/msg.txt', function(err, data) {
  if (err) {
	 throw err;
  }
  	
  console.log(data);
});

readFile 方法是一个异步方法,它会读取指定路径下的文件,然后触发一个读取完成的事件。当读取完成事件被触发时,回调函数会被执行,并打印出文件内容。

readFile 就是一个典型的高阶函数,将回调函数作为参数传递给 readFile。随着业务逻辑的增加,这种传入回调的方式也存在着很大的弊端,例如下面这样:

const fs = require('fs');

fs.readFile('1.json', (err, data) => {
  fs.readFile("2.json", (err, data) => {
	fs.readFile('3.json', (err, data) => {
	  fs.readFile('3.json', (err, data) => {
		console.log("data=", data);
	  }
	}
  }
}

像这种回调当中嵌套回调,也被称为 回调地狱,这种代码无论是可读性和维护性都是很差的。嵌套回调的层级太多还会导致这里面如果有失败都需要单独为每一个任务去处理,这样也就大大增加了代码的混乱程度。

10.2 事件发布/订阅模式

Node.js 的异步 I/O 操作,都会发送一个事件到时间队列中,当一个操作发起时,在事件驱动模型中会生成一个主循环来监听事件,当检测到事件后,会触发回调函数,如下图所示:

图1 事件处理流程

事件监听器模式是一种广泛用于异步编程的模式,它是函数回调的事件化,也被称为事件的发布/订阅模式。Node.js 的 events 模块其实就是发布/订阅模式的一个实现。

const EventEmitter = require('events');
// 创建一个名为 emitter 的 EventEmitter 实例
const emitter = new EventEmitter();

// 订阅
emitter.on('event', () => {
  // 绑定事件
  console.log("绑定事件event")
})

emitter.addListener('click', () => {
  // 监听事件
  console.log("监听click事件")
})


// 发布
emitter.emit('event'); // 提交事件
emitter.emit('click'); // 提交事件

通过上述步骤,我们可以实现模块之间的松散耦合,让不同的模块可以独立地定义和响应事件。事件发射器提供了一种简单而有效的方式来实现模块之间的通信,使得代码的组织和维护更加灵活和可扩展。Node.js 的事件发布/订阅模式为开发者提供了一种高效、灵活的编程范式,可以在异步环境中实现模块间的通信和协作。

10.3 Promise

Promise 是一种用于处理异步操作的编程模型,它可以将回调函数转换为链式调用的方式。Promise 可以通过 then 方法实现链式调用,从而使得代码更为简洁和易于维护。同时,Promise 还支持多个异步操作的并行处理和串行依赖关系的处理。

例如,下面的代码演示了使用 Promise 处理异步文件读取操作:

const fs = require('fs').promises;

fs.readFile('/Users/ydcq/Desktop/msg.txt')
  .then(data => {
    console.log(data);
  })
  .catch(err => {
    console.error(err);
  });

在上面的代码中,readFile 方法返回一个 Promise 对象。当 Promise 对象的状态变为 resolved 时,then 方法会被执行,并打印出文件内容。当 Promise 对象的状态变为 rejected 时,catch 方法会被执行,并打印出错误信息。

Promise 的链式调用在很大程度上避免了大量的嵌套带来的问题,也更加的复合人的思维逻辑,大大的方便了异步编程。

10.4 async/await

async/await 是 ES2017 中引入的新特性,它是一种基于 Promise 的异步编程模型。async/await 可以将异步操作的链式调用转换为顺序执行的方式,从而使得代码更为简洁和易于理解。同时,async/await 还支持多个异步操作的并行处理和串行依赖关系的处理。

async function readFile() {
  try {
    const data = await fs.readFile('/Users/ydcq/Desktop/msg.txt');
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}

readFile();

readFile 方法定义为一个异步函数。在函数内部,使用 await 关键字等待 readFile 方法返回的 Promise 对象。当 Promise 对象的状态变为 resolved 时,await 表达式会返回 Promise 对象的结果,并将其赋值给变量 data。当 Promise 对象的状态变为 rejected 时,try-catch 语句会捕获错误并打印出错误信息。

async/await 使得异步代码也能够以同步的方式去编写,更加不需要借助第三方库来实现。

10.5 Generator

在 Node.js 中,Generator 是一种特殊的函数,它可以暂停和恢复执行,使得异步编程变得更加简洁和可读。通过使用 Generator,我们可以使用同步的方式编写异步代码,避免回调地狱和复杂的 Promise 链式调用。

下面是使用 Generator 实现异步编程的基本流程:

  1. 定义 Generator 函数:
function* asyncFunction() {
  // 异步操作1
  yield someAsyncOperation1();

  // 异步操作2
  yield someAsyncOperation2();

  // ...
}
  1. 获取 Generator 对象:
const generator = asyncFunction();
  1. 执行 Generator 函数并获取结果:
const iterator = generator.next();

if (!iterator.done) {
  // 异步操作的回调函数中通过调用 iterator.next() 恢复 Generator 的执行
  iterator.value.then(() => {
    generator.next();
  });
}
  1. 重复执行步骤3,直到 Generator 函数执行完毕:
function run(generator) {
  let iterator = generator.next();

  function iterate(iterator) {
    if (!iterator.done) {
      iterator.value.then(() => {
        iterate(generator.next());
      });
    }
  }

  iterate(iterator);
}

run(generator);

通过以上步骤,我们可以使用 Generator 和 yield 关键字来暂停和恢复异步操作的执行。当异步操作完成时,通过调用 iterator.next() 继续执行 Generator 函数中的下一步操作,从而实现了异步编程的简洁性和可读性。

需要注意的是,上述代码是基本的 Generator 实现异步编程的流程,为了提高代码的可读性和可维护性,通常会结合使用 Promise、async/await 等语法糖来更加方便地处理异步操作。

Node.js 的 Generator 提供了一种优雅的方式来处理异步编程,通过使用 yield 关键字暂停和恢复执行,可以让异步代码更加简洁和可读。然而,由于 Generator 的使用需要手动管理迭代器和回调函数,所以在实际开发中,一般会使用 async/awaitPromise 等更加方便的异步编程方式。

总结

Node.js 的异步编程模型使得开发者能够高效地处理大量并发请求,避免阻塞和等待,提高系统的并发能力和响应性。开发者可以使用回调函数、Promise、Async/Await、事件触发器等方式来处理异步操作,从而实现更加简单、清晰和易于维护的异步代码。

11-util

util 是 Node.js 标准库中的一个内置模块,它提供了一些常用的工具函数和类,用于简化常见的 JavaScript 编程任务。这些函数包括格式化字符串、错误处理、对象检查等。下面是对 util 模块的一些详细介绍:

11.1 继承(Inheritance)

在 Node.js 中,util 模块提供了一些实用工具函数,其中包括用于继承的函数。

util.inherits(child, parent)

该方法用于实现对象之间的继承关系。它将子类的原型设置为父类的一个实例,从而实现了子类继承父类的属性和方法。

const util = require('util');

function Animal(name) {
  this.name = name;
}

Animal.prototype.walk = function() {
  console.log(this.name + ' is walking...');
};

function Bird(name) {
  Animal.call(this, name);
}

util.inherits(Bird, Animal);

Bird.prototype.fly = function() {
  console.log(this.name + ' is flying...');
};

let bird = new Bird('Sparrow');
bird.walk(); // output: Sparrow is walking...
bird.fly(); // output: Sparrow is flying...

11.2 类型判断(Type Checking

  • util.isPrimitive(input): 判断给定的值是否为原始类型(字符串、数字、布尔值、null 或 undefined)。

  • util.isArray(input): 判断给定的值是否为数组。

  • util.isDate(input): 判断给定的值是否为日期对象。

  • util.isError(input): 判断给定的值是否为错误对象。

const util = require('util');

console.log(util.isPrimitive('hello')); // true
console.log(util.isPrimitive(10)); // true
console.log(util.isPrimitive(true)); // true
console.log(util.isPrimitive(null)); // true
console.log(util.isPrimitive(undefined)); // true

console.log(util.isArray([1, 2, 3])); // true

console.log(util.isDate(new Date())); // true

console.log(util.isError(new Error('Something went wrong'))); // true

11.3 错误处理(Error Handling)

  • util.format(format, […args]): 根据指定的格式字符串和参数生成一个格式化的字符串。

  • util.inspect(object, [options]): 将给定的对象转换为字符串表示形式,用于调试目的。可以通过传递选项参数来自定义输出格式。

const util = require('util');

let name = 'John';
let age = 25;

console.log(util.format('My name is %s and I am %d years old', name, age));
// output: My name is John and I am 25 years old

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

let person = new Person('John', 25);
console.log(util.inspect(person));
/* output:
   Person {
     name: 'John',
     age: 25
   }
*/

11.4 修饰器(Decorators)

  • util.deprecate(function, warning): 返回一个包装后的函数,当该函数被调用时会发出警告。常用于标记已废弃的函数,提醒开发人员不要再使用。
const util = require('util');

function deprecatedFunction() {
  console.log('This function is deprecated and will be removed in future versions');
}

let wrappedFunction = util.deprecate(deprecatedFunction, 'This function is deprecated');

wrappedFunction();
// output: This function is deprecated and will be removed in future versions
// (node:1234) DeprecationWarning: This function is deprecated

11.5 事件触发器(Event Emitter)

  • util.promisify(original): 将基于回调的异步函数转换为返回 Promise 的形式,方便使用 async/await 处理异步操作。

    const util = require('util');
    const fs = require('fs');
    
    let readFileAsync = util.promisify(fs.readFile);
    
    async function readFile() {
      let data = await readFileAsync('file.txt', 'utf8');
      console.log(data);
    }
    
    readFile(); // output: contents of file.txt
    

11.6 工具函数(Utility Functions)

  • util.format(format[, …args]):util.format() 方法根据指定的格式字符串返回格式化后的字符串。

    const util = require('util');
    const formattedString = util.format('%s is %d years old', 'Alice', 30);
    console.log(formattedString);  // 输出:'Alice is 30 years old'
    
  • util.promisify(original):util.promisify() 方法用于将符合 Node.js 回调风格的函数(即以回调作为最后一个参数的函数)转换为基于 Promise 的函数。

    const util = require('util');
    const fs = require('fs');
    const readFileAsync = util.promisify(fs.readFile);
    
    readFileAsync('example.txt', 'utf8')
      .then(data => {
    	console.log(data);
      })
      .catch(err => {
    	console.error(err);
      });
    
  • util.inspect(object, [options]):util.inspect() 方法返回对象的字符串表示,通常用于调试和日志输出。

    const util = require('util');
    const obj = { name: 'John', age: 25 };
    console.log(util.inspect(obj, { showHidden: true, depth: null }));
    // 输出对象的详细信息,包括隐藏属性和深度遍历所有属性
    
  • util.inherits(constructor, superConstructor):util.inherits() 方法用于实现对象间原型继承。

    const util = require('util');
    
    function Animal(name) {
      this.name = name;
    }
    
    function Dog(name) {
      Animal.call(this, name);
    }
    
    util.inherits(Dog, Animal);
    
  • util.deprecate(fn, message):util.deprecate() 方法接受两个参数:待标记为废弃的函数(或方法)和发出的警告消息。它返回一个新的函数,当调用该函数时,会打印警告消息。

    const util = require('util');
    
    function oldMethod() {
      console.log('This method is deprecated. Please use the newMethod instead.');
    }
    
    const deprecatedMethod = util.deprecate(oldMethod, 'oldMethod is deprecated.');
    
    deprecatedMethod(); // 调用被标记为废弃的函数
    
    // 输出警告消息:
    //This method is deprecated. Please use the newMethod instead.
    //(node:3282) DeprecationWarning: oldMethod is deprecated.
    //(Use `node --trace-deprecation ...` to show where the warning was created)
    

11.7 其他方法

  • callbackify:util.callbackify 方法可以将返回 Promise 的函数转换为基于回调的函数。这在与旧的回调风格代码交互时非常有用。

  • TextEncoder 和 TextDecoder:util 中的 TextEncoder 和 TextDecoder 类是用于处理文本编码和解码的工具类。它们提供了将字符串转换为字节数组(或反之)的功能。

这些只是 util 模块提供的一些常用方法和工具函数,还有其他一些函数和类可以在 Node.js 的文档 中找到。

总结

Node.js 的 util 模块提供了一系列实用工具函数,用于扩展 Node.js 核心API的功能,以及简化开发过程。util 模块中包含了许多用于操作对象、处理数据和其他常见任务的函数和类。通过 util 模块,开发者可以更加便利地进行对象操作、错误处理、类型判断和格式化输出等常见任务,提高开发效率,减少重复工作。

12-文件系统 fs

fs(文件系统)模块是 Node.js 中用于进行文件系统操作的核心模块之一。它提供了许多方法来读取、写入、修改和删除文件,以及创建、删除和遍历目录等功能。下面将详细介绍 fs 模块的一些常见功能:

12.1 打开文件

fs.open(path, flags, [mode], callback) 方法用于在文件系统中打开文件。它接受文件路径和标志作为参数,并返回一个文件描述符(file descriptor)。

参数说明:

  • path:要打开的文件的路径。
  • flags:指定文件的打开方式,可以是以下之一:
    • ‘r’:读取模式,文件不存在则抛出错误。
    • ‘r+’:读写模式,文件不存在则抛出错误。
    • ‘rs’:同步读取模式,文件不存在则抛出错误。
    • ‘rs+’:同步读写模式,文件不存在则抛出错误。
    • ‘w’:写入模式,文件不存在则创建文件。
    • ‘wx’:排他写入模式,文件不存在则创建文件,存在则抛出错误。
    • ‘w+’:读写模式,文件不存在则创建文件。
    • ‘wx+’:排他读写模式,文件不存在则创建文件,存在则抛出错误。
    • ‘a’:追加写入模式,文件不存在则创建文件。
    • ‘ax’:排他追加写入模式,文件不存在则创建文件,存在则抛出错误。
    • ‘a+’:读取追加写入模式,文件不存在则创建文件。
    • ‘ax+’:排他读取追加写入模式,文件不存在则创建文件,存在则抛出错误。
  • mode(可选):指定文件的权限,默认为 0o666
  • callback:回调函数,接收两个参数 (err, fd),其中 fd 是文件描述符。

下面是一个打开文件的示例代码:

const fs = require('fs');

fs.open('/path/to/file.txt', 'r', (err, fd) => {
  if (err) {
    console.error(err);
    return;
  }
  // 文件已打开,可以进行操作
  console.log(`文件已打开,文件描述符为 ${fd}`);
  // 关闭文件
  fs.close(fd, (err) => {
    if (err) {
      console.error(err);
      return;
    }
    console.log('文件已关闭');
  });
});

注意:
1. 在回调函数中,我们可以进行文件操作,如读取、写入等。完成后,记得通过 fs.close() 方法关闭文件,以释放资源。
2. fs.open() 方法是异步的,也可以使用同步版本的 fs.openSync() 进行同步操作,但建议在大多数情况下使用异步方法,以避免阻塞进程。

12.2 文件读取

  • fs.readFile(path, [options], callback):异步地读取指定路径的文件内容。
  • fs.readFileSync(path, [options]):同步地读取指定路径的文件内容。

[options] 参数可以包含以下属性:

  • encoding:指定文件的编码格式,可以是字符串形式的编码名称(如 'utf8''ascii' 等),或者 null。如果不指定编码格式,则返回原始的缓冲区数据。
  • flag:指定打开文件时的标志位。常见的取值包括 'r'(默认值,表示以读取模式打开文件)、'w'(表示以写入模式打开文件)、'a'(表示以追加模式打开文件)等。
  • mode:在创建新文件时用于指定文件的权限,默认为 0o666
  • autoClose:指定在读取完文件后是否自动关闭文件描述符,默认为 true
  • emitClose:指定在文件关闭时是否触发 'close' 事件,默认为 false

下面是一个文件读取的示例代码:

const fs = require('fs');

// 异步读取文件
fs.readFile('/Users/ydcq/Desktop/hello.txt', 'Hello, World!', (err, data) => {
  if (err) throw err;
  console.log(data);
});

// 同步读取文件
const content = fs.readFileSync('/Users/ydcq/Desktop/hello.txt', 'utf8');
console.log(content);

上述代码中,首先通过 fs.readFile() 方法异步读取文件内容,并通过回调函数返回结果。然后使用 fs.readFileSync() 方法同步读取文件内容。

12.3 文件写入

  • fs.writeFileSync(path, [options]):同步地将数据写入文件。
  • fs.existsSync(path):检查指定路径的文件是否存在。

[options] 参数可以包含以下属性:

  • encoding:指定要写入文件的编码格式,可以是字符串形式的编码名称(如 'utf8''ascii' 等),或者 null。如果不指定编码格式,则写入原始的 BufferUint8Array 数据。
  • mode:在创建新文件时用于指定文件的权限,默认为 0o666
  • flag:指定打开文件时的标志位。常见的取值包括 'w'(默认值,表示以写入模式打开文件)、'a'(表示以追加模式打开文件)等。

signal:一个 AbortSignal 对象,用于在中止写入操作时发出信号。

下面是一个文件写入的示例代码:

const fs = require('fs');

// 获取文件信息
fs.stat('/Users/ydcq/Desktop/hello.txt', (err, stats) => {
  if (err) throw err;
  console.log(stats);
});

// 检查文件是否存在
const exists = fs.existsSync('/Users/ydcq/Desktop/hello.txt');
console.log(exists);

在上述示例中,我们使用 fs.writeFile() 方法异步地将字符串 'Hello, World!' 写入名为 file.txt 的文件,并在回调函数中处理可能出现的错误。另外,使用 fs.writeFileSync() 方法同步地将字符串写入文件。

12.4 获取文件信息

  • fs.stat(path, [options], callback):获取指定路径的文件信息。
  • fs.statSync(path, [options]):用于获取给定文件路径的详细信息,这是一个同步方法,与异步方法 fs.stat() 不同,fs.statSync() 是阻塞的,会在获取完文件信息后立即返回结果。

[options] 参数可以包含以下属性:

  • bigint:一个布尔值,用于指定是否返回 fs.Stats 对象中的整数值为 BigInt 类型。默认值为 false
  • throwIfNoEntry:一个布尔值,用于指定当文件不存在时是否抛出错误。默认值为 false,即不抛出错误,而是将错误作为参数传递给回调函数。
  • signal:一个 AbortSignal 对象,用于在中止获取状态操作时发出信号。

示例代码:

const fs = require('fs');

// 获取文件信息
fs.stat('/Users/ydcq/Desktop/hello.txt', (err, stats) => {
  if (err) throw err;
  console.log(stats);
});

在上述代码中,我们使用 fs.stat() 方法获取名为 hello.txt 的文件信息,并在回调函数中打印该信息。

fs.statSync() 方法返回一个 fs.Stats 对象,其中包含了有关文件的各种属性和方法,如文件大小、创建时间、修改时间等。可以使用 fs.Stats 对象的属性和方法来进一步处理文件的信息。

属性 描述
dev 文件所在设备的标识符。对于网络文件系统(NFS)来说,该值可能为未定义。
ino 文件的 inode 编号。一般情况下,每个文件系统都会给文件分配一个唯一的编号。
mode 文件的权限和类型。它表示文件的访问权限(如读、写、执行)以及文件的类型(如普通文件、目录、符号链接等)。
nlink 文件的硬链接数量。硬链接是指多个文件名指向同一个 inode 的情况。
uid 文件所有者的用户标识符。它代表文件所属用户的唯一标识符。
gid 文件所有者的组标识符。它代表文件所属组的唯一标识符。
rdev 如果文件是特殊文件(如设备文件),则表示设备的标识符。对于普通文件来说,该值为未定义。
size 文件大小,以字节为单位。
blksize 文件系统用于 I/O 操作的块大小。在许多文件系统中,它是文件分配的最小单位。
blocks 文件占用的块数量。一个块通常是一个固定大小的存储单元。
atimeMs 上次访问时间的毫秒数。它表示文件最后一次被访问的时间。
mtimeMs 修改时间的毫秒数。它表示文件内容最后一次被修改的时间。
ctimeMs 变化时间的毫秒数。它表示文件的状态信息(如权限、所有者等)最后一次被修改的时间。
birthtimeMs 创建时间的毫秒数。它表示文件的创建时间。

12.5 追加内容

使用 fs.appendFile(filename, data, [options], callback) 方法可以异步地追加数据到指定文件末尾。它接受文件路径、要追加的数据和一个回调函数作为参数,回调函数中会返回追加完成或错误信息。

[options] 参数可以包含以下属性:

  • encoding:指定要追加的数据的编码格式,可以是字符串形式的编码名称(如 'utf8''ascii' 等),或者 null。如果不指定编码格式,则直接将数据以 BufferUint8Array 的形式追加到文件中。
  • mode:在创建新文件时用于指定文件的权限,默认为 0o666
  • flag:指定打开文件时的标志位。常见的取值包括 'a'(默认值,表示以追加模式打开文件)和 'ax'(表示以追加模式打开文件,但如果文件已存在则会抛出错误)等。
  • signal:一个 AbortSignal 对象,用于在中止追加操作时发出信号。

示例代码:

const data = 'Hello, world!';

fs.appendFile('/Users/ydcq/Desktop/hello.txt', data, (err) => {
  if (err) {
    console.error(err);
    return;
  }

  console.log('Data has been appended.');
});

上述代码将字符串 "Hello, world!" 异步地追加到名为 hello.txt 的文件末尾。如果文件不存在,则会创建该文件;如果文件已存在,则会在原有内容末尾追加新内容。追加完成后,通过回调函数输出成功信息或错误信息。

12.6 移动/重命名文件

使用 fs.rename(oldPath, newPath, callback) 方法可以异步地移动或重命名文件。它接受旧文件路径、新文件路径和一个回调函数作为参数,回调函数中会返回移动/重命名完成或错误信息。

fs.rename('/Users/ydcq/Desktop/web/node_demo/demo.js', '/Users/ydcq/Desktop/web/node_demo/demo_new.js', (err) => {
  if (err) {
    console.error(err);
    return;
  }

  console.log('File has been renamed.');
});

上述代码将名为 demo.js 的文件重命名为 demo_new.js。如果重命名成功,将打印成功信息;如果重命名失败,将输出错误信息。

12.7 删除文件

  • fs.unlink(path, [callback(err)]):可以异步地删除指定的文件。它接受文件路径和一个回调函数作为参数,回调函数中会返回删除完成或错误信息。

  • fs.rmSync(path, [options]):用于同步删除文件或目录的方法。它的作用是删除指定路径的文件或目录,如果目标是一个目录,则会递归地删除该目录及其所有内容。

    [options] 参数可以包含以下属性:

    • force:一个布尔值,用于指定是否强制删除文件或目录。默认情况下,如果目录非空或文件不可写,则会抛出错误。将 force 设置为 true 可以忽略这些错误并强制执行删除操作。
    • maxRetries:一个整数,用于指定在删除失败时的最大重试次数。默认值为 0,表示不重试删除操作。
    • recursive:一个布尔值,用于指定是否递归删除目录及其内容。默认情况下,如果删除的是目录且目录非空,则会抛出错误。将 recursive 设置为 true 可以递归删除目录及其内容。
    • retryDelay:一个整数,用于指定在重试删除操作之间的延迟时间(以毫秒为单位)。默认值为 100
    • retryDelayMultiplier:一个数字,用于指定重试删除操作的延迟时间乘数。默认值为 1
fs.unlink('file.txt', (err) => {
  if (err) {
    console.error(err);
    return;
  }

  console.log('File has been deleted.');
});

// 递归删除目录及其内容
fs.rmSync('/Users/ydcq/Desktop/web', { recursive: true });

12.8 创建目录

  • fs.mkdir(path, [options], callback):创建一个目录。
  • fs.mkdirSync(path, [options]):同步创建一个目录。

[options] 参数可以包含以下属性:

  • recursive:一个布尔值,用于指定是否递归创建目录。默认情况下,如果上级目录不存在,则会抛出错误。将 recursive 设置为 true 可以递归创建目录及其上级目录。
  • mode:一个整数或字符串,用于指定新目录的权限,默认为 0o777。如果指定为一个字符串,则表示权限的八进制字符串表示形式(如 ‘755’)。
  • signal:一个 AbortSignal 对象,用于在中止创建操作时发出信号。

示例代码:

const fs = require('fs');

// 异步创建目录
fs.mkdir('/Users/ydcq/Desktop/web', { recursive: true }, (err) => {
  if (err) throw err;
  console.log('目录已创建');
});

// 同步创建目录
try {
  // recursive(布尔值):如果为 true,则会递归创建目录;默认为 false。
  fs.mkdirSync('/Users/ydcq/Desktop/web', { recursive: true });
  console.log('目录已创建');
} catch (err) {
  console.error(err);
}

12.9 读取目录下的所有文件

  • fs.readdir(path, [options], callback):可以异步地读取指定目录下的所有文件。它接受目录路径和一个回调函数作为参数,回调函数中会返回目录下的文件名列表。
  • fs.readdirSync(path, [options]):同步读取指定的目录路径下的所有文件。可选参数对象,包括 withFileTypes(布尔值):如果为 true,则返回 fs.Dirent 对象数组而不是文件名数组;默认为 false

[options] 参数可以包含以下属性:

  • encoding:指定目录中文件名的编码格式,可以是字符串形式的编码名称(如 'utf8''ascii' 等),或者 null。如果不指定编码格式,则返回的文件名将以 BufferUint8Array 的形式给出。
  • withFileTypes:一个布尔值,用于指定是否返回 fs.Dirent 对象而不是文件名的数组。默认情况下,返回的是文件名的数组。设置为 true 则返回 fs.Dirent 对象,该对象包含文件名以及文件类型等信息。
  • signal:一个 AbortSignal 对象,用于在中止读取操作时发出信号。

示例代码:

const fs = require('fs');

// 异步读取指定的目录下的文件
fs.readdir('/Users/ydcq/Desktop/web', (err, files) => {
  if (err) {
    console.error(err);
    return;
  }

  console.log(files);
});

// 同步读取指定的目录下的文件
const path = require('path');
function readDirFilesSync(dirPath) {
  const files = fs.readdirSync(dirPath, { withFileTypes: true });
  files.forEach((file) => {
    const filePath = path.join(dirPath, file.name);
    if (file.isDirectory()) {
      // 如果是目录,则递归读取其下的所有文件
      readDirFilesSync(filePath);
    } else {
      // 如果是文件,则输出其路径和大小
      const stats = fs.statSync(filePath);
      console.log(`${filePath} (${stats.size} bytes)`);
    }
  });
}
// 同步读取目录及其子目录下的所有文件
readDirFilesSync('/Users/ydcq/Desktop/web');

12.10 删除目录

  • fs.rmdir(path, [options], callback):异步删除指定的目录。
  • fs.rmdirSync(path, [options]):同步删除指定的目录。可选参数对象,包括 recursive(布尔值):如果为 true,则递归删除目录和其子目录;默认为 false

[options] 参数可以包含以下属性:

  • recursive:一个布尔值,用于指定是否执行递归目录删除。在这种模式下,如果找不到指定的路径并且在失败时重试该操作,则不会报告错误。默认值为 false
  • maxRetries:一个整数值,用于指定 Node.js 由于任何错误而失败时将尝试执行该操作的次数。在给定的重试延迟后执行操作。如果递归选项未设置为 true,则忽略此选项。默认值为 0
  • retryDelay:一个整数值,用于指定重试操作之前的等待时间(以毫秒为单位)。如果递归选项未设置为 true,则忽略此选项。默认值为 100毫秒

示例代码:

const fs = require('fs');

// 异步删除目录
fs.rmdir('/Users/ydcq/Desktop/web', { recursive: true }, (err) => {
  if (err) throw err;
  console.log('目录已删除');
});

// 同步删除目录
try {
  fs.rmdirSync('/Users/ydcq/Desktop/web', { recursive: true });
  console.log('目录已删除');
} catch (err) {
  console.error(err);
}

12.11 监听目录变更

使用 fs.watch() 方法可以异步地监听指定目录的变化。它接受目录路径和一个回调函数作为参数,回调函数中会返回变化的类型和文件名等信息。

示例代码:

const fs = require('fs');

fs.watch('/Users/ydcq/Desktop/web/demo', (eventType, filename) => {
  console.log(`Event type: ${eventType}`);
  console.log(`File name: ${filename}`);
});

上述代码监听当前目录的变化,将事件类型和文件名等信息打印出来。如果有文件被创建、修改或删除等操作,将触发回调函数并输出相应信息。

12.12 查询文件访问权限

fs.access(path, mode, callback) 方法用于检查文件或目录的访问权限。它可以检查文件/目录是否存在以及是否具有指定的权限。

  • path:要检查的文件或目录路径。
  • mode:一个可选的整数参数,用于指定要检查的权限,默认为 fs.constants.F_OK,表示检查文件/目录是否存在。其他可选的常量值包括:
    • fs.constants.R_OK:检查文件/目录是否可读。
    • fs.constants.W_OK:检查文件/目录是否可写。
    • fs.constants.X_OK:检查文件/目录是否可执行。
  • callback:回调函数,接收一个可能出现的异常参数。如果没有错误发生,则表示具有所需权限。

示例代码:

const fs = require('fs');

// 检查文件是否可读
fs.access('path/to/file', fs.constants.R_OK, (err) => {
  if (err) {
    console.error('文件不可读');
  } else {
    console.log('文件可读');
  }
});

// 检查目录是否存在
fs.access('path/to/directory', fs.constants.F_OK, (err) => {
  if (err) {
    console.error('目录不存在');
  } else {
    console.log('目录存在');
  }
});

注意:
1. fs.access 方法是异步的,通过回调函数来处理结果。如果没有指定权限参数 mode,则默认为检查文件/目录是否存在。在回调函数中,如果出现错误,则表示文件/目录不具备所需的权限;如果没有错误,则表示具有所需权限。
2. fs.access 方法只是检查权限,并不能用于修改权限。另外,该方法在进行文件/目录访问前先检查权限是一个良好的实践,可以避免在访问时出现异常。

12.13 修改文件权限

fs 模块提供了 fs.chmod(path, mode, callback) 方法来修改文件的权限。该方法将更改指定路径上文件/目录的权限,具体更改的权限由参数 mode 决定。以下是 fs.chmod() 方法的基本信息:

参数:

  • path:要更改权限的文件/目录的路径。
  • mode:一个整数或字符串,用于指定新的文件/目录权限。可以使用八进制表示法或字符串形式,如 0o755'rwxr-xr-x'。更多信息可以参考 chmod() 文档。
  • callback:回调函数,接收一个可能出现的异常参数。

示例代码:

const fs = require('fs');

// 修改文件权限为 644
fs.chmod('path/to/file', 0o644, (err) => {
  if (err) {
    console.error(`更改文件权限失败: ${err}`);
  } else {
    console.log('文件权限已更改');
  }
});

// 修改目录权限为 755
fs.chmod('path/to/directory', 'rwxr-xr-x', (err) => {
  if (err) {
    console.error(`更改目录权限失败: ${err}`);
  } else {
    console.log('目录权限已更改');
  }
});

注意:
1. fs.chmod() 方法是异步的,通过回调函数来处理结果。在回调函数中,如果出现错误,则表示权限更改失败;如果没有错误,则表示权限已成功更改。
2. fs.chmod() 方法需要有足够的权限才能更改文件/目录的权限,否则将会抛出权限不足的错误。在使用该方法时,建议先通过 fs.access() 方法检查当前用户是否具备更改权限。

总结

fs 模块提供了丰富的文件系统操作功能,可以很方便地读取、写入、删除和管理文件。当需要进行文件系统操作时,可以使用 fs 模块来实现相应的功能。

13-路径模块 Path

path 模块是 Node.js 中一个核心模块,用于处理文件路径相关的操作。它提供了一组常用的函数来处理文件路径,包括路径解析、规范化、拼接、相对路径等等。

以下是 path 模块中常用的一些方法:

13.1 path.normalize(p)

将一个路径字符串转换为标准路径格式。它会解析出其中的 ...,并将多个路径分隔符转换为单个分隔符。

const path = require('path');

console.log(path.normalize('/foo/bar//baz/asdf/quux/..')); // '/foo/bar/baz/asdf'

13.2 path.join([…paths])

将多个路径拼接成一个完整的路径。它会自动处理多余的路径分隔符,并返回一个标准路径格式的字符串。

const path = require('path');

console.log(path.join('/foo', 'bar', 'baz/asdf', 'quux', '..')); // '/foo/bar/baz/asdf'

13.3 path.resolve([…paths])

将多个路径解析为一个绝对路径。它会从右到左地依次处理每个路径片段,并返回一个绝对路径格式的字符串。

const path = require('path');

console.log(path.resolve('/foo/bar', './baz')); // '/foo/bar/baz'
console.log(path.resolve('/foo/bar', '/tmp/file/')); // '/tmp/file'

13.4 path.relative(from, to)

获取从一个路径到另一个路径的相对路径。它会返回一个相对路径格式的字符串。

const path = require('path');

console.log(path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb')); // '../../impl/bbb'

13.5 path.dirname(p)

获取一个路径的目录名。它会返回一个目录名格式的字符串。

const path = require('path');

console.log(path.dirname('/foo/bar/baz/asdf/quux')); // '/foo/bar/baz/asdf'

13.6 path.basename(p, [ext])

获取一个路径的文件名或者是目录名。可以通过第二个参数指定要去掉的文件扩展名。

const path = require('path');

console.log(path.basename('/foo/bar/baz/asdf/quux.html')); // 'quux.html'
console.log(path.basename('/foo/bar/baz/asdf/quux.html', '.html')); // 'quux'

13.7 path.extname(p)

获取一个路径的扩展名。它会返回一个扩展名格式的字符串。

const path = require('path');

console.log(path.extname('/foo/bar/baz/asdf/quux.html')); // '.html'

13.8 path.parse(pathString)

将一个路径字符串解析为一个对象。该对象包含以下属性:rootdirbasenameext

const path = require('path');

console.log(path.parse('/foo/bar/baz/asdf/quux.html'));
// {
//   root: '/',
//   dir: '/foo/bar/baz/asdf',
//   base: 'quux.html',
//   ext: '.html',
//   name: 'quux'
// }

总结

通过使用 path 模块的方法,可以方便地处理和操作文件路径和目录路径,避免手动处理字符串操作带来的错误和不兼容性。

14-Buffer

Buffer 是一个用于处理二进制数据的类。它的作用类似于 JavaScript 中的数组,但可以存储任意的二进制数据。

14.1 创建Buffer

Buffer 是以字节序列的形式表示二进制数据,并且可以在不同编码间进行转换。Buffer 在处理网络流、文件系统操作、加密和解密等方面都非常有用。在使用 Buffer 之前,需要先将它实例化。

可以使用以下方法之一来创建一个新的 Buffer 对象:

  1. Buffer.alloc(sizem, [fill], [encoding]):创建一个指定大小的 Buffer 对象,并将其填充为0或其他指定的值。

    const buf = Buffer.alloc(10); // 创建一个长度为10的 Buffer 对象,每个字节都填充为0
    
  2. Buffer.from(array):从一个数组中创建一个新的 Buffer 对象。

    const buf = Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]); // 创建一个包含ASCII字符串“buffer”的Buffer对象
    
  3. Buffer.from(string, [encoding]):从一个字符串中创建一个新的 Buffer 对象。

    const buf = Buffer.from('Hello, Node.js!', 'utf8'); // 创建一个包含UTF-8编码的字符串的Buffer对象
    

通过使用以上任意一种方法,都可以创建一个新的 Buffer 对象。接下来,您可以对其进行读写操作。

例如,可以通过给 Buffer 对象赋值来设置其内容。可以使用下标来访问和修改 Buffer 中的数据:

const buf = Buffer.alloc(4);
buf[0] = 0x61; // 字符 'a' 的ASCII码为0x61
buf[1] = 0x62; // 字符 'b' 的ASCII码为0x62
buf[2] = 0x63; // 字符 'c' 的ASCII码为0x63
buf[3] = 0x64; // 字符 'd' 的ASCII码为0x64

console.log(buf); // <Buffer 61 62 63 64>
console.log(buf.toString()); // 'abcd'

另外,也可以使用 buf.write(string, [offset], [length], [encoding]) 方法向 Buffer 对象中写入数据。

参数说明:

  • string:写入 buffer 的数据内容。
  • offset:可选参数,offset 指的是写入到 buffer 的位置,默认为 0
  • length:可选参数,写入到 buffer 的内容长度。
  • encoding:可选参数,写入内容的编码格式。

使用 buf.toString() 方法从 Buffer 对象中读取数据。

const buf = Buffer.alloc(11);
// 写入
const len = buf.write('Hello, Node');
console.log(`写入${len}个字节`);

const buf2 = Buffer.alloc(4);
// 写入
let len = buf2.write('abcd');
console.log(len); // 4
// 读取
console.log(buf2.toString()); // 'abcd'

Node.js 中,如果需要将多个 Buffer 对象拼接成一个 Buffer 对象时,可以使用 Buffer.concat(list, length) 方法:

const buf1 = Buffer.from('Hello');
const buf2 = Buffer.from('World');

const buf3 = Buffer.concat([buf1, buf2]);
console.log(buf3.toString()); // 'HelloWorld'

14.2 Buffer与字符编码

在处理文本数据时,我们需要将字符串转换为二进制数据类型,或者是将二进制数据转换为字符串类型,这时候需要用到字符串的编码格式。Buffer 对象用于处理二进制数据,由于字符编码是将字符映射到数字的过程,因此在处理字符串时需要考虑字符编码。

下面是字符串编码格式的详细说明:

编码 说明
ASCII ASCII编码是最早期的字符编码标准,使用7位二进制数表示128个字符,包括英文字母、数字和一些符号。它是一种单字节编码,每个字符占用一个字节的空间。
UTF-8 UTF-8是一种可变长度的Unicode字符编码,用于表示Unicode标准中的所有字符。在Node.js中,默认情况下使用UTF-8编码来处理字符串数据。这意味着大多数情况下,我们不需要显式地指定编码格式。
UTF-16LE UTF-16是一种针对Unicode的可变长度字符编码,使用1至2个16位编码单元(即2至4个字节)来表示一个字符。
UCS-2 UCS-2编码是一种定长编码格式,每个字符占用2个字节(16位),高位字节和低位字节分别存储字符的编码值。相比于UTF-8和UTF-16等可变长度编码格式,UCS-2编码的优势在于可以更方便地计算字符串的长度和索引。
Base64 Base64编码是一种常见的编码方式,用于将二进制数据转换为可打印字符的字符串表示。Base64编码由64个字符组成,通常包括A-Z、a-z、0-9以及两个额外的字符(通常是+和/),用于表示二进制数据中的各种字节组合。
Binary 这是二进制数据,字符串现在已经不推荐使用。
HEX Hex(十六进制)编码是一种常见的编码方式,用于将二进制数据转换为十六进制表示的字符串。Hex编码使用0-9和A-F这16个字符来表示一个字节的所有可能取值。

14.3 Buffer的转换

在 Node.js 中,可以使用 Buffer 类型来处理二进制数据。以下是一些常见的 Buffer 转换方法:

14.3.1 Buffer和字符串的转换

  1. Buffer转换为字符串

    使用 buf.toString([encoding], [start], [end]) 方法将 Buffer 对象转换为字符串。可以指定编码方式,默认为UTF-8编码。

    参数说明:

    • encoding:可选参数,指定要使用的字符编码。常见的编码方式包括 'utf8''ascii''latin1'等。
    • start:可选参数,指定要开始读取的索引位置,默认为0
    • end:可选参数,指定要结束读取的索引位置,默认为 Buffer 的长度。
    const buf = Buffer.from('Hello, Node.js!', 'utf8');
    const str = buf.toString('utf8');
    console.log(str); // 'Hello, Node.js!'
    
  2. 字符串转换为Buffer:

    使用 Buffer.from(string, [encoding]) 方法将字符串转换为 Buffer 对象。

    参数说明:

    • string:要转换为Buffer的字符串。
    • encoding:可选参数,指定要使用的字符编码,默认为UTF-8编码。
    const str = 'Hello, Node.js!';
    const buf = Buffer.from(str, 'utf8');
    console.log(buf); // <Buffer 48 65 6c 6c 6f 2c 20 4e 6f 64 65 2e 6a 73 21>
    
  3. Buffer转换为十六进制字符串

    使用 Buffer.from(string, ‘hex’) 方法将十六进制字符串转换为 Buffer 对象。

    参数说明:

    • encoding:可选参数,指定要使用的字符编码,默认为UTF-8编码。
    const buf = Buffer.from('Hello, Node.js!', 'utf8');
    const hexStr = buf.toString('hex');
    console.log(hexStr); // '48656c6c6f2c204e6f64652e6a7321'
    
  4. 十六进制字符串转换为Buffer:

    使用 Buffer.from(string, ‘hex’) 方法将十六进制字符串转换为 Buffer 对象。

    const hexStr = '48656c6c6f2c204e6f64652e6a7321';
    const buf = Buffer.from(hexStr, 'hex');
    console.log(buf); // <Buffer 48 65 6c 6c 6f 2c 20 4e 6f 64 65 2e 6a 73 21>
    

这些转换方法使得在 Node.js 中处理不同数据格式之间的转换变得非常方便。需要注意的是,在进行转换时应确保所使用的编码方式是一致的,以避免出现数据解析错误或乱码的问题。

14.3.2 Buffer和JSON对象的转换

在 Node.js 中,Buffer 对象用于处理二进制数据,而 JSON 对象用于处理 JavaScript 对象的序列化和反序列化。因此,有时我们需要在它们之间进行转换,以便在处理数据时能够方便地在二进制数据和 JavaScript 对象之间进行转换。

以下是如何在 Node.js 中进行 Buffer 和 JSON 对象之间的转换:

  1. 将Buffer对象转换为JSON对象:

    const buf = Buffer.from('Hello, 世界', 'utf8');
    const jsonStr = buf.toJSON();
    console.log(jsonStr); // 输出:{"type":"Buffer","data":[72,101,108,108,111,44,32,228,184,150,231,149,140]}
    
  2. 将JSON对象转换为Buffer对象:

    const jsonStr = '{"type":"Buffer","data":[72,101,108,108,111,44,32,228,184,150,231,149,140]}';
    const buf = Buffer.from(JSON.parse(jsonStr).data);
    console.log(buf.toString('utf8')); // 输出:Hello, 世界
    

在上述示例中,使用 Buffer 对象的 toJSON 方法可以将 Buffer 对象转换为符合 JSON 格式的对象,其中包含了 Buffer 数据的类型和具体的字节数组。而使用 JSON 对象的 parse 方法可以将符合特定格式的 JSON 字符串转换为 JavaScript 对象,然后再通过 Buffer.from 方法将字节数组转换回 Buffer 对象。

在实际开发中,这种转换通常在处理网络数据、文件读写等场景中会经常用到。例如,当从网络接收到的数据是 Buffer 类型时,可以将其转换为 JSON 对象以便进行传输、存储或展示;相反,当需要将收到的 JSON 对象中的二进制数据恢复为 Buffer 对象时,也可以使用上述方法进行转换。

14.4 Buffer的其他方法

  • buf.length:length 属性返回 Buffer 对象的长度。
  • Buffer.byteLength(string, [encoding]):返回 Buffer 对象占用的字节数。
  • buf.compare(otherBuffer):用于比较两个 Buffer 对象的内容是否相等。
  • buf.copy(target, [targetStart], [sourceStart], [sourceEnd]):用于将当前 Buffer 对象中的数据复制到另一个 Buffer 对象中。
  • Buffer.isBuffer(obj):判断一个对象是否是一个 Buffer 对象。
  • Buffer.isEncoding():用于判断一个字符串是否为一个有效的编码格式字符串。

总结

Buffer 类在 Node.js 中被广泛应用于处理文件、网络通信、加密解密等场景。由于它是基于原始二进制数据的,因此可以高效地进行数据操作和处理。

需要注意的是,在使用 Buffer 时应当小心处理内存的使用,避免出现内存泄漏或溢出的情况。同时,随着 Node.js 版本的更新,对 Buffer 的使用也有一些变化,开发者需要根据具体的 Node.js 版本来使用相应的 Buffer API。

15-Stream

Stream(流)是一种用于处理流式数据的抽象接口。它提供了一种逐块处理数据的方式,可以有效地处理大量的数据,同时减少内存占用和响应时间。Node.js 中的 Stream 模块提供了丰富的API来创建、读取、写入和处理流。

在 Node.js 中,流可以分为四种类型:

  1. 可读流(Readable Streams):可读流用于从数据源读取数据,例如文件读取流、HTTP 请求响应流等。可读流的特点是可以逐块地读取数据,而不需要一次性将所有数据存储在内存中。通过监听data事件或使用read()方法,可以逐块地获取数据。

  2. 可写流(Writable Streams):可写流用于向目标写入数据,例如文件写入流、HTTP 请求发送流等。可写流的特点是可以逐块地写入数据,而不需要一次性将所有数据存储在内存中。通过 write() 方法将数据逐块写入目标。

  3. 双工流(Duplex Streams):双工流即可读又可写,既可以从数据源读取数据,又可以向目标写入数据。一个常见的例子是网络套接字,它既可以接收数据也可以发送数据。

  4. 转换流(Transform Streams):转换流是一种特殊的双工流,它可以通过对输入数据进行转换来生成输出数据。转换流通常用于对数据进行处理,例如数据压缩、加密、解密等操作。

Node.js 中的 Stream 对象通常会继承自 EventEmitter。由于 Stream 继承自 EventEmitter,因此 Stream 对象也具有 EventEmitter 的所有功能。Stream 对象可以发出各种事件,如:dataenderror等,这些事件可以被监听器捕获,并在事件发生时执行相应的回调函数。例如,在可读流中,当有新的数据可用时,可读流会触发 data 事件,此时监听器可以对数据进行处理。

下面是 Stream 对象中常见的事件:

  • data:有新数据可用时触发。
  • end:数据读取完毕时触发。
  • error:数据写入缓冲区后触发。
  • finish:数据写入完成时触发。
  • drain:数据写入缓冲区后触发。

15.1 从Stream中读取数据

在 Node.js 中,可以通过可读流(Readable Stream)从数据源中读取数据。

// 1.引入fs模块
const fs = require('fs');
// 2.创建可读流对象
const readableStream = fs.createReadStream('file.txt');

// 设置编码为 utf8。(可选)
readerStream.setEncoding('UTF8');

// 3.为可读流添加事件监听器
readableStream.on('data', (chunk) => {
  // 处理每个数据块(chunk)
  console.log(chunk.toString()); // 可以转换或存储
});

readableStream.on('end', () => {
  // 数据读取完毕,关闭可读流 (可选)
  readableStream.close();
});

readableStream.on('error', (err) => {
  // 处理错误,关闭可读流 (可选)
  readableStream.close();
});

注意:
1. 在使用可读流读取大型文件时,数据可能会分为多个数据块,因此需要适当处理每个数据块的到达和结束的情况。
2. 通常情况下,不需要手动关闭可读流。当所有数据都被读取完毕时,可读流会自动触发end事件,表示数据读取结束。
3. 如果需要手动关闭可读流,可以调用close()方法进行关闭操作。

15.2 写入Stream

创建可写流对象,调用 write() 方法写入数据,最后使用 end() 方法结束写入操作。同时,可以通过事件机制监听数据写入缓冲区后、写入完成或发生错误时的情况。

// 1.引入fs模块
const fs = require('fs');
// 2.创建可写流对象
const writableStream = fs.createWriteStream('output.txt');
// 3.将数据写入目标
writableStream.write('Hello, world!', 'utf-8');
// 4.结束写入操作
writableStream.end();

writableStream.on('drain', () => {
  // 数据写入缓冲区后触发
});

writableStream.on('finish', () => {
  // 数据写入完成时触发
});

writableStream.on('error', (err) => {
  // 处理错误
});

注意:
1. 在写入完所有数据后,应该调用end()方法来结束写入操作,这会触发 finish 事件,表示数据写入完成。
2. 通常情况下,也不需要手动关闭可写流。在调用 end() 方法后,可写流会自动完成关闭操作。
3. 如果需要手动关闭可写流,同样可以调用 end() 方法或 destroy() 方法来关闭可写流。

15.3 管道流

管道流(Piping)是一种将可读流(Readable Stream)和可写流(Writable Stream)连接起来的方式。通过管道流,可以将数据从一个流传输到另一个流,而无需手动处理数据缓冲区或流结束等情况。这样可以简化代码,并提高性能。以下是使用管道流的基本步骤:

// 1.引入fs模块
const fs = require('fs');

// 2.创建可读流和可写流对象
const readableStream = fs.createReadStream('input.txt'); // 创建一个从文件中读取数据的可读流
const writableStream = fs.createWriteStream('output.txt'); // 创建一个向文件中写入数据的可写流

// 3.建立管道连接
readableStream.pipe(writableStream); // // 读取 input.txt 文件内容,并将内容写入到 output.txt 文件中

以上就是使用管道流的基本步骤。通过建立管道连接,可以自动将可读流中的数据传输到可写流中,无需手动处理数据缓冲区或流结束等情况。管道流可以简化代码,并提高性能。

注意:在使用管道流时,应该避免出现循环管道(Circular Piping)的情况,这会导致程序死锁。例如,下面的代码就会导致循环管道:

readableStream.pipe(writableStream);
writableStream.pipe(readableStream);

15.4 链式流

链式流(Chained Streams)是一种将多个流(Stream)连接起来形成一个流处理管道的方式,通过串联多个流,可以方便地对数据进行多次处理。在Node.js中,链式流可以通过多次调用pipe()方法来实现。

// 1.引入fs模块
const fs = require('fs');

// 2.创建可读流和可写流对象
const readableStream = fs.createReadStream('input.txt'); // 创建一个从文件中读取数据的可读流
const writableStream = fs.createWriteStream('output.txt'); // 创建一个向文件中写入数据的可写流

// 3.创建转换流对象
// 使用Transform类或其他相关类创建转换流(Transform Stream)对象,用于对数据进行处理。例如,使用zlib.createGzip()方法创建一个压缩转换流。
const zlib = require('zlib');
const gzip = zlib.createGzip();

// 4.建立链式连接
// 使用pipe()方法建立链式连接,将多个流对象串联起来。这样,可读流中的数据会经过多个流对象自动传输,并进行相应的处理。
readableStream.pipe(gzip).pipe(writableStream);

以上就是使用链式流的基本步骤。通过建立链式连接,可以方便地对数据进行多次处理,例如将数据进行压缩、加密等操作后再写入文件。在链式流中,每个流对象都是一个独立的处理单元,可以根据需要添加或删除流对象。

注意:在使用链式流时,也应该避免出现循环链式(Circular Chaining)的情况,这会导致程序死锁。例如,下面的代码就会导致循环链式:

readableStream.pipe(gzip).pipe(writableStream);
writableStream.pipe(readableStream);

总结

Node.js 的流提供了一种高效处理数据的方式,特别适合处理大型数据集或需要实时处理的场景。通过使用流,可以减少内存占用、提高响应速度,并且提供了灵活的数据处理管道。

16-系统 os

Node.js 的 os 模块提供了一些与操作系统相关的实用方法,可以用于获取有关操作系统的信息。以下是 os 模块中一些常用的方法和属性:

方法 描述 |
os.EOL 根据不同的操作系统生成不同的换行符。
os.arch() 返回当前计算机的处理器架构,如 x64、arm、ia32 等。
os.cpus() 返回一个对象数组,描述当前计算机的 CPU 内核信息,包括型号、速度、时间戳等。
os.endianness() 返回当前计算机的字节序(大端字节序或小端字节序)。
os.freemem() 返回当前计算机的空闲内存量(以字节为单位)。
os.totalmem() 返回当前计算机的总内存量(以字节为单位)。
os.hostname() 返回当前计算机的主机名。
os.loadavg() 返回最近 1 分钟、5 分钟和 15 分钟内的系统平均负载。
os.networkInterfaces() 返回当前计算机的网络接口信息,包括 IP 地址、子网掩码、MAC 地址等。
os.platform() 返回当前计算机的操作系统平台,如 win32、linux、darwin 等。
os.release() 返回当前计算机的操作系统版本号。
os.tmpdir() 返回操作系统的默认临时文件目录路径。
os.type() 返回当前计算机的操作系统类型,如 Linux、Windows_NT 等。
os.uptime() 返回当前计算机自上次启动以来的运行时间(以秒为单位)。

下面是os模块的一些示例:

const os = require('os');

console.log("Hello" + os.EOL + "World!"); // 换行
console.log(os.arch()); // 输出: x64
console.log(os.cpus()); // 输出cpu核心数
console.log(os.totalmem()); // 返回空闲系统内存的字节数
console.log(os.totalmem()); // 返回系统总内存的字节数
console.log(os.hostname()); // 返回主机名
console.log(os.networkInterfaces()); // 返回一个对象,其中包含有关网络接口的信息
console.log(os.platform()); // 返回操作系统平台
console.log(os.release()); // 返回操作系统版本
console.log(os.tmpdir()); // 返回默认临时文件目录的路径
console.log(os.type()); // 返回操作系统类型
console.log(os.uptime()); //  返回系统运行时间,以秒为单位

除了上述函数之外,os 模块还提供了其他一些函数和属性,可用于查询和与操作系统交互。例如,os.userInfo() 函数可用于获取当前用户的信息,os.constants 属性包含了操作系统的常量。

os 模块还提供了其他一些实用的方法,可以帮助开发者获取操作系统相关的信息,从而更好地编写跨平台的 Node.js 应用程序。如果有兴趣,可以查看 文档

总结

os 模块可以帮助开发者获取操作系统和系统硬件相关的信息,从而更好地编写跨平台的 Node.js 应用程序。

17-url和queryString

17.1 url

Node.js 的 url 模块是用于处理URL(Uniform Resource Locator)的模块。它提供了一组方法,用于解析、格式化和处理URL的各个部分。

17.1.1 解析URL

  • url.parse(urlString, [parseQueryString], [slashesDenoteHost]) 方法将一个URL字符串解析为一个URL对象。

    参数说明:

    • urlString 是要解析的URL字符串。
    • parseQueryString 是一个可选参数,用于指定是否解析查询字符串,默认为 false
    • slashesDenoteHost 也是一个可选参数,用于指定是否将双斜杠视为主机的标记,默认为 false

解析后的结果以URL对象的形式返回,包含了URL的各个部分:

  • protocol:协议部分(例如 http:)。
  • slashes:是否有双斜杠。
  • auth:认证信息部分(例如 username:password)。
  • host:主机部分(例如 www.example.com:8080)。
  • port:端口部分(字符串类型)。
  • hostname:主机名部分(例如 www.example.com)。
  • hash:片段标识符部分(例如 #section)。
  • search:查询字符串部分(例如 ?name=John&age=25)。
  • query:查询参数部分,以对象形式存储(例如 { name: ‘John’, age: ‘25’ })。
  • pathname:路径部分(例如 /path)。
  • path:路径和查询字符串部分(例如 /path?name=John&age=25)。
  • href:完整的URL字符串。

17.1.2 格式化URL

  • url.format(urlObject) 方法将一个URL对象格式化为一个URL字符串。

    • urlObject 是要格式化的URL对象,包含了URL的各个部分。

格式化后的URL字符串以字符串形式返回。

17.1.3 解析相对URL

  • url.resolve(from, to) 方法根据基础URL和相对URL生成一个完整的URL。

    参数说明:

    • from 是基础URL。
    • to 是相对URL。

生成的完整URL以字符串形式返回。

17.1.4 URLSearchParams

  • url.URLSearchParams 是一个构造函数,用于创建URL查询参数的实例。它提供了一组方法,用于操作URL查询参数,如添加、获取、删除参数等。

使用示例:

const url = require('url');

// 解析URL
const urlString = 'https://www.example.com:8080/path?name=John&age=25';
const parsedUrl = url.parse(urlString, true);
console.log(parsedUrl);
/*
输出:
{
  protocol: 'https:',
  slashes: true,
  auth: null,
  host: 'www.example.com:8080',
  port: '8080',
  hostname: 'www.example.com',
  hash: null,
  search: '?name=John&age=25',
  query: { name: 'John', age: '25' },
  pathname: '/path',
  path: '/path?name=John&age=25',
  href: 'https://www.example.com:8080/path?name=John&age=25'
}
*/

// 构建URL
const urlObject = {
  protocol: 'https:',
  host: 'www.example.com',
  pathname: '/path',
  search: '?name=John&age=25'
};
const formattedUrl = url.format(urlObject);
console.log(formattedUrl);
// 输出: https://www.example.com/path?name=John&age=25

// 解析相对URL
const base = 'https://www.example.com/';
const relative = 'about';
const resolvedUrl = url.resolve(base, relative);
console.log(resolvedUrl);
// 输出: https://www.example.com/about

// 使用URLSearchParams
const params = new url.URLSearchParams('name=John&age=25');
params.append('city', 'New York');
console.log(params.toString());

除了以上介绍的方法,url模块还提供了其他一些辅助方法,如 url.resolveObject()url.domainToASCII()url.domainToUnicode() 等,用于更详细地处理URL相关的操作。

url 模块是 Node.js 中处理URL的重要工具,它使得解析、构建和处理URL变得简单和方便。可以用于处理HTTP请求、路由匹配等场景。

17.2 queryString

queryString 模块是用于处理URL查询字符串的模块。查询字符串是URL中的一部分,通常用于将数据传递给服务器或从服务器接收数据。

queryString 模块提供了一组方法,可以解析和序列化查询字符串。下面是一些常用的方法:

  • queryString.parse(str, [sep], [eq], [options]):该方法将查询字符串解析为一个对象。其中,str是要解析的查询字符串,sep是用于分隔键值对的字符,默认为'&'eq是用于分隔键和值的字符,默认为'='options是一个可选的参数对象,可以用于定义其他选项。解析后的结果以键值对的形式存储在对象中。

  • queryString.stringify(obj, [sep], [eq], [options]]):该方法将一个对象序列化为查询字符串。其中,obj是要序列化的对象,sep是键值对之间的分隔符,默认为'&'eq是键和值之间的分隔符,默认为'='options是一个可选的参数对象,可以用于定义其他选项。序列化后的查询字符串以键值对的形式返回。

  • queryString.escape(str):该方法用于对字符串进行编码,使其可以安全地用作URL的一部分。编码后的字符串以URL编码的形式返回。

  • queryString.unescape(str):该方法用于对已编码的字符串进行解码,使其恢复为原始字符串。解码后的字符串以原始形式返回。

const queryString = require('querystring');

// 解析查询字符串
const params = queryString.parse('name=John&age=25');
console.log(params);
// 输出: { name: 'John', age: '25' }

// 序列化对象
const obj = { name: 'John', age: '25' };
const query = queryString.stringify(obj);
console.log(query);
// 输出: name=John&age=25

// 编码和解码
const encoded = queryString.escape('Hello, World!');
console.log(encoded);
// 输出: Hello%2C%20World%21

const decoded = queryString.unescape('Hello%2C%20World%21');
console.log(decoded);
// 输出: Hello, World!

以上是 queryString 模块的一些基本介绍和使用方法。它可以帮助你方便地处理URL查询字符串的解析和序列化操作。

总结

url 模块适用于操作和解析 URL,而 querystring 模块适用于操作和解析查询字符串。在 Node.js Web 开发中,这两个模块经常被用于处理 URL 和查询参数的相关操作。

18-dns

在 Node.js 中,dns 模块是用于进行DNS(域名系统)解析的核心模块。它提供了一系列方法来查询域名相关的信息,如 IP地址主机名MX记录等。

dns 模块提供了以下常用的方法:

18.1 dns.lookup(hostname, [options], callback)

该方法用于将域名解析为第一个找到的IPv4或IPv6地址。它接受三个参数:

  • hostname:要解析的域名。
  • options:一个可选的对象,用于配置解析行为。
  • callback:一个回调函数,用于处理解析结果。

示例代码:

const dns = require('dns');

dns.lookup('www.example.com', (err, address, family) => {
  console.log(`IP地址: ${address}`);
});

18.2 dns.resolve(hostname, [rrtype], callback)

该方法用于将域名解析为指定类型的记录。它接受三个参数:

  • hostname:要解析的域名。

  • rrtype:一个可选的字符串,表示要解析的记录类型,默认为'A'
    以下是一些常见的 DNS 记录类型:

    • ‘A’:表示 IPv4 地址记录。

    • ‘AAAA’:表示 IPv6 地址记录。

    • ‘CNAME’:表示别名记录,将一个域名解析到另一个域名,以便重用现有的 DNS 信息。

    • ‘MX’:表示邮件交换记录,指定邮件服务器的优先级和名称。

    • ‘NS’:表示命名服务器记录,指定域名权威命名服务器的名称。

    • ‘PTR’:表示指针记录,将 IP 地址解析到主机名。

    • ‘SOA’:表示起始授权记录,描述 DNS 区域的属性和 DNS 服务器的配置。

    • ‘SRV’:表示服务记录,指定提供特定服务的服务器的名称和端口号。

    • ‘TXT’:表示文本记录,包含任意文本信息。

  • callback:一个回调函数,用于处理解析结果。

示例代码:

const dns = require('dns');

dns.resolve('www.example.com', 'MX', (err, addresses) => {
  console.log('MX记录:');
  addresses.forEach((address) => {
    console.log(address);
  });
});

18.3 dns.reverse(ip, callback)

该方法用于将IP地址解析为域名。它接受两个参数:

  • ip:要解析的IP地址。
  • callback:一个回调函数,用于处理解析结果。

示例代码:

const dns = require('dns');

dns.reverse('8.8.8.8', (err, hostnames) => {
  console.log('域名:');
  hostnames.forEach((hostname) => {
    console.log(hostname);
  });
});

除了上述方法,dns 模块还提供了其他一些方法,如 dns.resolve4() 用于解析 IPv4 地址,dns.resolve6() 用于解析 IPv6 地址,dns.resolveCname() 用于解析 CNAME 记录等。

方法 说明
dns.resolve4(hostname, [options], callback) 执行 DNS 解析以查找给定主机名的 IPv4 地址记录。可选的 options 参数用于指定查询的服务器。
dns.resolve6(hostname, [options], callback) 执行 DNS 解析以查找给定主机名的 IPv6 地址记录。可选的 options 参数用于指定查询的服务器。
dns.resolveCname(hostname, callback) 执行 DNS 解析以查找给定主机名的 CNAME 记录。返回结果是一个数组,包含该主机名的所有别名。
dns.resolveMx(hostname, callback) 执行 DNS 解析以查找给定主机名的 MX 记录。返回结果是一个数组,包含所有与该主机名关联的邮件交换服务器。
dns.resolveNs(hostname, callback) 执行 DNS 解析以查找给定主机名的 NS 记录。返回结果是一个数组,包含所有与该主机名关联的命名服务器。
dns.resolvePtr(ip, callback) 执行 DNS 解析以查找给定 IP 地址的 PTR 记录。返回结果是一个数组,包含与该 IP 地址关联的主机名。
dns.resolveSrv(hostname, callback) 执行 DNS 解析以查找给定主机名的 SRV 记录。返回结果是一个数组,包含提供特定服务的服务器的名称和端口号。
dns.resolveTxt(hostname, callback) 执行 DNS 解析以查找给定主机名的 TXT 记录。返回结果是一个数组,包含与该主机名关联的任意文本信息。
dns.reverse(ip, callback) 执行反向 DNS 查询以查找给定 IP 地址的主机名。返回结果是一个数组,包含与该 IP 地址关联的所有主机名。

options 参数是一个可选的对象,用于指定 DNS 查询的选项。该参数具有以下属性:

  • ttl(Time to Live):表示 DNS 查询结果的生存时间,以秒为单位。默认情况下,查询结果会被缓存一段时间,以减少对 DNS 服务器的负载。通过设置 ttl 属性,可以控制查询结果的缓存时间。如果将 ttl 设置为 0,则禁用缓存。

  • timeout:表示 DNS 查询的超时时间,以毫秒为单位。如果查询超过指定的超时时间仍未完成,将触发错误回调。默认超时时间为 0,表示无限制。

  • family:表示要查询的 IP 地址族(IPv4 或 IPv6)。可接受的值为 46。如果未指定此选项,则默认查询 IPv4 地址记录。

  • hints:表示查询期望返回的类型的提示。可接受的值为 dns.ADDRCONFIGdns.V4MAPPEDdns.ALLdns.ADDRCONFIG 表示只返回与本地 IP 地址配置匹配的地址记录。dns.V4MAPPED 表示返回 IPv6 地址记录时,同时返回映射到 IPv4 的地址记录。dns.ALL 表示返回所有可能的地址记录。默认情况下,hints 设置为 dns.ADDRCONFIG

  • all:表示是否返回所有符合查询条件的结果。默认情况下,只返回一个 IP 地址。如果将 all 设置为 true,则返回所有符合条件的 IP 地址。

需要注意的是,DNS解析是一个异步操作,所有的方法都接受一个回调函数作为最后一个参数,并使用回调函数来处理解析结果。

18.4 dns 模块中的错误代码

当进行 DNS 查询时,可能会返回错误代码以指示查询过程中发生的问题。以下是一些常见的 dns 模块中的错误代码:

  • dns.NODATA:表示没有找到与查询匹配的数据。

  • dns.FORMERR:表示 DNS 查询格式错误。

  • dns.SERVFAIL:表示 DNS 服务器内部错误或服务不可用。

  • dns.NOTFOUND:表示找不到指定的域名。

  • dns.NOTIMP:表示 DNS 服务器不支持请求的操作。

  • dns.REFUSED:表示 DNS 服务器拒绝了请求。

  • dns.BADQUERY:表示 DNS 查询格式错误。

  • dns.BADNAME:表示域名格式错误。

这些错误代码可用于在处理 DNS 查询时判断查询结果的状态,根据不同的错误代码采取相应的处理逻辑。在使用 dns 模块进行 DNS 查询时,可以根据需要捕获和处理这些错误代码来确保 DNS 查询的准确性和稳定性。

总结

通过使用 dns 模块,可以方便地进行 DNS 查询和解析,实现域名与 IP 地址之间的转换,以及获取其他 DNS 记录类型的信息。

19-其他模块

19.1 加密与解密

Node.js 的 crypto 模块是 Node.js 的核心模块之一,为开发者提供了一套完整的加密功能。该模块包括了用于OpenSSL散列、HMAC、加密、解密、签名以及验证的函数的封装。

在 crypto 模块中,包含了多种加密和解密的方式,如对称加密和非对称加密等。例如,你可以使用它进行哈希值的计算,或者使用HMAC进行数据的验证。同时,它还支持密码器和解密器的操作。

crypto.getCiphers() 是一个 Node.js 中的函数,用于获取当前系统支持的所有加密算法。它返回一个包含所有可用加密算法名称的数组。

以下是一个示例代码,演示如何使用 crypto.getCiphers() 函数

const crypto = require('crypto');

// 获取当前系统支持的所有加密算法
const ciphers = crypto.getCiphers();
console.log(ciphers);

crypto.getHashes() 是一个 Node.js 中的函数,用于获取当前系统支持的所有哈希算法。它返回一个包含所有可用哈希算法名称的数组。

以下是一个示例代码,演示如何使用 crypto.getHashes() 函数:

const crypto = require('crypto');

// 获取当前系统支持的所有哈希算法
const hashes = crypto.getHashes();
console.log(hashes);

19.1.1 随机数

在 Node.js 中,可以使用 crypto 模块来生成随机数和加密功能。下面我将介绍如何使用 crypto 模块生成随机数。

const crypto = require('crypto');
// 使用 crypto.randomBytes 方法可以生成具有指定字节数的随机数。例如,生成一个 16 字节(128 位)的随机数
const randomBytes = crypto.randomBytes(16);
console.log(randomBytes)

// randomBytes 是一个 Buffer 对象,如果需要将其转换为可读的字符串格式,可以使用 toString 方法,并指定编码格式。例如,将随机数转换为十六进制字符串:
const randomStringHex = randomBytes.toString('hex');
console.log(randomString)
// 可以将其转换为 base64 编码的字符串:
const randomString64 = randomBytes.toString('base64');
console.log(randomString64)

19.1.2 散列算法

散列算法,也称为哈希算法,是一种将任意长度的输入数据转换为固定长度输出(通常称为哈希值或摘要)的算法。这种转换是一种压缩映射,即哈希值的空间通常远小于输入的空间,可能存在不同的输入映射到相同的哈希值,这种情况称为冲突。

Node.js 提供了多种散列算法,包括MD5、SHA-1、SHA-256等。这些算法都是不可逆的,可以将任意长度的数据转换为固定长度的哈希值。

以下是 Node.js 中常用的散列算法:

  • MD5(Message-Digest Algorithm 5):将数据转换为一个128位的哈希值。由于其安全性较低,已经被证明存在漏洞,因此不建议在需要高安全性的场景中使用。

  • SHA-1(Secure Hash Algorithm 1):将数据转换为一个160位的哈希值。与MD5类似,SHA-1也存在安全问题,因此也不建议在需要高安全性的场景中使用。

  • SHA-256(Secure Hash Algorithm 256):将数据转换为一个256位的哈希值。SHA-256是目前最常用的安全哈希算法之一,被广泛应用于密码存储、数字签名等领域。

  • SHA-3(Secure Hash Algorithm 3):是SHA-2系列的第三个版本,将数据转换为一个224、256或384位的哈希值。SHA-3比SHA-2更安全,但目前还没有得到广泛的应用。

Node.js中,散列算法的常用方法主要包括 crypto 模块中的一些函数。以下是一些常用的散列方法:

  • crypto.createHash(algorithm):创建一个哈希对象,algorithm参数指定了使用的哈希算法,如'sha256''md5'等。

  • hash.update(data, input_encoding):更新哈希对象的数据,data参数是要进行哈希计算的数据,input_encoding参数是数据的编码格式,如'utf8''ascii'等。

  • hash.digest(output_encoding):计算哈希对象的哈希值,output_encoding参数是哈希值的编码格式,如'hex''base64'等。

以下是一个使用SHA-256算法计算字符串哈希值的例子:

const crypto = require('crypto');

const hash = crypto.createHash('sha256');
hash.update('hello world', 'utf8');
console.log(hash.digest('hex')); // 输出哈希值的十六进制表示形式

此外,散列表(Hash Table)也是 Node.js 中常用的数据结构之一,它提供了快速的插入和取用操作。在散列表上插入、删除和取用数据是非常快的,但是对于查找操作来说却是效率低下。

19.1.3 HMAC

HMAC (Hash-based Message Authentication Code) 是一种基于哈希 HMAC HMAC(Hash-based Message Authentication Code)是一种基于哈希函数的消息认证码算法,用于验证消息的完整性和身份认证。在 Node.js 中,可以使用 crypto 模块提供的 createHmac() 方法来创建 HMAC 对象,并使用 update()digest() 方法来计算HMAC值。

Node.js 中的 HMAC 算法的常用方法包括:

crypto.createHmac(algorithm, key):创建一个HMAC对象,并指定使用的哈希算法和密钥。

  • hmac.update(data):将数据添加到HMAC对象中。

  • hmac.digest(encoding):计算HMAC值,并以指定的编码格式输出。

  • hmac.final():结束计算HMAC值的过程,并将结果存储在内部缓冲区中。

  • hmac.copy(hmac):将一个HMAC对象的内容复制到另一个HMAC对象中。

  • hmac.reset():重置HMAC对象的状态,以便重新使用它。

  • hmac.setEncoding(encoding):设置HMAC对象的编码格式。

  • hmac.write(data):将数据写入HMAC对象中。

  • hmac.toString(encoding):将HMAC值转换为字符串,并以指定的编码格式输出。

  • hmac.toBuffer():将HMAC值转换为 Buffer 对象。

以下是一个简单的示例代码,演示如何使用 Node.js 中的HMAC算法:

const crypto = require('crypto');

// 定义密钥和消息
const secretKey = 'mysecretkey';
const message = 'Hello, world!';

// 创建HMAC对象,指定使用的哈希算法为SHA256
const hmac = crypto.createHmac('sha256', secretKey);

// 更新HMAC对象的数据
hmac.update(message);

// 计算HMAC值,并以十六进制字符串形式输出
console.log(hmac.digest('hex')); // 输出:'c8e4e9e0e7e8e9e0e7e8e9e0e7e8e9e0e7e8e9e0e7e8e9e0e7e8e9e0e7e8e9e'

在上面的示例中,我们首先引入了 crypto 模块,然后定义了一个密钥和一个消息。接着,我们使用 createHmac() 方法创建了一个 HMAC 对象,并指定了使用的哈希算法为 SHA256。然后,我们使用 update() 方法将消息添加到 HMAC 对象中。最后,我们使用 digest() 方法计算了 HMAC 值,并以十六进制字符串形式输出。

19.1.4 非对称加密

非对称加密是一种公钥加密算法,也被称为公钥密码学。与对称加密算法不同,非对称加密使用一对密钥:公钥和私钥。

公钥可以自由传播给其他人,而私钥必须严格保密。通过这对密钥,非对称加密算法可以实现以下功能:

  • 加密:使用公钥加密数据。任何人都可以使用公钥对数据进行加密,但只有持有相应私钥的人才能解密。
  • 解密:使用私钥解密数据。只有私钥的持有者才能使用私钥解密以公钥加密的数据。
  • 数字签名:使用私钥对数据进行签名。私钥的持有者可以对数据进行签名,验证者可以使用公钥验证签名的真实性。

非对称加密算法的安全性基于数学上的难题,例如大数分解、离散对数等。常见的非对称加密算法包括RSA(Rivest-Shamir-Adleman)、DSA(Digital Signature Algorithm)和ECC(Elliptic Curve Cryptography)等。

在 Node.js 中,可以使用内置的 crypto 模块来实现非对称加密算法。crypto 模块提供了各种加密、解密、签名和验证等功能,包括 RSA 和 ECDSA 等非对称加密算法的实现。

下面详细介绍一下 crypto 模块中常用的非对称加密方法:

  1. crypto.generateKeyPair(type[, options], callback):生成公钥和私钥对。其中,type参数指定生成密钥对的算法,例如'rsa'表示使用RSA算法,'ec'表示使用ECDSA算法。options参数为可选配置项,例如密钥长度、编码格式等。callback参数为生成密钥对完成后的回调函数,错误优先的回调风格(error-first callback)。
const crypto = require('crypto');

// 生成密钥对
crypto.generateKeyPair('rsa', {
  modulusLength: 2048,
  publicKeyEncoding: {
    type: 'spki',
    format: 'pem'
  },
  privateKeyEncoding: {
    type: 'pkcs8',
    format: 'pem'
  }
}, (err, publicKey, privateKey) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log(publicKey);
  console.log(privateKey);
});
  1. crypto.privateEncrypt(privateKey, data):使用私钥对数据进行加密。其中,privateKey参数为私钥,可以是PEM编码的字符串或者一个 Buffer 对象;data参数为需要加密的数据,可以是一个字符串或者一个 Buffer 对象。
// 私钥加密数据
const encryptedData = crypto.privateEncrypt(privateKey, Buffer.from('要加密的数据'));
console.log(encryptedData.toString('base64'));
  1. crypto.publicDecrypt(publicKey, encryptedData):使用公钥对数据进行解密。其中,publicKey参数为公钥,可以是PEM编码的字符串或者一个 Buffer 对象;encryptedData参数为加密后的数据,必须是一个 Buffer 对象。
// 公钥解密数据
const decryptedData = crypto.publicDecrypt(publicKey, encryptedData);
console.log(decryptedData.toString());
  1. crypto.sign(algorithm, data, privateKey[, format]):使用私钥对数据进行签名。其中,algorithm参数为签名算法,例如'sha256'表示使用SHA-256算法;data参数为需要签名的数据,可以是一个字符串或者一个Buffer对象;privateKey参数为私钥,可以是PEM编码的字符串或者一个Buffer对象;format参数为可选参数,指定返回值的格式,默认为'buffer',也可以指定为'hex''base64'等。
// 数字签名
const sign = crypto.sign('sha256', Buffer.from('要签名的数据'), privateKey, 'hex');
console.log(sign);
  1. crypto.verify(algorithm, data, publicKey, signature[, format]):使用公钥验证数据的签名是否有效。其中,algorithm参数为签名算法,例如'sha256'表示使用SHA-256算法;data参数为需要验证签名的数据,可以是一个字符串或者一个 Buffer 对象;publicKey参数为公钥,可以是PEM编码的字符串或者一个Buffer对象;signature参数为需要验证的签名,可以是一个字符串或者一个Buffer对象;format参数为可选参数,指定签名的格式,默认为'buffer',也可以指定为'hex''base64'等。
// 验证签名
const isValidSignature = crypto.verify('sha256', Buffer.from('需要验证的数据'), publicKey, sign, 'hex');
console.log(isValidSignature);

注意:
1. 使用公钥加密就需要私钥解密,使用私钥加密就必须使用公钥解密。
2. createCipher已被标记为不推荐使用,并在 Node.js 版本16.0中被移除。

使用这些方法可以实现非对称加密、解密和数字签名等功能。同时,由于非对称加密算法的安全性较高,建议在实际应用中选择合适的算法和密钥长度,以确保应用的安全性。

19.2 压缩与解压缩

zlib 模块是 Node.js 内置的压缩模块,它提供了多种压缩和解压缩算法,并且可以用于对数据进行流式压缩和解压缩。下面我们将详细介绍zlib模块的主要方法。

19.2.1 zlib.deflate(buf, [options], callback)

使用DEFLATE算法对buf数据进行压缩。options对象可以指定压缩级别(默认值为 zlib.constants.Z_DEFAULT_COMPRESSION),以及其他一些参数,例如内存限制、字典等。callback函数接收两个参数:err和buffer,其中buffer参数包含压缩后的数据。

const zlib = require('zlib');

const input = Buffer.from('hello world');
zlib.deflate(input, (err, buffer) => {
	if (!err) {
		console.log(buffer); // <Buffer 78 9c cb c9 2f 4a cd 55 c8 2c 28 56 00 01 61>
	}
});

19.2.2 zlib.deflateRaw(buf, [options], callback)

使用DEFLATE算法对buf数据进行原始压缩,不带gzip文件头和尾。options对象可以指定压缩级别(默认值为 zlib.constants.Z_DEFAULT_COMPRESSION),以及其他一些参数,例如内存限制、字典等。callback函数接收两个参数:errbuffer,其中buffer参数包含压缩后的数据。

const zlib = require('zlib');

const input = Buffer.from('hello world');
zlib.deflateRaw(input, (err, buffer) => {
  if (!err) {
    console.log(buffer); // <Buffer cb c9 2f 4a cd 55 c8 2c 28 56 00>
  }
});

19.2.3 zlib.inflate(buf, [options], callback)

对使用DEFLATE算法压缩的buf数据进行解压缩。options对象可以指定解压缩参数,例如内存限制、字典等。callback函数接收两个参数:errbuffer,其中buffer参数包含解压缩后的数据。

const zlib = require('zlib');

const input = Buffer.from('eJwryczLLSjJzy/KSQEAGwwK');
zlib.inflate(input, (err, buffer) => {
  if (!err) {
    console.log(buffer.toString()); // hello world
  }
});

19.2.4 zlib.inflateRaw(buf, [options], callback)

对使用DEFLATE算法进行原始压缩的buf数据进行解压缩,不带gzip文件头和尾。options对象可以指定解压缩参数,例如内存限制、字典等。callback函数接收两个参数:errbuffer,其中buffer参数包含解压缩后的数据。

const zlib = require('zlib');

const input = Buffer.from('cb c9 2f 4a cd 55 c8 2c 28 56 00', 'hex');
zlib.inflateRaw(input, (err, buffer) => {
  if (!err) {
    console.log(buffer.toString()); // hello world
  }
});

19.2.5 zlib.createGzip([options])

创建一个gzip压缩流对象,可用于对数据进行流式压缩。options对象可以指定压缩级别(默认值为 zlib.constants.Z_DEFAULT_COMPRESSION)和内存限制等参数。

const zlib = require('zlib');

const fs = require('fs');
const input = fs.createReadStream('input.txt');
const output = fs.createWriteStream('input.txt.gz');
input.pipe(zlib.createGzip()).pipe(output);

19.2.6 zlib.createGunzip([options])

创建一个 gzip 解压流对象,可用于对 gzip 格式的数据进行流式解压。options对象可以指定内存限制等参数。

const zlib = require('zlib');

const fs = require('fs');
const input = fs.createReadStream('input.txt.gz');
const output = fs.createWriteStream('input.txt');
input.pipe(zlib.createGunzip()).pipe(output);

19.2.7 zlib.createDeflate([options])

创建一个DEFLATE压缩流对象,可用于对数据进行流式压缩。options对象可以指定压缩级别(默认值为 zlib.constants.Z_DEFAULT_COMPRESSION)和内存限制等参数。

const zlib = require('zlib');

const fs = require('fs');
const input = fs.createReadStream('input.txt');
const output = fs.createWriteStream('input.txt.deflate');
input.pipe(zlib.createDeflate()).pipe(output);

19.2.8 zlib.createInflate([options])

创建一个DEFLATE解压流对象,可用于对使用DEFLATE算法压缩的数据进行流式解压。options对象可以指定内存限制等参数。

const zlib = require('zlib');
const fs = require('fs');
const input = fs.createReadStream('input.txt.deflate');
const output = fs.createWriteStream('input.txt');
input.pipe(zlib.createInflate()).pipe(output);

下面是 options 参数的详细介绍:

  • level:指定压缩级别,默认值为 zlib.constants.Z_DEFAULT_COMPRESSION。可以使用以下常量:

    • zlib.constants.Z_NO_COMPRESSION:不进行压缩。
    • zlib.constants.Z_BEST_SPEED:最快速度的压缩。
    • zlib.constants.Z_BEST_COMPRESSION:最佳压缩率,但速度较慢。
    • zlib.constants.Z_DEFAULT_COMPRESSION:默认压缩级别。
    • 0-9:数字越大,压缩级别越高。
  • memLevel:指定内存使用级别,默认值为 zlib.constants.Z_DEFAULT_MEMLEVEL。可以使用以下常量:

    • zlib.constants.Z_DEFAULT_MEMLEVEL:默认内存使用级别。
    • 1-9:数字越大,内存使用越高。
  • strategy:指定压缩策略,默认值为 zlib.constants.Z_DEFAULT_STRATEGY。可以使用以下常量:

    • zlib.constants.Z_DEFAULT_STRATEGY:默认压缩策略。
    • zlib.constants.Z_FILTERED:使用滤波器策略。
    • zlib.constants.Z_HUFFMAN_ONLY:只使用Huffman编码策略。
    • zlib.constants.Z_RLE:只使用游程编码策略。
    • zlib.constants.Z_FIXED:只使用固定字典策略。
  • chunkSize:指定内部缓冲区的大小,默认值为 16 * 1024(16KB)。

  • windowBits:指定窗口大小,默认值为 zlib.constants.Z_DEFAULT_WINDOWBITS。可以使用以下常量:

    • zlib.constants.Z_DEFAULT_WINDOWBITS:默认窗口大小。
    • -8:窗口大小为最小值。
    • -15:窗口大小为最大值。
  • strategy:指定压缩策略,默认值为 zlib.constants.Z_DEFAULT_STRATEGY。可以使用以下常量:

    • zlib.constants.Z_DEFAULT_STRATEGY:默认压缩策略。
    • zlib.constants.Z_FILTERED:使用滤波器策略。
    • zlib.constants.Z_HUFFMAN_ONLY:只使用Huffman编码策略。
    • zlib.constants.Z_RLE:只使用游程编码策略。
    • zlib.constants.Z_FIXED:只使用固定字典策略。
    • flush:指定刷新模式,默认值为zlib.constants.Z_NO_FLUSH。可以使用以下常量:
      • zlib.constants.Z_NO_FLUSH:不刷新。
      • zlib.constants.Z_PARTIAL_FLUSH:部分刷新。
      • zlib.constants.Z_SYNC_FLUSH:同步刷新。
      • zlib.constants.Z_FULL_FLUSH:完全刷新。
      • zlib.constants.Z_FINISH:完成压缩。

以上就是zlib模块主要方法的介绍,该模块提供了多种压缩和解压缩算法,并且可以用于对数据进行流式压缩和解压缩,非常实用。

19.2.9 HTTP服务器里面压缩数据

当在HTTP服务器中传输数据时,可以使用压缩来减少传输的数据量,提高传输效率。以下是一个使用 Node.js 创建HTTP服务器并在其中压缩数据的示例代码:

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

// 创建一个HTTP服务器
const server = http.createServer((req, resp) => {
  // 要被压缩的文件路径和名称
  const filePath = 'test.txt';

  // 获取文件的可读流
  const readStream = fs.createReadStream(filePath);

  // 设置响应头,表明服务器将发送经过gzip压缩的数据
  res.writeHead(200, {
    'Content-Type': 'text/plain',
    'Content-Encoding': 'gzip'
  });

  // 创建一个gzip压缩流
  const gzip = zlib.createGzip();

  // 管道:读取文件 -> 压缩 -> 响应
  readStream.pipe(gzip).pipe(resp);

  // 监听压缩过程的事件
  gzip.on('error', err => {
    console.error(err);
  });
});

// 监听端口
const PORT = 3000;
server.listen(PORT, () => {
  console.log(`Server is running at http://localhost:${PORT}/`);
});

这段代码创建了一个HTTP服务器,当接收到请求时会将指定文件进行压缩并作为响应发送给客户端。在实际使用中,你可以根据需要添加其他的解压缩算法(如deflate)的支持,并根据自己的需求修改响应的Content-Type等内容。

19.2.10 HTTP请求并解压缩数据

当向HTTP服务器发送请求时,如果服务器返回的数据经过压缩,则客户端需要对其进行解压缩。以下是一个使用 Node.js 向HTTP服务器发送请求并解压缩响应数据的示例代码:

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

// 要请求的URL和请求头
const options = {
  hostname: 'localhost',
  port: 3000,
  path: '/',
  method: 'GET',
  headers: {
    'Accept-Encoding': 'gzip, deflate'
  }
};

// 发送HTTP请求
const req = http.request(options, (resp) => {
  const { statusCode, headers } = res;

  // 检查响应头中是否包含压缩信息
  const contentEncoding = headers['content-encoding'];
  const writeStream = fs.createWriteStream("test.txt");

  if (contentEncoding && contentEncoding.includes('gzip')) {
    // 创建一个gzip解压缩流
    const gunzip = zlib.createGunzip();

    // 管道:响应 -> 解压缩 -> 控制台输出
    //resp.pipe(gunzip).pipe(process.stdout);
	// 管道:响应 -> 解压缩 -> 文件
	resp.pipe(gunzip).pipe(writeStream);
  } else if (contentEncoding && contentEncoding.includes('deflate')) {
    // 创建一个deflate解压缩流
    const inflate = zlib.createInflate();

    // 管道:响应 -> 解压缩 -> 控制台输出
    //resp.pipe(inflate).pipe(process.stdout);
	// 管道:响应 -> 解压缩 -> 文件
	resp.pipe(inflate).pipe(writeStream);
  } else {
    // 如果响应头中不包含压缩信息,则直接将响应输出到控制台
	//resp.pipe(process.stdout);
	// 如果响应头中不包含压缩信息,则直接将响应输出文件
    resp.pipe(writeStream);
  }
});

// 监听错误事件
req.on('error', error => {
  console.error(error);
});

// 发送请求
req.end();

这段代码向指定的HTTP服务器发送了一个GET请求,并在响应中检查是否包含压缩信息,如果包含则对数据进行解压缩后输出到控制台或者是写入到程序的根目录。在实际使用中,你可以根据需要添加其他的解压缩算法(如deflate)的支持,并根据自己的需求修改请求的URL、请求头等内容。

19.3 readline

readline 模块是 Node.js 提供的内置模块之一,用于从可读流(如标准输入)中逐行读取数据。它提供了一个接口,可以监听用户的输入,并根据输入进行相应的处理。

19.3.1 创建 Interface 对象

Interface 是 readline 模块中的一个类,用于创建一个可以读取用户输入的接口,并构建交互式的命令行界面。

要使用 readline 模块,首先需要通过 require('readline') 引入该模块。然后,可以使用 createInterface(options) 方法创建一个 Interface 对象,该对象与指定的输入输出流相关联。

options 参数是一个包含以下属性的对象:

  • input:指定输入流,可以是可读流(如 process.stdin)或文件流。默认值为 process.stdin
  • output:指定输出流,可以是可写流(如 process.stdout)或文件流。默认值为 process.stdout
  • prompt:指定命令行提示符。当调用 rl.prompt() 方法时,会显示该提示符,并等待用户输入。默认值为 '> '
  • completer:自动完成函数,用于为用户提供输入的自动补全建议。该函数接收两个参数:line 是用户输入的内容,callback 是一个回调函数。回调函数应接受两个参数:errresult,其中 result 是一个包含建议的数组。默认值为 null,表示禁用自动补全。
  • terminal:指定终端模式。如果设置为 true,则终端会根据需要适应窗口大小和光标移动。如果设置为 false,则终端将被视为非交互式,即不会自动换行或调整窗口大小。默认值为 false
const readline = require('readline');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

在上面的示例中,我们通过 createInterface 方法创建了一个 Interface 对象,并将其关联到 process.stdinprocess.stdout,分别表示标准输入和标准输出。

19.3.2 提问用户并获取输入

一旦创建了 Interface 对象,我们可以使用 question 方法向用户提问,并在用户输入回答后执行回调函数。

rl.question('What is your name? ', (name) => {
  console.log(`Hello, ${name}!`);
  rl.close();
});

在上面的示例中,我们使用 rl.question 方法向用户提问,并传入一个字符串作为提示信息。当用户输入回答后,回调函数将被调用,其中的参数 name 将包含用户输入的内容。在这个简单的示例中,我们将用户输入的内容打印到控制台。

下面是一个更完整的示例,演示了如何使用 readline 模块构建一个简单的命令行计算器:

const readline = require('readline');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

rl.question('Enter the first number: ', (num1) => {
  rl.question('Enter the second number: ', (num2) => {
    const result = Number(num1) + Number(num2);
    console.log(`The result is: ${result}`);
    rl.close();
  });
});

在这个示例中,我们首先向用户提问要求输入两个数字。然后,通过将用户输入转换为数字类型,计算并输出结果。

19.3.3 Interface 对象的方法和事件

Interface 对象提供了一些方法和事件,用于处理用户输入和输出。以下是 Interface 对象的一些常用方法:

  • rl.question(query, callback):向用户提出问题,并等待用户输入。当用户输入完成后,将调用回调函数,并将用户输入作为参数传递给回调函数。

    rl.question('What is your name? ', (name) => {
      console.log(`Hello, ${name}!`);
      rl.close();
    });
    
  • rl.write(data, [key]):向输出流写入数据。可以使用 data 参数指定要写入的内容,也可以使用 key 参数指定按下的键。

    rl.write('Hello, world!');
    
  • rl.prompt([preserveCursor]):显示提示符,并等待用户输入。如果 preserveCursor 参数设置为 true,则光标位置将保持不变。默认情况下,会根据需要自动调整窗口大小和光标位置。

  • rl.setPrompt(prompt):设置提示符。你可以使用此方法在用户输入之前显示一个自定义的提示符。

  • rl.pause():暂停读取输入流。你可以使用此方法来暂停用户输入,并执行一些其他操作。

  • rl.resume():恢复读取输入流。你可以使用此方法来恢复用户输入,并继续等待用户输入。

  • rl.close():关闭 Interface 对象。这将结束输入流,并触发 ‘close’ 事件。

  • rl.on(event, callback):监听指定的事件,并在事件触发时执行回调函数。你可以使用此方法来处理用户输入和其他事件。

    以下是 Interface 对象的一些常用事件:

    • ‘line’:当用户输入完成一行文本时触发。回调函数将接收用户输入的文本作为参数。

    • ‘pause’:当 Interface 对象暂停读取输入流时触发。例如,当用户按下 Ctrl + S(默认情况下)来暂停输入流时,此事件将被触发。

    • ‘resume’:当 Interface 对象恢复读取输入流时触发。例如,当用户按下 Ctrl + Q(默认情况下)来恢复输入流时,此事件将被触发。

    • ‘SIGINT’:当用户按下 Ctrl + C(默认情况下)来发送中断信号时触发。你可以监听此事件来处理用户中断操作。

const readline = require('readline');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

rl.question('What is your name? ', (name) => {
  console.log(`Hello, ${name}!`);
  rl.prompt();
});

rl.on('line', (line) => {
  console.log(`You entered: ${line}`);
});

rl.on('pause', () => {
  console.log('Input stream paused');
});

rl.on('resume', () => {
  console.log('Input stream resumed');
});

rl.on('SIGINT', () => {
  rl.close();
});

// rl.on('close', callback) 方法在 Interface 对象被关闭时触发。当用户按下 Ctrl + D(或在 Windows 上是 Ctrl + Z)来结束输入流时,会触发 close 事件。
// 此外,你还可以调用 rl.close() 方法来关闭 Interface 对象,这也会触发 close 事件。
rl.on('close', () => {
  console.log('Interface closed');
});

在这个示例中,我们创建了一个 Interface 对象,并使用 rl.question() 方法向用户提出问题。在回调函数中,我们打印出带有用户姓名的欢迎消息,并调用 rl.prompt() 方法以显示提示符,并等待用户输入。

我们还监听了 'line' 事件,在用户输入一行文本后执行回调函数,打印出用户输入的内容。

我们还监听了 'pause''resume' 事件,在输入流暂停和恢复时执行相应的回调函数。

最后,我们监听了 'SIGINT' 事件,以处理用户按下 Ctrl + C 的中断操作。在回调函数中,我们调用 rl.close() 方法来关闭 Interface 对象。

当用户完成输入并按下回车键后,'line' 事件将被触发,在回调函数中打印出用户输入的内容。当用户按下 Ctrl + C 来中断操作时,'SIGINT' 事件将被触发,并关闭 Interface 对象。在 Interface 对象关闭后,‘close’ 事件将被触发,在回调函数中打印出相应的消息。

readline 模块非常适合用于开发命令行工具、交互式应用程序和命令行界面。它简化了处理用户输入的过程,使得开发者可以更轻松地构建交互式的命令行界面。你可以根据需要使用 readline 模块提供的方法来满足自己的需求。

19.4 punycode

Punycode 模块是 Node.js 内置的一个模块,用于处理 Punycode 编码和解码。它提供了一些方法,可以将国际化域名(IDN)转换为 ASCII 字符串(Punycode 编码),以及将 Punycode 编码的字符串解码回原始的 Unicode 字符序列。

下面是 Punycode 模块提供的主要方法:

  • punycode.encode(domain):将给定的域名字符串编码为 Punycode 编码的字符串。如果域名中包含非 ASCII 字符,则会对其进行编码。返回编码后的字符串。

  • punycode.decode(domain):将给定的 Punycode 编码的域名字符串解码为原始的 Unicode 字符串。如果输入字符串不是有效的 Punycode 编码字符串,则会抛出一个错误。返回解码后的字符串。

  • punycode.toASCII(domain):将给定的域名字符串转换为 ASCII 字符串,包括对非 ASCII 字符的 Punycode 编码。返回转换后的 ASCII 字符串。

  • punycode.toUnicode(domain):将给定的 Punycode 编码的域名字符串转换回原始的 Unicode 字符串。返回转换后的 Unicode 字符串。

这些方法可以帮助你在处理国际化域名时进行编码和解码操作。以下是一些示例使用:

const domain = '中国.com';
const encodedDomain = punycode.encode(domain);
console.log(encodedDomain); // 输出: "xn--fiqs8s.com"

const decodedDomain = punycode.decode(encodedDomain);
console.log(decodedDomain); // 输出: "中国.com"

const asciiDomain = punycode.toASCII(domain);
console.log(asciiDomain); // 输出: "xn--fiqs8s.com"

const unicodeDomain = punycode.toUnicode(asciiDomain);
console.log(unicodeDomain); // 输出: "中国.com"

Punycode 模块是一个非常有用的工具,可用于在处理国际化域名时进行编码和解码操作,使其能够正确地与现有的域名系统进行交互和处理。

总结

Node.js 提供了丰富的工具模块,包括加解密、readline、punycode、压缩和解压缩等模块。下面对这些模块进行简要总结:

  • 加解密模块:Node.js 提供了 crypto 模块用于加解密操作,包括对称加密(如 AES、DES)、非对称加密(如 RSA)和哈希算法(如 MD5、SHA-256)。使用 crypto 模块可以进行安全的数据传输和存储。

  • readline 模块:Node.js 的 readline 模块提供了一组 API,用于从标准输入流(stdin)读取数据,并在终端上输出提示信息和接收用户输入。它可以帮助我们方便地实现命令行交互式程序。

  • punycode 模块:Punycode 是一种编码方案,用于在互联网域名中表示非 ASCII 字符。Node.js 提供了 punycode 模块,可以将国际化域名(IDN)转换为 ASCII 字符串(Punycode 编码),以及将 Punycode 编码的字符串解码回原始的 Unicode 字符序列。

  • 压缩和解压缩模块:Node.js 提供了 zlib 模块,用于对数据进行压缩和解压缩。zlib 模块支持 gzip、deflate 和 raw 三种压缩格式,可以帮助我们在网络传输和数据存储中节省带宽和存储空间。

这些模块是 Node.js 提供的核心模块,可以方便地使用 Node.js 进行加解密、读取用户输入、处理国际化域名、压缩和解压缩数据等操作。同时,Node.js 社区也提供了大量的第三方模块,可以扩展 Node.js 的功能,满足不同场景下的需求。

20-net

Node.js 的net模块是一个用于创建基于 TCP或 IPC(进程间通信)的网络服务器和客户端的核心模块。Node.js 提供了 netdgramhttphttps等模块,可用于处理 TCP、UDP、HTTP 和 HTTPS,适用于服务端和客户端。

net 模块主要包含以下几个核心类和方法:

  • net.createServer([options], [connectionListener]):用于创建一个 TCP 服务器。接受一个可选的options对象,用于配置服务器参数,例如指定服务器的本地地址、端口等。connectionListener是一个回调函数,用于处理每个新连接的客户端。

  • server.listen(port, [host], [backlog], [callback]):用于将服务器绑定到指定的主机和端口,并开始监听连接。port是要监听的端口号,host是可选的本地地址,默认为 '0.0.0.0',表示监听所有可用的网络接口。backlog是可选的参数,表示在拒绝连接之前,操作系统可以挂起的最大连接数,默认为 511。callback是一个可选的回调函数,用于在服务器开始监听时触发。

  • server.close([callback]):关闭服务器,停止接受新的连接。callback是一个可选的回调函数,在服务器完全关闭后触发。

  • server.getConnections(callback):获取当前服务器的连接数量。callback是一个回调函数,它的参数是当前的连接数。

  • server.address():返回服务器的地址信息,包括address(IP地址)和port(端口号)。

  • server.maxConnections:一个数值属性,表示服务器允许的最大连接数。

  • server.on(event, callback):用于监听服务器的事件。常见的事件有'listening'(服务器开始监听时触发)、'connection'(新连接建立时触发)、'close'(服务器关闭时触发)、'error'(服务器发生错误时触发)等。

  • net.createConnection(options, [connectionListener]):创建一个TCP或IPC的客户端连接。options是一个对象,包含要连接的服务器的地址和端口等信息。connectionListener是一个回调函数,用于处理连接建立后的操作。

  • socket.write(data, [encoding], [callback]):向连接的另一端发送数据。data是要发送的数据,可以是字符串或Buffer对象。encoding是可选的字符编码,默认为’utf-8’。callback是一个可选的回调函数,在数据写入底层操作系统缓存后触发。

  • socket.end([data], [encoding]):关闭连接。可以选择性地发送最后一次数据,并在数据发送完成后关闭连接。

  • socket.destroy():强制关闭连接,不会发送任何额外的数据。

20.1 TCP

TCP 服务在网络服务中非常的常见,目前大多数的应用都是基于 TCP 搭建而成的。

TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的网络传输协议。它在OSI模型中属于传输层协议,很多应用层协议都是基于 TCP 构建的,例如:HTTP、SMTP、IMAP 等协议。它是互联网中最常用的传输协议之一,广泛应用于各种网络应用程序,如网页浏览、电子邮件、文件传输等。

20.1.1 TCP服务端

Node.js 是一个基于事件驱动、非阻塞I/O模型的 JavaScript 运行时环境,它提供了 net 模块用于创建 TCP 服务器和客户端。

要创建一个 TCP 服务端,我们可以使用 Node.js 的 net 模块。下面是一个简单的示例:

server.js

const net = require('net');

// 创建TCP服务器
const server = net.createServer((socket) => {
  // 当有新的客户端连接时触发
  console.log('客户端已连接');

  // 监听客户端发送的数据
  socket.on('data', (data) => {
    console.log('接收到客户端数据:', data.toString());

    // 向客户端发送数据
    socket.write('Hello Client!');
  });

  // 监听客户端断开连接事件
  socket.on('end', () => {
    console.log('客户端已断开连接');
  });
});

// 监听指定端口和主机地址
const PORT = 3000;
const HOST = '127.0.0.1';

server.listen(PORT, HOST, () => {
  console.log(`TCP服务器运行在:${HOST}:${PORT}`);
});

在上面的示例中,我们首先引入了 net 模块,然后使用 createServer 方法创建了一个 TCP 服务器。这个方法接受一个回调函数作为参数,该函数会在每次有新的客户端连接时被调用。在回调函数中,我们可以处理客户端的请求和数据。

通过使用 socket 对象,我们可以监听客户端发送的数据(通过data事件),向客户端发送数据(通过write方法),以及监听客户端断开连接事件(通过end事件)。

最后,我们使用 listen 方法指定服务器要监听的端口和主机地址。在本例中,我们将服务器运行在本地的3000端口上。

20.1.2 TCP客户端

Node.js 提供了 net 模块,可以用于创建 TCP 客户端。下面是一个简单的示例:

const net = require('net');

// 创建TCP客户端
const client = net.createConnection({ port: 3000, host: '127.0.0.1' }, () => {
  console.log('已连接到TCP服务器');

  // 向服务端发送数据
  client.write('Hello Server!');
});

// 监听从服务端接收的数据
client.on('data', (data) => {
  console.log('接收到TCP服务端数据:', data.toString());

  // 关闭与服务端的连接
  client.end();
});

// 监听与服务端断开连接事件
client.on('end', () => {
  console.log('与TCP服务端断开连接');
});

在上面的示例中,我们首先引入了 net 模块,然后使用 createConnection 方法创建了一个 TCP 客户端连接。该方法接受一个配置对象作为参数,其中包含要连接的服务器的端口和主机地址。在连接成功时,回调函数会被调用。

通过 client 对象,我们可以向服务端发送数据(通过 write 方法),监听从服务端接收的数据(通过 data 事件),以及监听与服务端断开连接的事件(通过 end 事件)。

在本例中,我们连接到本地的3000端口上的服务器。当连接成功后,我们向服务端发送了一条数据,并监听从服务端返回的数据。最后,我们在与服务端断开连接时输出相应的消息。

20.1.3 测试TCP客户端和服务端

  1. 我们先打开一个命令行工具,然后调用 node server.js 命令启动一个服务端应用,启动监听 3000 端口。

    图1 运行TCP服务端 server.js

  2. 我们再打开一个命令行工具,然后调用 node client.js 命令并按回车键使用客户端去访问 server.js 这个服务端应用,我们可以看到服务端返回给客户端的数据。

    图2 运行TCP客户端 client.js

  3. 再看看服务端收到了客户端的请求,以及发送的数据。

    图3 TCP服务端收到客户端的反馈消息

请注意你自己的 server.jsclient.js 的所在地址。

20.2 UDP

UDP(User Datagram Protocol)是一种无连接的传输层协议,它提供了一种简单的数据传输机制,适用于那些对数据可靠性要求较低但速度要求较高的场景。与 TCP 不同,UDP 不保证数据的可靠传输和顺序交付,也不提供拥塞控制和流量控制。

在 Node.js 中,可以使用 dgram 模块进行 UDP 通信。该模块提供了创建 UDP 套接字、发送和接收 UDP 数据包的功能。

dgram模块主要提供了以下几个方法和事件:

  • dgram.createSocket(type, [callback]):创建一个UDP套接字,其中type参数指定套接字类型,可以是udp4或udp6。可选的callback函数在套接字绑定后会被调用。

  • socket.send(msg, [offset, length, port, address], [callback]):向指定地址和端口发送UDP数据包。msg参数是一个Buffer或Uint8Array,用于存储要发送的数据。offset和length参数可用于指定要发送的数据在msg中的位置和长度。port和address参数分别指定目标服务器的端口和IP地址。可选的callback函数在数据包发送完成后会被调用。

  • socket.bind([port], [address], [callback]):将套接字绑定到指定的端口和IP地址上。可选的port和address参数用于指定要绑定的端口和IP地址。可选的callback函数在绑定完成后会被调用。

  • socket.close([callback]):关闭套接字。可选的callback函数在套接字关闭完成后会被调用。

  • socket.on(‘message’, callback):监听message事件,当收到UDP数据包时触发回调函数。回调函数的参数包括接收到的数据和发送方的信息。

  • socket.on(‘error’, callback):监听error事件,当套接字发生错误时触发回调函数。回调函数的参数为错误对象。

20.2.1 UDP服务端

在 Node.js 中,可以使用 dgram 模块创建 UDP 服务器。以下是一个简单的示例:

server.js

const dgram = require('dgram');

// 创建UDP服务器
const server = dgram.createSocket('udp4');

// 监听消息事件
server.on('message', (msg, remote) => { 
  console.log(`接收到来自 ${remote.address}:${remote.port} 的消息: ${msg}`);

  // 向客户端发送响应
  const responseMsg = Buffer.from('Hello Client!');
  server.send(responseMsg, 0, responseMsg.length, remote.port, remote.address, (err, bytes) => {
    if (err) {
      console.error('发送响应时发生错误:', err);
    } else {
      console.log(`已向客户端 ${remote.address}:${remote.port} 发送响应`);
    }
  });
});

// 监听错误事件
server.on('error', (err) => {
  console.error('UDP服务器发生错误:', err);
  server.close();
});

// 监听服务器启动事件
server.on('listening', () => {
  const address = server.address();
  console.log(`UDP服务器运行在:${address.address}:${address.port}`);
});

// 指定服务器要监听的端口和主机地址
const PORT = 3000;
const HOST = '127.0.0.1';
server.bind(PORT, HOST);

在上面的示例中,我们首先引入了 dgram 模块,然后使用 createSocket 方法创建了一个 UDP 服务器。该方法接受一个参数指定要使用的IP协议版本,例如 udp4 表示 IPv4

通过监听 message 事件,我们可以获取到客户端发送的消息以及客户端的地址和端口信息。在回调函数中,我们可以处理接收到的消息,并向客户端发送响应。

通过监听 error 事件,我们可以处理服务器发生的错误,并在错误发生时关闭服务器。

通过监听 listening 事件,我们可以获取服务器实际绑定的地址和端口信息,并在服务器启动时进行相应的输出。

最后,我们使用 bind 方法指定服务器要监听的端口和主机地址,并开始监听 UDP 连接。

20.2.2 UDP客户端

在 Node.js 中,可以使用 dgram 模块创建 UDP 客户端。UDP 客户端可以发送 UDP 数据包到指定的服务器,并接收服务器返回的响应

以下是一个简单的 UDP 客户端示例:

client.js

const dgram = require('dgram');

// 创建UDP套接字
const client = dgram.createSocket('udp4');

// 监听消息事件
client.on('message', (msg, remote) => {
  console.log(`接收到来自UDP服务器的消息: ${msg}`);
  // 关闭客户端
  client.close();
});

// 监听错误事件
client.on('error', (err) => {
  console.error('发生错误:', err);
  // 关闭客户端
  client.close();
});

// 定义服务器地址和端口
const serverPort = 3000;
const serverHost = '127.0.0.1';

// 发送消息给服务器
const message = Buffer.from('Hello Server!');
client.send(message, 0, message.length, serverPort, serverHost, (err, bytes) => {
  if (err) {
    console.error('发送消息时发生错误:', err);
	// 关闭客户端
    client.close();
  } else {
    console.log(`已向UDP服务器 ${serverHost}:${serverPort} 发送消息`);
  }
  
  // 关闭客户端
  //client.close();
});

在上述示例中,我们首先引入了 dgram 模块,然后使用 createSocket 方法创建了一个 UDP 套接字。

通过监听 message 事件,我们可以获取到服务器返回的消息以及服务器的地址和端口信息。在回调函数中,我们可以处理接收到的消息。

通过监听 error 事件,我们可以处理 UDP 客户端发生的错误,并在错误发生时关闭 UDP 套接字。

接下来,我们定义了服务器的地址和端口,并使用 send 方法发送 UDP 数据包到服务器。在回调函数中,我们可以处理发送数据包时发生的错误。

最后,我们使用 close 方法关闭 UDP 套接字。

需要注意的是,UDP 是无连接的,因此客户端可以在任何时候发送数据包,而不需要事先建立连接。这也意味着在 UDP 通信中,不保证数据包的可靠传输和顺序交付。

20.2.3 测试UDP客户端和服务端

  1. 同样的,我们先打开一个命令行工具,然后调用 node server.js 命令启动一个服务端应用,启动监听 3000 端口。

    图4 运行UDP服务端 server.js

  2. 然后再打开一个命令行工具,然后调用 node client.js 命令并按回车键使用客户端去访问 server.js 这个服务端应用,我们可以看到服务端返回给客户端的数据。

    图5 运行UDP客户端 client.js

  3. 最后服务端也收到了客户端的请求以及数据。

    图6 UDP服务端收到客户端的反馈消息

请注意你自己的 server.jsclient.js 的所在地址。

总结

Node.js 的 net 模块是一个内置模块,用于创建基于网络的服务器和客户端应用程序。它提供了一些函数和类,可用于实现 TCP 和 IPC(进程间通信)的网络通信。无论是构建 Web 服务器、聊天应用程序还是实现自定义的网络协议,net 模块都是一个强大且灵活的工具。

21-http

HTTP (Hypertext Transfer Protocol) 是一种用于传输超文本的应用层协议。它是构建在 TCP/IP 协议之上的,常用于Web应用中进行客户端和服务器之间的通信。

HTTP在Web应用中扮演着重要的角色,它使得浏览器能够向服务器请求网页并获取资源,也使得服务器能够向客户端发送数据和响应。通过HTTP,我们可以进行网页浏览、数据传输、API调用等各种Web应用场景。

21.1 http模块的常用方法

http 模块是 Node.js 内置的一个核心模块,用于创建基于 HTTP协议的网络服务器和客户端。它提供了一组API,使得在 Node.js 环境下可以轻松地实现 HTTP服务器和客户端功能。

以下是 Node.js 的 http 模块的一些详细介绍:

  1. 创建HTTP服务器:

    • http.createServer([requestListener]):该方法用于创建一个HTTP服务器。接受一个可选的请求处理函数作为参数,该函数会在每个收到的请求上被调用。返回一个 http.Server 对象。
  2. HTTP服务器事件:

    • request:当有新请求到达服务器时触发该事件。回调函数会接收到一个 http.IncomingMessage 对象和一个 http.ServerResponse 对象,用于处理请求和发送响应。
    • connection:当新的TCP连接建立时触发该事件。
    • close:当服务器关闭时触发该事件。
  3. HTTP请求对象(http.IncomingMessage):

    • req.url:获取请求的URL。
    • req.method:获取请求的HTTP方法。
    • req.headers:获取请求头。
    • req.on(event, callback):监听请求对象的事件,如’data’、'end’等。
  4. HTTP响应对象(http.ServerResponse):

    • res.statusCode:设置响应的状态码。
    • res.setHeader(name, value):设置响应头。
    • res.write(data, [encoding], [callback]):将数据写入响应体。
    • res.end([data], [encoding], [callback]):结束响应并发送给客户端。
  5. 创建HTTP客户端请求:

    • http.request(options, [callback]):创建一个HTTP客户端请求。接受一个配置对象作为参数,用于指定请求的URL、方法、头部等信息。返回一个 http.ClientRequest 对象。
    • http.get(options, [callback]):与 http.request() 类似,但使用GET方法,并自动调用 req.end()
  6. HTTP客户端请求对象(http.ClientRequest):

    • req.write(chunk, [encoding], [callback]):将数据写入请求体。
    • req.end([data], [encoding], [callback]):结束请求并发送给服务器。

除了上述的基本功能外,http 模块还提供了其他一些重要特性和方法,如:

  • 在服务器中处理路由和URL路径。
  • 支持 HTTPS 协议,通过 https 模块可以创建安全的 HTTPS 服务器和客户端。
  • 支持 WebSocket 协议,通过 upgrade 事件可以将 HTTP 协议升级到 WebSocket。
  • 支持代理服务器,可以实现反向代理和负载均衡等功能。

Node.js 的 http 模块非常强大且灵活,适用于构建各种类型的网络应用程序,包括Web服务器、API服务器、代理服务器等。它使得处理 HTTP 请求和响应变得简单高效。

21.2 HTTP服务端

通过使用 Node.js 的内置 http 模块的 http.createServer() 方法,开发者可以轻松地构建HTTP服务器,监听HTTP请求并响应客户端请求。

server.js

// 导入http模块
const http = require('http');

// 创建服务器
const server = http.createServer((req, res) => {
  // 处理HTTP请求逻辑
  if (req.method === 'GET' && req.url === '/') {
	// 发送 HTTP 头部
	// HTTP 状态码:200: OK
	// 内容类型:text/plain
    res.writeHead(200, {'Content-Type': 'text/plain'});
	// 发送响应数据:'Hello, World!'
    res.write('Hello, World!');
    res.end();
  } else {
	// 其他请求路径访问为404
    res.writeHead(404, {'Content-Type': 'text/plain'});
    res.write('404 Not Found\n');
    res.end();
  }
});

//// 处理HTTP请求和发送响应
//server.on('request', (req, res) => {
// // 处理HTTP请求逻辑
//});

const port = 3000;

// 监听HTTP请求
server.listen(port, () => {
  console.log(`Server running at http://localhost:${port}/`);
});

以上代码创建了一个简单的HTTP服务器,当请求根路径 / 时,返回一个包含 "Hello, World!";对于其他路径,返回 404 错误。

这只是 Node.js 的HTTP服务端的基本使用方式。你可以通过使用框架(如Express、Koa)来简化路由、中间件等功能的开发。另外,还可以使用其他第三方模块来处理更复杂的需求,如身份验证、数据解析等。

21.3 HTTP客户端

Node.js 提供了内置的 http 模块,可以用于创建HTTP客户端。使用 http 模块可以发送HTTP请求并接收服务器返回的响应。

client.js

const http = require('http');

const options = {
  hostname: '127.0.0.1',
  port: 3000,
  path: '/',
  method: 'GET',
};

const req = http.request(options, (res) => {
  let data = '';

  res.on('data', (chunk) => {
    data += chunk;
  });

  res.on('end', () => {
	  console.log("请求结束 data=", data);
  });
});

req.on('error', (error) => {
  console.error(error);
});

req.end();

你可以使用http.ClientRequest对象的abort方法终止本次请求

request.abort();

还可以通过 request.setTimeout(timeout, [callback]) 设置请求的超时间

21.4 测试HTTP服务端和客户端

  1. 我们先使用 node server.js 命令启动HTTP服务端。

    图1 运行HTTP服务端 server.js

  2. 在使用 node client.js 命令启动客户端并访问服务端,然后拿到相应数据。

    图2 运行HTTP客户端 client.js

请注意你自己的 server.jsclient.js 的所在地址。

总结

Node.js 的 http 模块非常强大且灵活,适用于构建各种类型的网络应用程序,包括Web服务器、API服务器、代理服务器等。它使得处理HTTP请求和响应变得简单高效。

22-https

HTTPS (HyperText Transfer Protocol Secure) 是基于传输层安全协议(TLS/SSL)的HTTP协议,用于在客户端和服务器之间进行加密和安全通信。

在HTTPS通信中,客户端和服务器之间的通信数据是经过加密处理的,这使得通信过程中的数据不容易被窃听和篡改。为了实现HTTPS通信,服务器需要使用数字证书来验证自己的身份并提供公钥,客户端则使用公钥对数据进行加密,然后通过互联网发送给服务器。服务器使用私钥解密数据并进行相应的操作,然后将响应数据通过公钥进行加密,并发送回客户端。在整个过程中,客户端和服务器之间的通信数据都是加密和安全的。

与HTTP相比,HTTPS具有以下优点:

  • 数据安全性:HTTPS使用加密机制保护通信过程中的数据,防止数据被窃听和篡改。

  • 身份验证:HTTPS使用数字证书验证服务器的身份,确保客户端与真正的服务器进行通信。

  • 排名优化:搜索引擎通常会更倾向于显示使用HTTPS协议的网站。

虽然HTTPS具有很多优点,但也存在一些缺点:

  • 加密处理会增加服务器的处理负荷,对服务器的性能要求较高。

  • 需要购买数字证书,增加了网站运营成本。

Node.js 的 https 模块是对 http 模块的扩展,用于创建和处理HTTPS服务器和客户端。它提供了以下功能:

  • 创建HTTPS服务器:https.createServer(options, requestListener) 方法用于创建一个HTTPS服务器,并返回一个 Server 对象,可以使用 listen() 方法指定服务器监听的端口号和IP地址。

  • 发送HTTPS请求:https.get(options, callback) 方法用于向HTTPS服务器发起GET请求,并返回一个 http.ClientRequest 对象,可以使用 on('data')on('end') 等事件处理器接收响应数据。

  • 处理HTTPS请求:https.Server 对象继承自 http.Server 对象,在 request 事件回调中,可以使用 req.connection.encrypted 属性判断是否为HTTPS连接,使用 res.writeHead() 方法设置响应头,使用 res.write()res.end() 方法返回响应数据。

  • HTTPS选项:https 模块还提供了一个 globalAgent 对象,可以设置全局的HTTPS代理选项,例如最大套接字数、证书验证等。

  • 证书和密钥:HTTPS服务器需要使用证书和私钥来进行加密通信,https.createServer() 方法的第一个参数可以传入一个包含 keycert 属性的对象或字符串,分别表示私钥和证书文件路径或内容。此外,还可以使用 https.request(options, callback) 方法的 options 参数指定证书和私钥等选项。

Node.js 的 https 模块中,options 参数用于配置HTTPS请求和服务器的选项。这个参数是一个包含各种配置选项的 JavaScript 对象。下面介绍一些常用的 options 参数及其作用:

  • key: 指定服务器的私钥。可以是一个包含私钥内容的字符串,也可以是一个指向私钥文件路径的字符串。
  • cert: 指定服务器的证书。可以是一个包含证书内容的字符串,也可以是一个指向证书文件路径的字符串。
  • ca: 指定证书颁发机构(CA)的证书列表。可以是一个包含 CA 证书内容的字符串数组,也可以是一个指向包含 CA 证书的文件路径的字符串数组。
  • pfx: 指定一个包含私钥、证书和 CA 的 PFX 或 PKCS12 格式文件的路径。
  • passphrase: 如果私钥文件被密码保护,使用此选项提供密码。
  • ciphers: 指定可接受的加密算法列表,用于在客户端和服务器之间协商加密算法。
  • secureProtocol: 指定要使用的安全协议。默认为 SSLv23_method,支持多个安全协议(如 TLSv1、TLSv1.1、TLSv1.2)。
  • servername: 指定 server 的名称,用于服务器名称指示(SNI)扩展。
  • rejectUnauthorized: 指定是否拒绝未经验证的证书,默认为 true。设置为 false 可以允许使用自签名证书。
  • requestCert: 指定是否要求客户端提供证书,默认为 false。
  • agent: 指定一个代理对象,用于自定义 HTTPS 代理的行为。

22.1 TLS/SSL

TLS (Transport Layer Security) 和 SSL (Secure Sockets Layer) 是用于保护网络通信的协议。它们都是在TCP/IP 协议之上工作的安全层,用于加密和验证客户端和服务器之间的通信。SSL 最初由 Netscape 公司开发,后来被标准化为 TLS 1.0。

TLS/SSL 使用了公开密钥加密技术(Public Key Cryptography)来确保通信的机密性、完整性和身份验证。在 TLS/SSL 协议中,客户端和服务器通过握手协议建立连接并进行身份验证。一旦连接建立,双方就可以使用对称密钥加密技术(Symmetric Key Cryptography)来加密和解密数据。

TLS/SSL 协议提供了以下主要功能:

  • 数据加密:使用加密算法将数据转换为不可读的形式,以确保数据在传输过程中不会被窃听或篡改。
  • 身份验证:通过证书验证来确保通信双方的身份,防止假冒和中间人攻击。
  • 完整性保护:使用消息认证码(MAC)来确保数据在传输过程中不会被篡改。
  • 抗重放攻击:使用时间戳和随机数等机制来防止攻击者使用已经传输过的数据来进行攻击。

在应用程序中使用 TLS/SSL 协议时,需要注意以下几点:

  • 证书管理:使用受信任的证书颁发机构颁发的证书来确保通信的安全性。
  • 协议版本:选择支持较新版本的协议,以获得更好的安全性和功能。
  • 密钥管理:定期更换密钥,以防止密钥泄漏或被破解。
  • 配置安全性:配置加密算法、密钥长度、身份验证方式等参数,以确保通信的安全性。

总之,TLS/SSL 协议是保护网络通信的重要协议,应用广泛。在实际使用中,需要合理配置和管理,以确保通信的安全性。

22.2 生成签名证书

创建HTTPS服务器之前,我们需要先创建好 公钥私钥证书。要生成HTTPS签名证书,你可以遵循以下步骤:

  1. 生成私钥(Private Key):

    使用以下命令在终端中生成私钥文件(例如private.key):

    # 这将生成一个1024位的RSA私钥。
    openssl genrsa -out private.key 1024
    
  2. 生成证书签名请求(Certificate Signing Request,简称CSR):

    使用以下命令生成CSR文件(例如csr.pem),其中common_name是你的域名:

    openssl req -new -key private.key -out csr.pem
    

    在此过程中,你将需要提供一些相关信息,如组织名称、所在国家/地区、电子邮件地址等。这些信息将用于生成证书。

  3. 提交CSR并获取证书:

    将生成的CSR文件提交给可信任的证书颁发机构(Certification Authority,简称CA),例如Symantec、Let’s Encrypt等,以获得签名证书。CA会验证你的域名和身份,并颁发相应的证书文件。

    如果你正在开发环境中进行测试或个人使用,你可以使用自签名证书。使用以下命令生成自签名证书(例如certificate.crt):

    openssl x509 -req -in csr.pem -signkey private.key -out certificate.crt
    
  4. 将私钥和证书应用到HTTPS服务器:

    将生成的私钥(private.key)和证书(certificate.crt)应用到你的HTTPS服务器配置中。具体步骤将取决于你使用的服务器软件,例如Node.js、Nginx、Apache等。

22.3 创建HTTPS服务端

https.createServer() 方法用于创建一个HTTPS服务器,并返回一个 Server 对象。该方法的第一个参数是一个包含 keycert 属性的对象或字符串,分别表示私钥和证书文件路径或内容。如果使用自签名证书进行开发和测试,则可以使用以下代码来创建一个HTTPS服务器:

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

// 读取私钥和证书文件
const privateKey = fs.readFileSync('/path/to/private/key.pem', 'utf8');
const certificate = fs.readFileSync('/path/to/certificate.pem', 'utf8');
// ca证书
const ca = fs.readFileSync('/path/to/ca.crt', 'utf8');

const serverOptions = {
	key: privateKey,
	cert: certificate,
	ca: [ca]
};

// 创建HTTPS服务器
const server = https.createServer(serverOptions, (req, resp) => {
	// 设置响应头
	resp.setHeader('Content-Type', 'text/plain');
	resp.statusCode = 200;

	// 返回响应消息
	if (req.method === 'GET' && req.url === '/') {
      resp.writeHead(200, {'Content-Type': 'text/plain'});
      resp.write('Hello, World!');
      resp.end();
    } else {
      resp.writeHead(404, {'Content-Type': 'text/plain'});
      resp.write('404 Not Found\n');
      resp.end();
    }
});

// 监听端口443
server.listen(443, () => {
	console.log('Server running on port 443');
});

需要注意的是,在生产环境中,应该使用受信任的证书颁发机构颁发的正式证书,以确保通信安全。
受信任的证书一般建议去阿里云、腾讯云等去申请下载。

图1 SSL证书下载

https.Server 对象继承自 http.Server 对象,在 request 事件回调中,可以使用 req.connection.encrypted 属性判断是否为HTTPS连接。如果是HTTPS连接,则可以使用 resp.writeHead() 方法设置响应头,使用 resp.write()resp.end() 方法返回响应数据。

22.4 HTTPS客户端

Node.js 中,https.get()https.request() 是用于发送HTTPS GET和HTTPS请求的两个核心函数。

22.4.1 的http.get发起HTTPS请求

https.get() 方法用于发送HTTPS GET请求,并接收响应数据。它接受两个参数:

  • options:一个对象,用于指定请求的URL、头部信息等配置。
  • callback:一个回调函数,用于处理响应数据。

https.get() 方法返回一个 http.ClientRequest 对象,可以通过该对象设置请求的头部信息和监听请求事件。

client.js


const https = require('https');

// 发送HTTPS请求
https.get('https://www.example.com', (resp) => {
  let data = '';

  // 接收响应数据
  resp.on('data', (chunk) => {
    data += chunk;
  });

  // 响应结束
  resp.on('end', () => {
    // 打印响应数据
    console.log(data);
  });
}).on('error', (err) => {
  console.error(err);
});

22.4.2 https.request发起HTTPS请求

https.request()方法用于发送HTTPS请求,可以进行更加灵活的配置。它接受两个参数:

  • options:一个对象,用于指定请求的URL、头部信息等配置。
  • callback:一个回调函数,用于处理响应数据。

https.request()方法返回一个http.ClientRequest对象,可以通过该对象设置请求的头部信息和监听请求事件。

22.4.3 加密HTTPS请求

使用https模块发起加密HTTPS协议和直接发送HTTPS请求的区别在于需要指定证书等参数。

如果服务器使用自签名的证书,那么我们需要在options对象中设置rejectUnauthorized为false,允许使用自签名证书。

client.js

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

const options = {
  hostname: '127.0.0.1',
  port: 443,
  path: '/',
  method: 'GET',
  key: fs.readFileSync("client.key"),
  cert: fs.readFileSync("client.key"),
  ca: [fs.readFileSync("ca.crt")]
};

options.agent = https.Agent(options);
const req = https.request(options, (resp) => {
  console.log("状态码 statusCode=", resp.statusCode);
  console.log("响应体 headers=", JSON.stringify(resp.headers));

  let data = '';

  resp.on('data', (chunk) => {
    data += chunk;
  });

  resp.on('end', () => {
	  console.log("请求结束 data=", data);
  });
});

req.on('error', (error) => {
  console.error(error);
});

req.end();

https模块还提供了一个 globalAgent 对象,可以设置全局的HTTPS代理选项,例如最大套接字数、证书验证等。该对象的默认值为 https.globalAgent,可以使用以下代码修改它的选项:

const https = require('https');

https.globalAgent.maxSockets = 10;
https.globalAgent.options.rejectUnauthorized = false;

上面的代码将全局的最大套接字数设置为10,将证书验证设置为不拒绝未经验证的证书。

注意:受信任的证书颁发机构颁发的证书的域名需要和HTTPS服务器的域名一致才能正常访问,不然请求会被拦截。

https 模块是用于创建和处理HTTPS服务器和客户端的 Node.js 模块。它提供了创建HTTPS服务器、发送HTTPS请求、处理HTTPS请求和配置HTTPS选项等功能。在使用HTTPS时,需要使用受信任的证书颁发机构颁发的正式证书,以确保通信安全。

总结

Node.js 的 https 模块是用于创建和处理HTTPS请求的模块。它提供了一些方法来操作HTTPS连接和服务器。

  • https.get(options, callback): 发起一个GET请求到指定的HTTPS服务器,并在获取到响应时调用回调函数。
  • https.request(options, callback): 发起一个自定义的HTTPS请求到指定的服务器,并在获取到响应时调用回调函数。
  • https.createServer(options, requestListener): 创建一个HTTPS服务器,并传入一个用于处理传入请求的回调函数。
  • https.globalAgent: 全局的HTTPS代理对象,可用于自定义HTTPS请求的行为。

这些方法可以帮助你创建安全的HTTPS连接、发送请求,并处理响应。你可以使用证书和私钥等配置来保护你的连接和服务器。https 模块提供了灵活的选项,以适应不同的需求。通过 https 模块你可以构建安全的HTTPS连接并与其他服务进行通信。

23-WebSocket

WebSocket 是一种在单个 TCP 连接上提供全双工通信的协议。与 HTTP 不同,WebSocket 的连接是持久的,客户端和服务器可以在任何时间点上进行双向通信,而不需要为每个请求创建新的连接。WebSocket 协议通过建立长连接,允许服务器主动向客户端推送数据,实现实时的双向通信。

以下是 WebSocket 和 HTTP 的一些主要区别:

  1. 连接方式:HTTP 协议使用短连接,每个请求-响应周期都会创建一个新的连接。而 WebSocket 协议使用长连接,在客户端和服务器之间建立持久的连接,以实现实时的双向通信。

  2. 通信方式:HTTP 是一种请求-响应模式的通信方式,客户端发送请求,服务器返回响应。而 WebSocket 允许客户端和服务器之间实时地双向通信,无需等待请求和响应。

  3. 数据格式:HTTP 通常使用文本或二进制格式传输数据,而 WebSocket 支持以原始二进制格式或文本格式传输数据。

  4. 端口:HTTP 默认使用 80 端口进行通信,HTTPS 使用 443 端口。而 WebSocket 通常使用 80 或 443 端口作为默认端口。同时,WebSocket 还可以使用其他非常用端口进行通信。

  5. 应用场景:HTTP 适用于传统的请求-响应模式,例如获取网页内容、上传文件等。而 WebSocket 更适用于实时性要求较高的应用场景,如实时聊天、实时协作、实时游戏等,其中需要服务器主动向客户端推送数据。

WebSocket 和 HTTP 是两种不同的协议,适用于不同的通信需求。HTTP 适合传统的请求-响应模式,而 WebSocket 提供了持久的双向通信能力,更适合实时性要求较高的应用。

23.1 ws

Node.js 的 ws(WebSocket)模块是一个轻量级的 WebSocket 实现,用于在 Node.js 服务器和客户端之间建立实时通信的连接。它使用了标准的 WebSocket 协议(RFC 6455),支持高速、双向、实时数据传输,非常适合构建实时应用程序,如聊天应用、实时游戏等。

ws 模块的主要特点包括:

  • 简单易用:提供简单的 API,容易上手。
  • 高效性能:采用 C++ 扩展编写,具有良好的性能。
  • 跨平台兼容性:可运行于 Windows、Linux、macOS 等多种平台,并且与浏览器的 WebSocket API 兼容。
  • 安全性:支持 WSS(WebSocket over SSL/TLS)协议,使用加密的 WebSocket 连接,保证通信安全。

使用 ws 需要先安装依赖:

npm install ws --save

下面是 ws 模块的一些主要 API:

23.1.1 服务端 API

WebSocket.Server

WebSocket.Server 是 WebSocket 的服务端类,用于创建和管理 WebSocket 服务器。可以通过实例化该类来创建 WebSocket 服务器,并使用其提供的方法管理连接。

构造函数
const WebSocket = require('ws');
const wss = new WebSocket.Server(options);

options:可选参数对象。包含以下属性:

  • port:WebSocket 服务器监听的端口号,默认为 80。
  • server:HTTP 服务器实例,用于绑定 WebSocket 服务器。
  • clientTracking:是否记录客户端连接数,默认为 false。
  • perMessageDeflate:是否开启压缩功能,默认为 true。
实例方法
  • wss.on(eventName, callback)

    监听事件,可以监听以下事件:

    • connection:新客户端连接成功后触发。
    • error:发生错误时触发。
    • headers:收到客户端 HTTP 请求头时触发。
    • listening:WebSocket 服务器开始监听连接请求时触发。
wss.clients

获取所有连接到当前 WebSocket 服务器的 WebSocket 对象。

WebSocket

WebSocket 是 WebSocket 的客户端类,用于创建和管理 WebSocket 客户端。可以通过实例化该类来创建 WebSocket 客户端,并使用其提供的方法管理连接。

构造函数
const WebSocket = require('ws');
const ws = new WebSocket(url, [options]);

参数说明:

  • url:WebSocket 服务器的 URL 地址。
  • options:可选参数对象。包含以下属性:
    • protocol:指定客户端使用的协议,默认为无协议。
    • headers:指定客户端发送的 HTTP 请求头。
webSocket.readyState

readyState 表示 23.WebSocket 连接的当前状态,它是一个只读属性。readyState 属性的值是一个整数,代表不同的连接状态:

  • CONNECTING(值为 0):正在建立连接,WebSocket 对象已经创建,但尚未完成连接握手。

  • OPEN(值为 1):连接已经建立,可以进行通信。

  • CLOSING(值为 2):连接正在关闭中,数据传输已经停止,但是还没有完全关闭。

  • CLOSED(值为 3):连接已经关闭或无法打开。

在使用 ws 库时,你可以通过访问 WebSocket 实例的 readyState 属性获取当前连接状态。

实例方法
  • ws.send(data, [options], [callback])
    向服务器发送数据。可以发送字符串、Buffer、ArrayBuffer 等类型的数据。

  • ws.close(code, [reason])
    关闭 WebSocket 连接。

  • ws.on(eventName, callback)
    监听事件,可以监听以下事件:

    • open:连接成功时触发。
    • close:断开连接时触发。
    • message:收到服务器发送的消息时触发。
    • error:发生错误时触发。

23.1.2 客户端 API

new WebSocket(url, [protocols])

创建 WebSocket 实例,用于连接 WebSocket 服务器。

参数说明:

  • url:WebSocket 服务器的 URL 地址。
  • protocols:可选参数,指定使用的协议。
实例方法
  • ws.send(data)
    向服务器发送数据。可以发送字符串、Buffer、ArrayBuffer 等类型的数据。

  • ws.close()
    关闭 WebSocket 连接。

  • ws.addEventListener(eventName, callback)
    监听事件,可以监听以下事件:

    • open:连接成功时触发。
    • close:断开连接时触发。
    • message:收到服务器发送的消息时触发。
    • error:发生错误时触发。

23.1.3 创建 WebSocket 服务器

server.js

// 需要安装依赖:npm install ws --save
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  console.log('新的连接已建立');

  ws.on('message', (message) => {
    console.log(`接收到消息:${message}`);
  });

  ws.send('欢迎连接 WebSocket 服务器');
});

上述代码创建了一个 WebSocket 服务器,并监听在本地的 8080 端口。当有新的连接建立时,会触发 connection 事件,并在回调函数中处理连接。

在连接建立后,可以通过 ws.on('message', callback) 方法监听来自客户端的消息。当接收到消息时,会触发 message 事件,并在回调函数中处理消息。

通过 ws.send(message) 方法可以向客户端发送消息。

23.1.4 创建 WebSocket 客户端

client.js

const WebSocket = require('ws');

const ws = new WebSocket('ws://localhost:8080');

ws.on('open', () => {
  console.log('连接到 WebSocket 服务器');

  ws.send('Hello, Server!');
});

ws.on('message', (message) => {
  console.log(`接收到消息:${message}`);
});

上述代码创建了一个 WebSocket 客户端,并连接到本地的 8080 端口的服务器。

当连接成功后,会触发 open 事件,并在回调函数中处理。可以通过 ws.send(message) 方法向服务器发送消息。

同样地,通过 ws.on('message', callback) 方法可以监听来自服务器的消息,并在回调函数中处理。

注意:
1. ws 只能在 Node.js 环境中使用,在浏览器中不能使用,浏览器请直接使用原生的 WebSocket 构造函数。
2. ws 一般不会单独使用,更优的方案都是将 ws 集成到 Express 框架中去使用。

上述是使用 ws 模块创建 WebSocket 服务器和客户端的基本示例。你还可以使用其他第三方库或框架来实现 WebSocket 功能,如 socket.iouws 等。

WebSocket 是一种用于在客户端和服务器之间进行双向通信的协议,在 Node.js 中可以使用 ws 模块来创建 WebSocket 服务器和客户端,实现实时的数据推送和双向通信功能。

总结

WebSocket 是一种在客户端和服务器之间进行双向通信的网络协议,它提供了实时性、低延迟和较小的数据包开销等优势。通过使用 WebSocket,可以构建实时的应用程序,实现即时通信和实时数据推送的功能。同时,Node.js 还提供了一些第三方模块和库,例如 socket.io 和 ws,使得开发者可以方便地构建实时性强的 WebSocket 应用程序。

24-进程和子进程

进程 是计算机中正在运行的程序的实例。每个 进程 都有独立的内存空间和系统资源,它们相互隔离,互不干扰。进程 是操作系统进行任务调度和资源分配的基本单位。

线程 是进程中的一个独立执行单元,它可以并行地执行多个任务。在一个多线程程序中,不同的线程可以同时处理不同的任务,从而提高程序的执行效率。然而,多线程编程也带来了一定的复杂性,如线程同步、死锁等问题。

Node.js 采用单线程模型来处理任务。这意味着在 Node.js 中,所有的 I/O 操作、事件循环和定时器等都是由同一个线程来处理的。

当谈到 Node.js 的 进程子进程 时,我们需要了解 Node.js 是基于单线程事件循环模型构建的。然而,Node.js 提供了一些机制来处理并发操作,其中包括利用进程和子进程。

24.1 Node.js中的进程

在 Node.js 中,进程是指计算机系统中运行的程序的实例。每个进程都有自己独立的内存空间和系统资源。Node.js 可以在单个进程上执行 JavaScript 代码。

Node.js 提供了 process 全局对象,它表示当前 Node.js 进程的实例。通过 process 对象,你可以访问关于进程的信息,例如进程 ID、环境变量等。此外,还可以使用 process 对象来控制进程的行为,例如终止进程、捕获信号等。

24.1.1 process 对象的属性:

当涉及到 Node.js 的 process 对象时,它提供了许多方法和属性来控制和管理 Node.js 应用程序的执行环境。下面是一些常用的 process 方法和属性的详细解释以及示例代码:

  1. process.argv: argv 是一个包含命令行参数的数组。其中,process.argv[0] 表示 Node.js 的可执行文件路径,process.argv[1] 表示当前脚本文件的路径,后续元素是传递给脚本的命令行参数。

    // 命令行执行:node app.js hello world
    console.log(process.argv);
    // 输出:[ 'node', '/path/to/app.js', 'hello', 'world' ]
    
  2. process.env: env 是一个对象,包含了当前进程的环境变量。通过 process.env,你可以读取或设置环境变量的值。

    // 获取环境变量的值
    console.log(process.env.HOME);
    // 设置环境变量的值
    process.env.API_KEY = 'YOUR_API_KEY';
    
  3. process.pid: pid 表示当前进程的进程 ID。

    console.log(`当前进程的进程 ID:${process.pid}`);
    
    // 输出:当前进程的进程 ID:1456
    
  4. process.platform: platform 表示当前操作系统平台。

    console.log(process.platform);
    
    // 输出:darwin
    
  5. process.version:该属性返回一个字符串,表示当前 Node.js 的版本号。

    console.log(process.version);
    
    // 输出:v18.17.1
    
  6. process.versions:该属性是一个包含 Node.js 和其依赖的版本信息的对象。

    console.log(process.versions);
    
    // 输出:
    // {
    //   node: '18.17.1',
    //   acorn: '8.8.2',
    //   ada: '2.5.0',
    //   ares: '1.19.1',
    //   brotli: '1.0.9',
    //   cldr: '43.1',
    //   icu: '73.2',
    //   llhttp: '6.0.11',
    //   modules: '108',
    //   napi: '9',
    //   nghttp2: '1.55.1',
    //   openssl: '3.1.2',
    //   simdutf: '3.2.12',
    //   tz: '2023c',
    //   undici: '5.22.1',
    //   unicode: '15.0',
    //   uv: '1.46.0',
    //   uvwasi: '0.0.18',
    //   v8: '10.2.154.26-node.26',
    //   zlib: '1.2.11'
    // }
    
  7. process.arch:该属性返回一个字符串,表示当前 Node.js 进程的处理器架构。

    console.log(`当前处理器架构是:${process.arch}`);
    
    // 输出:当前处理器架构是:arm64
    
  8. process.title:该属性用于设置或获取当前 Node.js 进程的进程名。

    console.log(`当前进程的名称是:${process.title}`);
    
    process.title = 'MyProcess';
    console.log(`修改后的进程名称是:${process.title}`);
    
  9. process.stdinout 和 process.stderr: stdoutstderr 分别表示标准输出流和标准错误流。你可以通过 process.stdout.write()process.stderr.write() 方法向这些流写入数据。

    // 向标准输出流写入数据
    process.stdout.write('Hello, world!\n');
    
    // 向标准错误流写入数据
    process.stderr.write('Error occurred!\n');
    
  10. process.stdin:process 对象的一个可读流属性,用于从标准输入流(stdin)读取数据。

    process.stdin.setEncoding('utf-8');
    
    // 监听用户输入的数据
    process.stdin.on('data', (data) => {
      console.log(`用户输入了:${data.trim()}`);
    });
    
    // 监听输入流结束事件
    process.stdin.on('end', () => {
      console.log('输入流已经关闭');
    });
    
  11. process.config:提供了有关当前 Node.js 进程配置选项的信息。它返回一个包含各种配置值的对象。

    console.log(process.config);
    
    // 输出:
    // {
    //   target_defaults: {
    //     cflags: [ '-msign-return-address=all' ],
    //     default_configuration: 'Release',
    //     defines: [
    //       'NODE_OPENSSL_CONF_NAME=nodejs_conf',
    //       'NODE_OPENSSL_CERT_STORE',
    //       'ICU_NO_USER_DATA_OVERRIDE'
    //     ],
    //     include_dirs: [
    //       '/opt/homebrew/opt/libuv/include',
    //       '/opt/homebrew/opt/brotli/include',
    //       '/opt/homebrew/opt/c-ares/include',
    //       '/opt/homebrew/opt/libnghttp2/include',
    //       '/opt/homebrew/opt/openssl@3/include',
    //       '/opt/homebrew/Cellar/icu4c/73.2/include'
    //     ],
    //     libraries: [
    //       '-lz',
    //       '-L/opt/homebrew/opt/libuv/lib',
    //       '-luv',
    //       '-L/opt/homebrew/opt/brotli/lib',
    //       '-lbrotlidec',
    //       '-lbrotlienc',
    //       '-L/opt/homebrew/opt/c-ares/lib',
    //       '-lcares',
    //       '-L/opt/homebrew/opt/libnghttp2/lib',
    //       '-lnghttp2',
    //       '-L/opt/homebrew/opt/openssl@3/lib',
    //       '-lcrypto',
    //       '-lssl',
    //       '-L/opt/homebrew/Cellar/icu4c/73.2/lib',
    //       '-licui18n',
    //       '-licuuc',
    //       '-licudata'
    //     ]
    //   },
    //   variables: {
    //     arm_fpu: 'neon',
    //     asan: 0,
    //     coverage: false,
    //     dcheck_always_on: 0,
    //     debug_nghttp2: false,
    //     debug_node: false,
    //     enable_lto: false,
    //     enable_pgo_generate: false,
    //     enable_pgo_use: false,
    //     error_on_warn: false,
    //     force_dynamic_crt: 0,
    //     host_arch: 'arm64',
    //     icu_gyp_path: 'tools/icu/icu-system.gyp',
    //     icu_small: false,
    //     icu_ver_major: '73',
    //     is_debug: 0,
    //     libdir: 'lib',
    //     llvm_version: '14.0',
    //     napi_build_version: '9',
    //     node_builtin_shareable_builtins: [
    //       'deps/cjs-module-lexer/lexer.js',
    //       'deps/cjs-module-lexer/dist/lexer.js',
    //       'deps/undici/undici.js'
    //     ],
    //     node_byteorder: 'little',
    //     node_debug_lib: false,
    //     node_enable_d8: false,
    //     node_enable_v8_vtunejit: false,
    //     node_fipsinstall: false,
    //     node_install_corepack: true,
    //     node_install_npm: true,
    //     node_library_files: [
    //       'lib/_http_agent.js',
    //       'lib/_http_client.js',
    //       'lib/_http_common.js',
    //       'lib/_http_incoming.js',
    //       'lib/_http_outgoing.js',
    //       'lib/_http_server.js',
    //       'lib/_stream_duplex.js',
    //       'lib/_stream_passthrough.js',
    //       'lib/_stream_readable.js',
    //       'lib/_stream_transform.js',
    //       'lib/_stream_wrap.js',
    //       'lib/_stream_writable.js',
    //       'lib/_tls_common.js',
    //       'lib/_tls_wrap.js',
    //       'lib/assert.js',
    //       'lib/assert/strict.js',
    //       'lib/async_hooks.js',
    //       'lib/buffer.js',
    //       'lib/child_process.js',
    //       'lib/cluster.js',
    //       'lib/console.js',
    //       'lib/constants.js',
    //       'lib/crypto.js',
    //       'lib/dgram.js',
    //       'lib/diagnostics_channel.js',
    //       'lib/dns.js',
    //       'lib/dns/promises.js',
    //       'lib/domain.js',
    //       'lib/events.js',
    //       'lib/fs.js',
    //       'lib/fs/promises.js',
    //       'lib/http.js',
    //       'lib/http2.js',
    //       'lib/https.js',
    //       'lib/inspector.js',
    //       'lib/internal/abort_controller.js',
    //       'lib/internal/assert.js',
    //       'lib/internal/assert/assertion_error.js',
    //       'lib/internal/assert/calltracker.js',
    //       'lib/internal/async_hooks.js',
    //       'lib/internal/blob.js',
    //       'lib/internal/blocklist.js',
    //       'lib/internal/bootstrap/browser.js',
    //       'lib/internal/bootstrap/loaders.js',
    //       'lib/internal/bootstrap/node.js',
    //       'lib/internal/bootstrap/switches/does_not_own_process_state.js',
    //       'lib/internal/bootstrap/switches/does_own_process_state.js',
    //       'lib/internal/bootstrap/switches/is_main_thread.js',
    //       'lib/internal/bootstrap/switches/is_not_main_thread.js',
    //       'lib/internal/buffer.js',
    //       'lib/internal/child_process.js',
    //       'lib/internal/child_process/serialization.js',
    //       'lib/internal/cli_table.js',
    //       'lib/internal/cluster/child.js',
    //       'lib/internal/cluster/primary.js',
    //       'lib/internal/cluster/round_robin_handle.js',
    //       'lib/internal/cluster/shared_handle.js',
    //       'lib/internal/cluster/utils.js',
    //       'lib/internal/cluster/worker.js',
    //       'lib/internal/console/constructor.js',
    //       'lib/internal/console/global.js',
    //       'lib/internal/constants.js',
    //       'lib/internal/crypto/aes.js',
    //       'lib/internal/crypto/certificate.js',
    //       'lib/internal/crypto/cfrg.js',
    //       'lib/internal/crypto/cipher.js',
    //       'lib/internal/crypto/diffiehellman.js',
    //       'lib/internal/crypto/ec.js',
    //       'lib/internal/crypto/hash.js',
    //       'lib/internal/crypto/hashnames.js',
    //       'lib/internal/crypto/hkdf.js',
    //       'lib/internal/crypto/keygen.js',
    //       'lib/internal/crypto/keys.js',
    //       'lib/internal/crypto/mac.js',
    //       'lib/internal/crypto/pbkdf2.js',
    //       'lib/internal/crypto/random.js',
    //       'lib/internal/crypto/rsa.js',
    //       'lib/internal/crypto/scrypt.js',
    //       'lib/internal/crypto/sig.js',
    //       'lib/internal/crypto/util.js',
    //       'lib/internal/crypto/webcrypto.js',
    //       'lib/internal/crypto/webidl.js',
    //       'lib/internal/crypto/x509.js',
    //       'lib/internal/debugger/inspect.js',
    //       'lib/internal/debugger/inspect_client.js',
    //       'lib/internal/debugger/inspect_repl.js',
    //       'lib/internal/dgram.js',
    //       'lib/internal/dns/callback_resolver.js',
    //       'lib/internal/dns/promises.js',
    //       'lib/internal/dns/utils.js',
    //       'lib/internal/dtrace.js',
    //       'lib/internal/encoding.js',
    //       'lib/internal/error_serdes.js',
    //       'lib/internal/errors.js',
    //       'lib/internal/event_target.js',
    //       'lib/internal/file.js',
    //       'lib/internal/fixed_queue.js',
    //       'lib/internal/freelist.js',
    //       'lib/internal/freeze_intrinsics.js',
    //       'lib/internal/fs/cp/cp-sync.js',
    //       ... 205 more items
    //     ],
    //     node_module_version: 108,
    //     node_no_browser_globals: false,
    //     node_prefix: '/opt/homebrew/Cellar/node@18/18.17.1',
    //     node_release_urlbase: '',
    //     node_shared: false,
    //     node_shared_brotli: true,
    //     node_shared_cares: true,
    //     node_shared_http_parser: false,
    //     node_shared_libuv: true,
    //     node_shared_nghttp2: true,
    //     node_shared_nghttp3: false,
    //     node_shared_ngtcp2: false,
    //     node_shared_openssl: true,
    //     node_shared_zlib: true,
    //     node_tag: '',
    //     node_target_type: 'executable',
    //     node_use_bundled_v8: true,
    //     node_use_dtrace: true,
    //     node_use_etw: false,
    //     node_use_node_code_cache: true,
    //     node_use_node_snapshot: true,
    //     node_use_openssl: true,
    //     node_use_v8_platform: true,
    //     node_with_ltcg: false,
    //     node_without_node_options: false,
    //     openssl_is_fips: false,
    //     openssl_quic: false,
    //     ossfuzz: false,
    //     shlib_suffix: '108.dylib',
    //     single_executable_application: true,
    //     target_arch: 'arm64',
    //     v8_enable_31bit_smis_on_64bit_arch: 0,
    //     v8_enable_gdbjit: 0,
    //     v8_enable_hugepage: 0,
    //     v8_enable_i18n_support: 1,
    //     v8_enable_inspector: 1,
    //     v8_enable_javascript_promise_hooks: 1,
    //     v8_enable_lite_mode: 0,
    //     v8_enable_object_print: 1,
    //     v8_enable_pointer_compression: 0,
    //     v8_enable_shared_ro_heap: 1,
    //     v8_enable_webassembly: 1,
    //     v8_no_strict_aliasing: 1,
    //     v8_optimized_debug: 1,
    //     v8_promise_internal_field_count: 1,
    //     v8_random_seed: 0,
    //     v8_trace_maps: 0,
    //     v8_use_siphash: 1,
    //     want_separate_host_toolset: 0
    //   }
    // }
    

24.1.2 process 对象的方法和事件:

Node.js 的进程对象 process 提供了一些方法和事件,用于管理当前 Node.js 进程的执行环境。下面是一些常用的进程对象方法和事件的详细介绍。

process对象的方法

  1. process.uptime():该方法返回当前 Node.js 进程的运行时间,以秒为单位。

    console.log(`当前进程已运行 ${process.uptime()}`);
    
  2. process.cwd(): cwd() 方法返回当前工作目录的路径。

    console.log(process.cwd());
    // 输出:/path/to/current/directory
    
  3. process.exit([code]): exit() 方法用于终止当前进程,并返回一个指定的退出码(默认为 0)。通过调用 process.exit(),你可以在代码中主动退出应用程序。

    // 在某个条件满足时退出进程
    if (someCondition) {
      process.exit(1); // 退出并返回退出码 1
    }
    
  4. process.chdir(directory):该方法将 Node.js 应用程序的当前工作目录更改为指定的目录。它主要用于将 Node.js 应用程序的工作目录更改为指定目录,以便访问其他文件或资源。

    process.chdir('/path/to/new/directory');
    console.log(process.cwd()); // 输出:/path/to/new/directory
    
  5. process.memoryUsage():该方法返回一个对象,其中包含有关当前 Node.js 进程内存使用情况的信息,包括堆内存、RSS(常驻集大小)和虚拟内存等。

    console.log(process.memoryUsage());
    
    // 输出:
    // {
    //   rss: 38518784,
    //   heapTotal: 6406144,
    //   heapUsed: 5313064,
    //   external: 413654,
    //   arrayBuffers: 17678
    // }
    
  6. process.nextTick(callback, […args]):该方法将指定的回调函数添加到事件队列中,以在当前操作完成后尽快执行。它类似于 setImmediate(),但比 setImmediate() 更快且优先级更高。

    process.nextTick(() => {
      console.log('next tick');
    });
    console.log('current tick');
    

    在上面的示例代码中,nextTick() 回调函数会在当前操作完成后尽快执行,而不是等待下一个事件循环。因此,在输出 current tick 之后,将立即输出 next tick

  7. process.on(eventName, callback):该方法用于注册事件监听器,以便在发生特定事件时触发回调函数。

process对象的事件

  1. process.on(eventName, callback):该方法用于注册事件监听器,以便在发生特定事件时触发回调函数。

    exit 事件:该事件在 Node.js 进程即将退出时触发,并返回退出码。

    process.on('exit', (code) => {
      console.log(`进程即将退出,退出码:${code}`);
    });
    

    uncaughtException 事件:该事件在 Node.js 进程中捕获到未处理的异常时触发,允许开发者对异常进行适当的处理。

    process.on('uncaughtException', (err) => {
      console.error('捕获到未处理的异常:', err);
      // 进行适当的处理,如记录日志等
      process.exit(1); // 退出并返回退出码 1
    });
    

    beforeExit 事件:该事件在 Node.js 进程即将退出之前触发,但在 exit 事件之前。它允许程序在退出之前执行一些清理操作。

    process.on('beforeExit', () => {
      console.log('进程即将退出,开始清理资源...');
      // 执行清理操作
    });
    

    warning 事件:该事件在 Node.js 进程发出警告时触发,通常是由于使用了过时的 API 或其他不推荐使用的功能。

    process.on('warning', (warning) => {
      console.warn('发出警告:', warning.name);
      console.warn(warning.message);
      console.warn(warning.stack);
    });
    

    unhandledRejection 事件:该事件在 Node.js 进程中发生未处理的 Promise 拒绝时触发。

    process.on('unhandledRejection', (reason, promise) => {
      console.error('未处理的 Promise 拒绝:', reason);
      // 进行适当的处理,如记录日志等
    });
    
  2. abort():该方法会立即终止进程的执行并生成一个包含详细错误信息的核心转储文件。这个核心转储文件可以用于调试和分析进程崩溃的原因。该方法无需任何参数。

  3. getgid():用于获取当前进程的组标识符(Group ID)的方法。Group ID 是用于标识用户所属组的数字。在 Node.js 中,process.getgid() 方法返回一个整数,表示当前进程的组标识符。

    console.log(`当前进程的组标识符是: ${process.getgid()}`);
    
    // 输出:当前进程的组标识符是: 20
    

    请注意,process.getgid() 方法只能在支持的操作系统上使用,并且可能需要适当的权限才能访问组标识符信息。如果无法获取组标识符,则该方法可能会抛出异常。

  4. setgid(id):用于设置当前进程的组标识符(Group ID)的方法。

    // 将当前进程的组标识符设置为 1000
    process.setgid(1000);
    
    // 你也可以使用字符串形式的组名称来设置组标识符,例如:
    //process.setgid('staff'); // 将当前进程的组标识符设置为 "staff"
    

    请注意,设置组标识符需要适当的权限。如果你尝试设置一个无效的组标识符或者没有足够的权限进行设置,process.setgid() 方法可能会抛出异常

  5. getuid():用于获取当前进程的用户标识符(User ID)的方法。User ID 是用于标识用户的数字。在 Node.js 中,process.getuid() 方法返回一个整数,表示当前进程的用户标识符。

    console.log(`当前进程的用户标识符是: ${process.getuid()}`);
    
    // 输出:当前进程的用户标识符是: 501
    

    请注意,process.getuid() 方法只能在支持的操作系统上使用,并且可能需要适当的权限才能访问用户标识符信息。如果无法获取用户标识符,则该方法可能会抛出异常。

  6. setuid(id):用于设置当前进程的用户标识符(User ID)的方法。

    // 将当前进程的用户标识符设置为 1000
    process.setuid(1000);
    
    // 你也可以使用字符串形式的用户名来设置用户标识符,例如:
    //process.setuid('john'); // // 将当前进程的用户标识符设置为 "john"
    

    请注意,设置用户标识符需要适当的权限。如果你尝试设置一个无效的用户标识符或者没有足够的权限进行设置,process.setuid() 方法可能会抛出异常。

  7. getgroups():用于获取当前进程所属的组列表的方法。在 Node.js 中,process.getgroups() 方法返回一个数组,包含了当前进程所属的组标识符(Group ID)。

    console.log(`当前进程所属的组列表:`, process.getgroups());
    
    // 输出:
    // 当前进程所属的组列表: [
    //    20,  12,  61,  79,  80, 81,
    //    98, 702, 704, 705, 703, 33,
    //   100, 204, 250, 395
    // ]
    

    请注意,process.getgroups() 方法只能在支持的操作系统上使用,并且可能需要适当的权限才能访问组列表信息。如果无法获取组列表,则该方法可能会抛出异常。

  8. setgroups(groups):方法需要一个数组作为参数,其中包含了进程的新组列表。

    const newGroups = [100, 200, 300];
    process.setgroups(newGroups);
    

    注意:
    1. 由于设置组列表需要更高级别的特权和操作系统级别的支持,因此 process.setgroups() 在普通用户上下文中几乎不可能成功。实际上,大多数系统都禁止普通用户更改他们所属的组列表。
    2. 使你拥有足够的权限来更改进程的组列表,也应该谨慎使用此方法。更改组列表可能会带来安全风险,并且可能会破坏系统的一致性和可靠性。通常情况下,只有超级用户或具有必要权限的用户才能更改进程的组列表。

  9. initgroups():用于初始化进程的附加组列表,并不是设置或更改组列表。它通常在 Unix-like 系统中使用,用于设置进程的有效组 ID 和附加组 ID。该方法需要第一个参数为用户名,第二个参数为用户所属的组 ID。它会根据这些信息初始化进程的附加组列表。

    注意:
    1. 由于安全性和权限的考虑,Node.js 并没有提供直接的接口来执行此操作。如果你需要设置或更改进程的组列表,你需要使用适当的系统命令或平台特定的工具。
    2. 在进行任何与组列表相关的操作时,务必谨慎,并确保你拥有足够的权限和了解可能带来的风险。

  10. kill(pid, [signal]):Node.js 的内置方法,它将信号发送给进程(即进程id),信号是字符串类型,默认 SIGTERM

    const process = require('process');
    
    // 获取当前进程的 PID
    console.log(`当前进程的 PID: ${ process.pid}`);
    
    // 使用 process.kill() 方法终止指定进程
    // 注意:这里只是演示,实际上不会终止当前进程
    process.kill(currentPid, 'SIGTERM');
    
  11. unmask([mask]):设置 Node.js 进程的文件模式创建掩码。子进程从父进程继承掩码。返回前一个掩码。

    const newmask = 0o010
    const oldmask = process.unmask(newmask);
    
    console.log("修改前的掩码:" + oldmask.toString(8) + " 修改后的掩码:" + oldmask.toString(8))
    

process的信号事件

  • SIGINT:当用户按下 Ctrl+C 时,会触发此信号。通常用于捕获中断操作。
  • SIGTERM:当进程被要求终止时,会触发此信号。通常用于优雅地关闭进程。
  • SIGHUP:当终端挂起或控制进程从控制终端断开连接时,会触发此信号。通常用于重新加载配置文件。
  • SIGUSR1 和 SIGUSR2:这两个信号主要用于用户自定义用途。可以通过编写一个程序来监听这些信号,并在接收到信号时执行特定操作。
  • SIGCHLD:当子进程结束时,会触发此信号。可以监听此信号以获取子进程的状态信息。
  • SIGCONT:当进程从暂停状态恢复运行时,会触发此信号。通常用于恢复阻塞的操作。
  • SIGSTOP:当进程被暂停时,会触发此信号。通常用于暂停进程。
  • SIGTSTP:当用户按下 Ctrl+Z 时,会触发此信号。通常用于暂停进程。
  • SIGTTIN 和 SIGTTOU:这两个信号分别表示后台进程请求将自身放入后台运行以及前台进程请求将自身从后台切换到前台运行。

24.2 子进程

Node.js 的子进程(Child Process)是指在一个 Node.js 进程中启动另一个独立的 Node.js 进程。子进程可以用于执行一些耗时的任务,如文件读写、网络请求等,而不会阻塞主进程的执行。在 Node.js 中,可以使用 child_process 模块来创建和管理子进程。

child_process 模块提供了几种方法来创建子进程,包括 spawn()exec()execFile()fork()

  1. spawn(command, args, options):创建一个子进程并返回一个 ChildProcess 实例。command是要执行的命令,args 是一个包含命令及其参数的数组,options 是一个可选的配置对象。

    options 参数可以包含以下属性:

    • cwd(字符串):子进程的工作目录。默认为当前进程的工作目录。
    • env(对象):子进程的环境变量。默认为当前进程的环境变量。
    • ext(字符串):可执行文件的扩展名。默认为'.cmd'(Windows)或'.sh'(macOS/Linux)。
    • execArgv(数组):传递给可执行文件的参数列表。默认为空数组。
    • stdio(字符串):子进程的输入、输出和错误流。默认为'inherit',表示继承父进程的流。其他可选值包括’pipe’(将子进程的输出发送到父进程的输入流)、'ignore'(忽略子进程的输出)和'ipc'(使用IPC通道进行通信)。
    • detached(布尔值):是否将子进程与父进程分离。默认为 false。如果设置为 true,则子进程将在后台运行,不会阻塞父进程。
    • preexecPath(字符串):在启动子进程之前设置的 PATH 环境变量。默认为当前进程的PATH环境变量。
    • user(字符串):以指定用户身份运行子进程。默认为当前进程的用户。
    • uid(数字):以指定用户ID运行子进程。默认为当前进程的用户ID。
    • gid(数字):以指定组ID运行子进程。默认为当前进程的组ID。
    • windowsVerbatimArguments(布尔值):是否在Windows上使用严格的参数解析。默认为 false。如果设置为 true,则在Windows上使用严格的参数解析,以避免某些命令行注入攻击。

    swpan 示例代码:

    const { spawn } = require('child_process');
    
    const child = spawn('ls', ['-lh', '/usr'], {
      cwd: '/tmp',
      env: { ...process.env, MY_ENV_VAR: 'my-value' },
      stdio: 'inherit',
      detached: true,
    });
    

    spawn 创建多个子进程

    const { spawn } = require('child_process');
    
    // 创建多个子进程
    const child1 = spawn('command1', ['arg1', 'arg2']);
    const child2 = spawn('command2', ['arg1', 'arg2']);
    const child3 = spawn('command3', ['arg1', 'arg2']);
    
    // 监听子进程的输出数据
    child1.stdout.on('data', (data) => {
      console.log(`子进程1输出:${data}`);
    });
    
    child2.stdout.on('data', (data) => {
      console.log(`子进程2输出:${data}`);
    });
    
    child3.stdout.on('data', (data) => {
      console.log(`子进程3输出:${data}`);
    });
    
    // 监听子进程的错误信息
    child1.stderr.on('data', (data) => {
      console.error(`子进程1错误:${data}`);
    });
    
    child2.stderr.on('data', (data) => {
      console.error(`子进程2错误:${data}`);
    });
    
    child3.stderr.on('data', (data) => {
      console.error(`子进程3错误:${data}`);
    });
    
    // 监听子进程的退出事件
    child1.on('exit', (code, signal) => {
      if (code !== null) {
    	console.log(`子进程1退出,退出码:${code}`);
      } else if (signal !== null) {
    	console.log(`子进程1被信号 ${signal} 终止`);
      } else {
    	console.log('子进程1正常退出');
      }
    });
    
    child2.on('exit', (code, signal) => {
      if (code !== null) {
    	console.log(`子进程2退出,退出码:${code}`);
      } else if (signal !== null) {
    	console.log(`子进程2被信号 ${signal} 终止`);
      } else {
    	console.log('子进程2正常退出');
      }
    });
    
    child3.on('exit', (code, signal) => {
      if (code !== null) {
    	console.log(`子进程3退出,退出码:${code}`);
      } else if (signal !== null) {
    	console.log(`子进程3被信号 ${signal} 终止`);
      } else {
    	console.log('子进程3正常退出');
      }
    });
    
  2. exec(command, callback):同步地执行一个命令,当命令执行完成后,会调用回调函数。回调函数接收两个参数:errorstdout

    示例代码:

    const { exec } = require('child_process');
    
    exec('ls -lh /usr', (error, stdout, stderr) => {
      if (error) {
    	console.error(`执行错误: ${error}`);
    	return;
      }
      console.log(`输出: ${stdout}`);
      console.error(`错误输出: ${stderr}`);
    });
    

    exec 创建多个子进程

    const { exec } = require('child_process');
    
    // 创建第一个子进程
    const child1 = exec('command1', (error, stdout, stderr) => {
      if (error) {
    	console.error(`执行错误: ${error}`);
    	return;
      }
      console.log(`输出: ${stdout}`);
      console.error(`错误: ${stderr}`);
    });
    
    // 创建第二个子进程
    const child2 = exec('command2', (error, stdout, stderr) => {
      if (error) {
    	console.error(`执行错误: ${error}`);
    	return;
      }
      console.log(`输出: ${stdout}`);
      console.error(`错误: ${stderr}`);
    });
    
    // 等待所有子进程完成
    child1.on('exit', () => {
      console.log('子进程1已完成');
    });
    
    child2.on('exit', () => {
      console.log('子进程2已完成');
    });
    
  3. execFile(file, args, options, callback):类似于 spawn 方法,但接受一个文件路径作为命令。其他参数与 spawn 方法相同。

    示例代码:

    const { execFile } = require('child_process');
    
    execFile('ls', ['-lh', '/usr'], (error, stdout, stderr) => {
      if (error) {
    	console.error(`执行错误: ${error}`);
    	return;
      }
      console.log(`输出: ${stdout}`);
      console.error(`错误输出: ${stderr}`);
    });
    

    execFile 创建多个子进程

    const { execFile } = require('child_process');
    
    // 创建第一个子进程
    execFile('command1', [], (error, stdout, stderr) => {
      if (error) {
    	console.error(`执行错误: ${error}`);
    	return;
      }
      console.log(`输出: ${stdout}`);
      console.error(`错误: ${stderr}`);
    });
    
    // 创建第二个子进程
    execFile('command2', [], (error, stdout, stderr) => {
      if (error) {
    	console.error(`执行错误: ${error}`);
    	return;
      }
      console.log(`输出: ${stdout}`);
      console.error(`错误: ${stderr}`);
    });
    
  4. fork(modulePath, [args], options):创建一个子进程,并加载指定的模块。子进程将继承父进程的代码,因此它们可以共享相同的内存空间。modulePath 是要加载的模块的路径,args 是一个可选的参数数组,options 是一个可选的配置对象。

    options 参数可以包含以下属性:

    • execArgv(Array):传递给子进程的可执行文件的参数列表。默认值为 undefined,表示使用父进程的argv数组。
    • env(Object):传递给子进程的环境变量对象。默认值为 undefined,表示使用父进程的环境变量。
    • ext(String):可执行文件的扩展名。默认值为 '.js'
    • cwd(String):子进程的工作目录。默认值为 undefined,表示使用父进程的工作目录。
    • detached(Boolean):是否将子进程与父进程分离。默认值为 false。如果设置为 true,则子进程将在后台运行,不会继承父进程的文件描述符。
    • stdio(String):子进程的标准输入、输出和错误流。默认值为 'inherit',表示从父进程继承这些流。其他有效值包括 'pipe'(创建一个管道)、'ignore'(忽略子进程的输出)和 'ipc'(使用IPC通道进行通信)。
    • silent(Boolean):是否禁止子进程的输出。默认值为 false。如果设置为 true,则子进程的输出将被丢弃。
    • uid(Number):子进程的用户ID。默认值为 undefined,表示使用父进程的用户ID。
    • gid(Number):子进程的组ID。默认值为 undefined,表示使用父进程的组ID。
    • signal(String):要发送给子进程的信号。默认值为 undefined,表示不发送任何信号。其他有效值包括 'SIGTERM''SIGINT'等。

    示例代码:

    const { fork } = require('child_process');
    
    const child = fork('./child.js');
    

    fork 创建多个子进程

    main.js

    const { fork } = require('child_process');
    
    const child1 = fork('./child1.js');
    const child2 = fork('./child2.js');
    
    child1.on('message', (msg) => {
      console.log(`子进程1收到消息: ${msg}`);
    });
    
    child2.on('message', (msg) => {
      console.log(`子进程2收到消息: ${msg}`);
    });
    
  5. kill(pid[, signal]):终止一个子进程。pid 是要终止的子进程的进程ID,signal 是一个可选的信号名称,默认为 SIGTERM

    const { kill } = require('child_process');
    
    kill(child.pid, 'SIGKILL');
    
  6. unref():取消子进程的引用计数。当子进程不再被引用时,它将自动退出。这在某些情况下可以提高性能,例如在事件循环空闲时。

    const { spawn } = require('child_process');
    
    const child = spawn('ls', ['-lh', '/usr']);
    child.unref();
    

24.3 cluster

Node.js 的 cluster 模块是一个用于创建多进程程序的工具。它允许你将一个大的任务分解成多个子任务,然后使用多个进程并行地执行这些子任务,从而提高程序的性能和吞吐量。

24.3.1 cluster的工作原理

Node.js 的 cluster 模块允许你利用多核CPU的优势,通过将一个大型的 Node.js 的 cluster 模块允许你利用多核CPU的优势,通过将一个大型的计算任务分解成多个子任务并行处理,从而提高应用程序的性能和吞吐量。

在 cluster 模式中,存在两个主要的概念:masterworkermaster 即主进程,负责创建和管理所有的子进程(即worker)。 在 Node.js 程序启动时,首先由主进程生成多个子进程。每个子进程都是一个独立的 Node.js 实例,它们共享相同的代码和数据,但各自拥有自己的内存空间和事件循环,彼此之间不会相互影响。

Master 进程负责监听新的连接请求,当有新的请求到达时,它将请求分配给其中一个 Worker 进程。如果某个 Worker 进程因某种原因崩溃或无法处理请求,Master 进程将会重新分配该请求给其他 Worker 进程。

此外,Worker 进程之间可以通过 process.send() 方法来发送信息,主进程和其他 Worker 进程则可以通过监听 'message' 事件来接收这些信息。

cluster 模块的主要功能如下:

  • 创建进程:cluster.fork() 方法用于创建一个新的子进程。这个方法接受一个回调函数作为参数,当子进程创建成功后,这个回调函数会被调用。setupPrimary() 方法则用于设置主进程的配置

  • 监听事件:cluster 对象提供了一些事件,如'exit''message'等,可以用来监听子进程的状态变化。

  • 发送消息:可以使用 process.send() 方法向主进程或其他子进程发送消息。

  • 接收消息:可以使用 process.on('message') 方法监听并处理来自其他进程的消息。

cluster方法和事件

cluster 模块提供了一些方法和事件,用于在 Node.js 应用程序中创建和管理多个子进程。下面是对 cluster 模块常用的方法和事件的详细介绍:

cluster的属性和方法:

  • cluster.isMaster: 判断当前进程是否为主进程。
  • cluster.fork(): 创建一个新的子进程。
  • cluster.setupMaster([settings]): 设置主进程的工作模式。
  • cluster.workers: 返回所有子进程的引用。
  • cluster.disconnect(callback): 断开与子进程的连接。
  • cluster.kill(pid, signal): 终止指定的子进程。
  • cluster.exec(command, args, options): 在主进程中执行命令。
  • cluster.schedulingPolicy(): 获取或设置工作调度策略。
  • cluster.maxSockets: 获取或设置最大文件描述符数。
  • cluster.openPort(port[, address], callback): 打开指定端口并返回一个端口对象。
  • cluster.registerDomain(options, callback): 注册一个自定义域名。
  • cluster.unregisterDomain(domain, callback): 注销一个自定义域名。

cluster 模块也定义了一些特定的事件,主要包括:

  • cluster.on(‘fork’, callback): 用于监听子进程创建事件。当一个新的子进程被创建时,会触发这个事件。回调函数 callback 会在子进程创建后执行,接收一个参数 worker,表示新创建的子进程对象。
  • cluster.on(‘exit’, callback): 用于监听子进程退出事件。当一个子进程退出时,会触发这个事件。回调函数 callback 会在子进程退出后执行,接收两个参数:worker 表示退出的子进程对象,code 表示子进程退出时的退出码。
  • cluster.on(‘message’, callback): 用于监听主进程与子进程之间的消息传递。当主进程向子进程发送消息时,会触发这个事件。回调函数 callback 会在收到消息后执行,接收两个参数:worker 表示发送消息的子进程对象,message 表示发送的消息内容。
  • cluster.on(‘online’, callback): 用于监听子进程启动成功事件。当一个子进程启动成功后,会触发这个事件。回调函数 callback 会在子进程启动成功后执行,接收一个参数:worker 表示启动成功的子进程对象。
  • cluster.on(‘listening’, callback): 用于监听服务器开始监听指定端口的事件。当服务器开始监听指定端口时,会触发这个事件。回调函数 callback 会在服务器开始监听后执行,接收一个参数:server 表示服务器对象。
  • cluster.on(‘message’, callback): 用于监听主进程与子进程之间的消息传递。当主进程向子进程发送消息时,会触发这个事件。回调函数 callback 会在收到消息后执行,接收两个参数:worker 表示发送消息的子进程对象,message 表示发送的消息内容。
  • cluster.on(‘disconnect’, callback): 用于监听主进程 cluster.on('disconnect', callback) 是一个事件监听器,用于监听主进程与子进程之间的连接断开事件。当主进程与子进程之间的连接断开时,会触发这个事件。回调函数 callback 会在连接断开后执行,接收一个参数:worker 表示断开连接的子进程对象。

这些方法和事件可以帮助我们创建和管理多个子进程,并与它们之间进行通信。通过合理地使用 cluster 模块,我们可以利用多核处理器的能力,提高应用程序的性能和可扩展性。

worker的方法和事件

Worker 对象是 cluster 模块中表示工作进程的引用,它提供了一些方法和事件,用于在工作进程中进行操作和处理事件。下面是对 Worker 对象常用的方法和事件的详细介绍:

worker 对象的属性和方法:

  • worker.process: 返回当前 worker 进程的引用。
  • worker.id: 返回当前 worker 的唯一标识符。
  • worker.send(message): 向主进程发送消息。
  • worker.disconnect(): 断开与主进程的连接。
  • worker.kill(): 终止 worker 进程。
  • worker.restart(): 重启 worker 进程。
  • worker.stdout.write(data): 向标准输出流写入数据。
  • worker.stderr.write(data): 向标准错误流写入数据。
  • worker.stdin.pause(): 暂停从标准输入流读取数据。
  • worker.stdin.resume(): 恢复从标准输入流读取数据。
  • worker.nextTick(callback): 将回调函数添加到事件队列中,在下一个tick时执行。
  • worker.setImmediate(callback): 将回调函数添加到事件队列中,在当前tick结束时执行。
  • worker.uncaughtException(err): 监听未捕获的异常事件。
  • worker.unhandledRejection(reason, promise): 监听未处理的 Promise 拒绝事件。

worker 对象的事件

  • worker.on(‘fork’): 用于监听子进程的衍生 fork 事件。当 Node.js 进程通过 cluster.fork() 方法衍生出一个新的子进程时,就会触发这个事件。回调函数接收一个参数,表示新创建的子进程对象。
  • worker.on(‘listening’): 用于监听子进程开始监听指定端口的事件。当子进程成功启动并开始监听指定的端口时,就会触发这个事件。回调函数接收一个参数,表示服务器对象。
  • worker.on(‘message’): 用于监听子进程发送消息的事件。当主进程向子进程发送消息时,就会触发这个事件。回调函数接收两个参数,第一个参数是发送消息的子进程对象,第二个参数是发送的消息内容。
  • worker.on(‘exit’): 是一个事件监听器,用于监听子进程退出的事件。当子进程正常退出时,就会触发这个事件。回调函数接收两个参数,第一个参数是退出码,第二个参数是信号。
  • worker.on(‘online’): 是一个事件监听器,用于监听子进程成功连接到主进程的事件。当子进程成功连接到主进程时,就会触发这个事件。回调函数接收一个参数,表示子进程对象。
  • worker.on(‘disconnect’): 用于监听主进程与子进程之间的连接断开的事件。当主进程与子进程之间的连接断开时,就会触发这个事件。回调函数接收一个参数,表示子进程对象。
  • worker.on(‘error’): 用于监听子进程发生错误的事件。当子进程发生错误时,就会触发这个事件。回调函数接收两个参数,第一个参数是发送消息的子进程对象,第二个参数是错误对象。
  • worker.on(‘setup’): 用于监听子进程启动时执行的设置操作的事件。当子进程启动并开始执行任务之前,就会触发这个事件。回调函数没有参数。
  • worker.on('cleanup): 用于监听子进程退出时执行的清理操作的事件。当子进程正常退出时,就会触发这个事件。回调函数没有参数。

Worker 对象的方法和事件使我们能够在工作进程中发送消息、结束进程、处理事件等。通过这些方法和事件,我们可以更好地管理工作进程的行为,并与主进程进行通信和协作。

24.3.2 父进程开启多个子进程

在 Node.js 中,可以使用 cluster 模块来创建多个子进程。cluster 模块允许开发者将一个 Node.js 应用分解成多个独立的工作进程,每个工作进程可以独立地处理请求,从而提高应用的性能和可伸缩性。

多个子进程运行服务器并监听同一个地址:

服务端

server.js

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`主进程 ${process.pid} 正在运行`);

  // 衍生工作进程。
  for (let i = 0; i < numCPUs; i++) {
    const worker = cluster.fork();
	
	// 为子进程的worker注册listening事件
	worker.on('listening', (address) => {
	  console.log("子进程中服务器的地址:" + address.address + ":" + address.port);
	});
	
	// 使用子进程的wroker向主进程发送消息
	worker.send("message");
	  
	worker.on('message', (msg) => {
	  console.log("主进程收到子进程的消息:", msg.toString())
	});
	  
	// 强制关闭子进程
	//worker.kill();
	//worker.destroy();
	// 子进程退出
	worker.on('exit', (code, signal) => {
	  console.log("子进程退出,code", code);
	});
  }

  // fork事件
  cluster.on('fork', (worker) => {
	console.log("子进程:" + worker.id);
  });
	
  // 主进程接接收子进程的反馈信息
  cluster.on('online', (worker) => {
    console.log("已收到子进程:" + worker.id + " 反馈信息");
  });

  // 主进程退出事件
  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 已退出`);
  });
} else {
  // 工作进程代码
  process.on('message', (message) => {
    console.log(`Received message from master: ${JSON.stringify(message)}`);
  });

  // 工作进程可以共享任何TCP连接。
  // 在本例子中,共享的是一个HTTP服务器。
  const server = http.createServer();

  const serverPort = 9000;
  const serverHost = '127.0.0.1';
	
  server.listen(serverPort, serverHost, () => {
    console.log("子进程中服务器已经创建");
  })
	
  server.on('request', (req, resp) => {
    console.log("请求路径:", req.url);
    resp.writeHead(200, {'Content-Type': 'text/plain'});
    resp.write('hello client!');
    resp.end();
    resp.on('error', (err) => { 
      console.log("发生错误 error:", err);
    })
    // 向主进程发送消息
    process.send("sub_process message");
  });

  console.log(`工作进程 ${process.pid} 已启动`);
}

在上面的代码中,首先引入了 cluster 模块。然后使用 cluster.isMaster 判断当前进程是否为主进程。如果是主进程,则通过循环调用 cluster.fork() 方法创建多个子进程。

需要注意的是,在工作进程中使用 process.send() 方法向主进程发送消息。在主进程中,可以通过监听 worker.on('message') 事件来接收来自工作进程的消息,并进行处理。

在主进程中使用 worker.send() 方法向工作进程发送消息,同时在工作进程中使用 process.on()方法监听子进程中发送的消息。

工作进程中,首先会输出当前工作进程的进程ID。然后,创建一个HTTP服务器,并输出服务器已经创建的消息。当有请求到达服务器时,会输出请求路径,并向客户端返回一个简单的响应。同时,工作进程还会向主进程发送一条消息。

如果不是主进程,则表示该进程是子进程。在这个例子中,子进程会创建一个HTTP服务器并监听9000端口。最后输出相应的日志信息。

以上代码使用了 cluster 模块创建了多个子进程,并实现了主进程与子进程之间的通信。主进程和子进程之间可以相互发送消息,而且每个子进程都运行着一个独立的HTTP服务器。

客户端

使用 Node.js 的 http 模块发送10个GET请求到本地服务器(IP地址为127.0.0.1,端口号为9000)。

client.js

const http = require('http');
const numCPUs = require('os').cpus().length;

const options = {
  hostname: '127.0.0.1',
  port: 9000,
  path: '/',
  method: 'GET',
};

for (let i = 0; i < 10; i++) {
  const req = http.request(options, (resp) => {
    let data = '';
	resp.on('data', (chunk) => {
	  data += chunk;
	});
	resp.on('end', () => {
	  console.log("请求结束 data=", data);
    });
  });

  req.on('error', (error) => {
	console.error(error);
  });

  req.end();
}

总结

当涉及到在 Node.js 中处理多任务和系统资源利用率时,进程和子进程是非常重要的概念。下面是进程和子进程的总结:

  1. 进程:进程是操作系统中正在运行的程序的实例。在 Node.js 中,每个应用程序都是一个进程。通过 process 模块,我们可以控制和管理当前进程。这包括获取和设置进程的环境变量、命令行参数、工作目录等信息,并且可以使用 process.exit() 方法来退出当前进程。

  2. 子进程:子进程是由父进程创建的新进程。在 Node.js 中,我们可以使用 child_process 模块来创建和控制子进程。子进程通常用于执行一些耗时的或需要额外计算资源的任务,例如执行外部程序或脚本。

    • spawn(): 通过流的方式将数据传输到子进程中,适用于长时间运行的进程。
    • exec(): 在新的Shell中执行命令,并将结果输出到回调函数中。
    • execFile(): 直接执行一个文件,无需先打开一个 Shell。
    • fork(): 在新的Node.js进程中执行指定的模块,用于创建可独立运行的进程。

通过使用进程和子进程,我们可以实现并发处理和更高的系统资源利用率。Node.js 提供了方便的工具和API来控制和管理进程,从而实现多任务处理的需求。

25-数据库访问

对于 Node.js 应用程序中的数据库访问,通常会使用一些流行的数据库管理系统,比如 MongoDB、MySQL、PostgreSQL、Redis 等。下面我们将简要介绍如何在 Node.js 应用程序中进行数据库访问:

  1. 使用 Node.js 驱动程序

    大多数数据库系统都提供了针对 Node.js 的官方或第三方驱动程序库,可以通过这些驱动程序来连接和操作数据库。比如对于 MongoDB,你可以使用官方的 mongodb 驱动程序,对于 MySQL 和 PostgreSQL,你可以使用 mysql 和 pg 等驱动程序。

  2. 使用 ORM 或 ODM 框架

    ORM(对象关系映射)和 ODM(对象文档映射)框架可以帮助开发人员更轻松地与数据库进行交互,而不必直接编写 SQL 或 MongoDB 查询。在 Node.js 中,一些流行的 ORM 框架包括 Sequelize(适用于关系型数据库)和 TypeORM,而流行的 ODM 框架包括 Mongoose(适用于 MongoDB)。

  3. 异步操作

    Node.js 以其异步非阻塞的特性而闻名,因此在进行数据库访问时,通常会使用数据库驱动程序提供的异步 API 或者利用 Promise、async/await 等语法来处理异步操作,以避免阻塞整个应用程序。

  4. 连接池管理

    为了提高数据库访问的性能和效率,通常会使用连接池管理工具来管理数据库连接。例如,在 Node.js 中,可以使用 generic-pool 等库来实现数据库连接池管理,以便在需要时从池中获取连接,并在使用完毕后释放连接。

25.1 MySQL

在web项目中我们经常要用到 MySQL 数据库,现在我们介绍一下在 Node.js 中该如何使用 MySQL 数据库。

25.1.1 安装依赖

要在 Node.js 中操作 MySQL ,首先需要安装一个名为 mysqlmysql2 的npm包。这两个模块都可以用于在 Node.js 中连接和操作 MySQL 数据库。可以通过以下命令安装:

# 安装mysql依赖包
npm install mysql

# 安装mysql2依赖包
npm install mysql2

25.1.2 MySQL 和 mysql2

mysqlmysql2 都是 Node.js 中用于连接和操作 MySQL 数据库的库。它们之间的主要区别如下:

  • 安装方式:mysql 是官方推荐的库;而 mysql2 是一个第三方库,需要通过 npm 安装。

  • API 差异:mysqlmysql2 的 API 基本相同,但在某些细节上有所不同。例如,mysql2 提供了 Promise 支持,使得异步操作更加方便。

  • 性能:由于 mysql2 使用了流式处理,因此在处理大量数据时,它的性能可能会优于 mysql。

  • 社区支持:mysql2 是由 MySQL 官方维护的,因此它得到了更多的社区支持和更新。

如果您正在使用 Node.js,并且已经熟悉 mysql 库,那么继续使用它是完全可以的。但是,如果你想要一个更现代化、功能更丰富的库,或者你正在寻找更好的社区支持,那么可以考虑使用 mysql2。我们主要以 mysql2 为例。

25.1.3 建立连接

mysql.createConnection() 是 Node.js 中用于创建与 MySQL 数据库连接的方法。它创建一个表示与 Mysql 连接的 Connection 对象。

该方法接受一个包含连接配置的对象作为参数,并返回一个表示数据库连接的连接对象。通过该连接对象,可以执行各种数据库操作,如查询、插入、更新和删除等。

以下是 mysql.createConnection 方法的参数说明:

  • host(可选):数据库服务器的主机名或 IP 地址。默认值为 localhost。
  • user(可选):用于连接到数据库的用户名。默认值为 root。
  • password(可选):用于连接到数据库的密码。默认值为空字符串。
  • database(可选):要连接的数据库名称。默认值为 test。
  • port(可选):数据库服务器的端口号。默认值为 3306。
  • socketPath(可选):当未指定主机时使用的 Unix 套接字路径。默认值为空字符串。
  • flags(可选):传递给底层驱动程序的标志。默认值为 ‘’。
  • charset(可选):指定字符集编码。默认值为 ‘utf8mb4’。
  • timezone(可选):指定时区。默认值为 ‘local’。
  • connectTimeout(可选):连接超时时间,单位为毫秒。
  • stringifyObjects:决定是否将对象转换为字符串。
  • debug(可选):是否启用调试模式。默认值为 false。
  • typeCast(可选):决定是否应该自动将从数据库接收的数据类型转换为JavaScript类型。

如果不需要数据库,可以使用 connectionend 或者 destroy 方法关闭数据库连接。

需要注意的是,mysql.createConnection 方法只会创建一个新的连接对象,但并不会建立实际的数据库连接。实际的连接会在调用 connection.connect 方法时建立。另外,每次使用 createConnection 创建数据库连接时,都会创建一个新的连接对象,因此在使用完毕后,应该及时调用 connection.end 方法关闭数据库连接,释放资源。

除了 mysql.createConnection 方法,mysql2 模块还提供了其他创建数据库连接的方法,如 mysql.createPoolmysql.createPoolCluster,它们适用于连接池和多个数据库服务器的情况。根据具体的需求,选择合适的方法来创建数据库连接。

const mysql = require('mysql');

// 创建连接对象
const connection = mysql.createConnection({
  host: '127.0.0.1', // 数据库地址
  user: 'root', // 数据库用户名
  password: '123456', // 数据库密码
  database: 'db_test', // 数据库名称
});

// 连接到数据库
connection.connect((err) => {
  if (err) {
    console.error('连接失败 err:', err);
    return;
  }
  console.log('成功连接到数据库');
});

// 关闭数据库连接
connection.end();

注意:您需要自己先安装好 MySQL 软件,才能正常访问 MySQL 数据库。如果没有安装 MySQL 可以参考这篇文章:超详细的MySQL下载安装教程(免费社区版)

报错: Error: ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by server; consider upgrading MySQL client
原因:MySQL 8.0.4开始,MySQL默认身份验证插件从 mysql_native_password 改为 caching_sha2_password 导致。
解决方法:
安装 mysql2 依赖:npm install mysql2,引入依赖 const mysql = require('mysql2'); 然后再次运行即可。

以下是 connection 对象常用的一些方法:

  • query(sql, values, callback):执行数据库查询操作。该方法接收一个 SQL 查询字符串或预处理语句,一个可选的参数数组 values,以及一个回调函数 callback。当查询完成时,callback 函数将被调用,它接收两个参数,第一个参数为错误信息(如果有),第二个参数为查询结果。

  • execute(sql, values, callback):执行数据库查询操作,并返回一个结果集对象。该方法接收一个 SQL 查询字符串或预处理语句,一个可选的参数数组 values,以及一个回调函数 callback。当查询完成时,callback 函数将被调用,它接收两个参数,第一个参数为错误信息(如果有),第二个参数为一个结果集对象,代表查询结果。

  • beginTransaction(callback):开启一个新的事务。该方法接收一个回调函数 callback。当事务开启后,callback 函数将被调用。

  • commit(callback):commit(callback):提交当前事务。该方法接收一个回调函数 callback。当提交成功后,callback 函数将被调用。

  • rollback(callback):回滚当前事务。该方法接收一个回调函数 callback。当回滚成功后,callback 函数将被调用。

  • ping(callback):测试数据库连接是否可用。该方法接收一个回调函数 callback。当连接可用时,callback 函数将被调用。

  • changeUser(options, callback):更改数据库连接的用户信息。该方法接收一个包含新用户信息的对象 options,以及一个回调函数 callback。当更改成功后,callback 函数将被调用。

  • end([callback]):关闭数据库连接。接受一个可选的回调函数作为参数,在连接关闭后执行。如果没有提供回调函数,则默认打印一条消息到控制台。该方法返回一个 Promise,当连接关闭时 resolve。

  • escape(value):将字符串转义为安全的 SQL 字符串。该方法接收一个字符串类型的参数 value,并返回一个已经转义的字符串。

  • format(sql, values):格式化 SQL 语句,并将参数列表中的值替换到 SQL 语句中。该方法接收一个 SQL 查询字符串或预处理语句,一个参数数组 values,并返回一个已经格式化的 SQL 语句。

  • promise():返回一个 Promise 对象,用于执行异步操作。

使用这些方法可以实现各种数据库操作,如查询、插入、更新、删除等。但是,在使用 queryexecute 方法执行查询时,需要小心防范 SQL 注入攻击,避免在查询中使用未经过验证和转义的字符串。同时还需要注意,使用 beginTransaction 方法开启事务时,务必要确保在事务结束前使用 commitrollback 方法结束事务,避免出现数据不一致的情况。

25.1.4 查询

connection.query 是一个用于执行 SQL 查询的方法,通常在 Node.js 中使用 mysql 模块与 MySQL 数据库进行交互时使用。它接受一个 SQL 查询语句作为参数,并返回一个 Promise,当查询完成时 resolve

我们先创建一个数据表:

# 创建一张用户表
CREATE TABLE `t_user` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
    `name` VARCHAR(20) DEFAULT '' COMMENT '姓名',
    `age` INT(11) DEFAULT 18 COMMENT '年龄',
	`balance` DECIMAL(10,2) COMMENT '金额',
    PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

#添加数据
INSERT INTO t_user(name, balance) VALUES('张三', 1200), ('李四', 500);

以下是一个示例代码,演示如何使用 connection.query 方法执行查询操作:

const mysql = require('mysql2');

// 创建连接对象 port(默认3306端口号)
const connection = mysql.createConnection({
  host: '127.0.0.1', // 数据库地址
  user: 'root', // 数据库用户名
  password: '123456', // 数据库密码
  database: 'db_test', // 数据库名称
});

// 连接到数据库
connection.connect((err) => {
  if (err) {
    console.error('连接失败:', err);
    return;
  }
  console.log('成功连接到数据库');
});

// 执行查询操作(单条)
connection.query('SELECT * FROM t_user where id=1;', (err, results, fields) => {
  if (err) {
    console.error('查询失败:', err);
    return;
  }
  console.log('查询结果:', results);
});

// // 执行查询操作(所有)
// connection.query('SELECT * FROM t_user', (err, results, fields) => {
//   if (err) {
//     console.error('查询失败:', err);
//     return;
//   }
//   console.log('查询结果:', results);
// });

// 关闭数据库连接
connection.end();

注意:生产环境中尽量避免使用 select * 查询语句,应该使用 select 字段名1, 字段名2, ... 的方式去查询数据。

25.1.5 插入、更新和删除

connection.query 是一个用于执行 SQL 查询的方法可以完成插入、更新和删除等操作。

插入

下面是插入数据的例子:

const mysql = require('mysql2');

// 创建数据库连接 port(默认3306端口号)
const connection = mysql.createConnection({
  host: '127.0.0.1', // 数据库地址
  user: 'root', // 数据库用户名
  password: '123456', // 数据库密码
  database: 'db_test', // 数据库名称
});

// 连接到数据库
connection.connect((err) => {
  if (err) {
	throw err;
  }
  console.log('已连接到数据库');

  // 插入一条数据
  const sql = 'INSERT INTO t_user(name, age, balance) VALUES (?, ?, ?);';
  const values = ['王五', 20, 0];

  connection.query(sql, values, (err, result) => {
    if (err) throw err;
    console.log('数据插入成功');
    console.log('插入的数据ID为:', result.insertId);
  });

  // 关闭数据库连接
  connection.end();
});

注意:插入多条数据,可以使用for循环,先拼接好sqlvalues,然后去执行 connection.query 方法。

更新

下面是更新数据的示例:

const mysql = require('mysql2');

// 创建数据库连接 port(默认3306端口号)
const connection = mysql.createConnection({
  host: '127.0.0.1', // 数据库地址
  user: 'root', // 数据库用户名
  password: '123456', // 数据库密码
  database: 'db_test', // 数据库名称
});

// 连接到数据库
connection.connect((err) => {
  if (err) {
	throw err;
  }
  console.log('已连接到数据库');

  // 更新数据
  const sql = 'UPDATE t_user SET balance = ? WHERE id = ?;';
  const values = [1000, 3]; // 假设要更新id为3的数据

  connection.query(sql, values, (err, result) => {
    if (err) throw err;
    console.log('数据更新成功');
  });

  // 关闭数据库连接
  connection.end();
});

注意:更新多条数据同样可以使用for循环来完成。

删除

下面是一条删除数据的案例:

const mysql = require('mysql2');

// 创建数据库连接 port(默认3306端口号)
const connection = mysql.createConnection({
  host: '127.0.0.1', // 数据库地址
  user: 'root', // 数据库用户名
  password: '123456', // 数据库密码
  database: 'db_test', // 数据库名称
});

// 连接到数据库
connection.connect((err) => {
  if (err) {
	throw err;
  }
  console.log('已连接到数据库');

  // 删除数据
  const sql = 'DELETE FROM t_user where id=?;';
  const values = [3]; // 假设要删除id为3的数据

  connection.query(sql, values, (err, result) => {
    if (err) throw err;
    console.log('数据更新成功');
  });

  // 关闭数据库连接
  connection.end();
});

连接池

在 mysql2 中,连接池是一种管理数据库连接的机制,用于提高应用程序的性能和可靠性。连接池维护了一个固定数量的数据库连接,并将这些连接存储在一个池中。当应用程序需要访问数据库时,它从连接池中获取一个可用的连接,并在使用完毕后将连接返回到连接池中,以便其他请求可以重复利用该连接。

mysql2 提供了 mysql.createPool 方法用于创建连接池。该方法接收一个配置对象,其中包含连接池的参数,例如最大连接数、最小连接数、连接超时时间等。下面是一个创建连接池的示例代码:

const mysql = require('mysql2');

// 创建连接池
const pool = mysql.createPool({
  host: '127.0.0.1', // 数据库地址
  user: 'root', // 数据库用户名
  password: '123456', // 数据库密码
  database: 'db_test', // 数据库名称
  connectionLimit: 10,   // 最大连接数
  queueLimit: 0          // 请求队列长度,0 表示不限制
});

// 从连接池中获取一个连接
pool.getConnection((err, connection) => {
  if (err) {
    console.error('无法获取数据库连接:', err);
    return;
  }
  console.log('成功获取数据库连接');
  
  // 执行数据库操作
  connection.query('SELECT * FROM t_user', (err, results, fields) => {
    if (err) {
      console.error('执行查询失败:', err);
      return;
    }
    console.log('查询结果:', results);
    
    // 将连接返回到连接池中
    connection.release();
    console.log('成功返回数据库连接');
    
    // 关闭连接池
    pool.end((err) => {
      if (err) {
        console.error('关闭连接池失败:', err);
        return;
      }
      console.log('成功关闭连接池');
    });
  });
});

在上面的代码中,首先使用 mysql.createPool 方法创建了一个连接池对象 pool,并设置了最大连接数为 10。然后使用 pool.getConnection 方法从连接池中获取一个连接对象 connection,并通过 connection.query 方法执行了一条查询语句。在查询完成后,通过 connection.release 方法将连接对象返回到连接池中,并使用 pool.end 方法关闭连接池。

需要注意的是,使用连接池时,应该始终在使用完毕后,将连接对象返回到连接池中,以便其他请求可以重复利用该连接。另外,在使用连接池时,应该遵循最佳实践,例如避免在查询中使用未经过验证和转义的字符串,避免在事务中同时使用多个连接对象等。

使用连接池可以有效地提高应用程序的性能和可靠性,减少因创建和释放连接而带来的开销和风险。在实际应用中,应该根据具体的需求和负载情况,合理地配置连接池的参数,以获得最佳的性能和可靠性。

25.1.7 事物操作

以下是一个使用 mysql2 实现事务操作的示例代码:

const mysql = require('mysql2');

// 创建连接池
const pool = mysql.createPool({
  host: '127.0.0.1', // 数据库地址
  user: 'root', // 数据库用户名
  password: '123456', // 数据库密码
  database: 'db_test', // 数据库名称
  connectionLimit: 10
});

// 获取连接
pool.getConnection((err, connection) => {
  if (err) {
    console.error('无法获取数据库连接:', err);
    return;
  }
  console.log('成功获取数据库连接');

  // 开始事务
  connection.beginTransaction((err) => {
    if (err) {
      console.error('开启事务失败:', err);
      connection.release();
      return;
    }

    // 执行事务中的 SQL 命令
    connection.query('UPDATE t_user SET balance = balance - 100 WHERE id = 1', (err, result) => {
      if (err) {
        console.error('执行 SQL 失败:', err);
        connection.rollback(() => {
          console.log('回滚事务');
          connection.release(); // 释放连接
        });
        return;
      }
      console.log('更新用户1余额:', result.affectedRows);

       connection.query('UPDATE t_user SET balance = balance + 100 WHERE id = 2', (err, result) => {
      if (err) {
        console.error('执行 SQL 失败:', err);
        connection.rollback(() => {
          console.log('回滚事务');
          connection.release(); // 释放连接
        });
        return;
      }
      console.log('更新用户2余额:', result.affectedRows);

        // 提交事务
        connection.commit((err) => {
          if (err) {
            console.error('提交事务失败:', err);
            connection.rollback(() => {
              console.log('回滚事务');
              connection.release(); // 释放连接
            });
            return;
          }
          console.log('成功提交事务');
          connection.release(); // 释放连接
		  
		  // 关闭连接池
		  pool.end((err) => {
			if (err) {
			  console.error('关闭连接池失败:', err);
			  return;
			}
			console.log('成功关闭连接池');
		  });
        });
      });
    });
  });
});

在上述示例中,首先使用 mysql2 创建了一个连接池对象 pool,然后使用 pool.getConnection 方法获取了一个连接对象 connection。接着,通过调用 connection.beginTransaction 方法开始了一个事务,并在事务中执行了两条 SQL 命令:一条更新用户余额的命令和一条创建订单的命令。如果执行 SQL 出现错误,将会回滚事务并释放连接。如果两条 SQL 命令都执行成功,将通过调用 connection.commit 方法提交事务,并在提交事务后释放连接。

需要注意的是,在使用事务时,应该始终将相关操作放在事务中,并确保在事务结束后,通过调用 connection.commit 方法提交事务或者调用 connection.rollback 方法回滚事务。另外,在使用事务时,应该遵循最佳实践,例如避免在事务中同时使用多个连接对象等。

25.1.8 防止 SQL 注入

mysql2 本身并不能直接防止 SQL 注入攻击。然而,通过合理使用 mysql2 提供的功能和一些最佳实践,可以有效地防止 SQL 注入。

以下是一些方法来防止 SQL 注入攻击:

  1. 使用参数化查询:使用占位符或预处理语句来将用户输入作为参数传递给查询,而不是将其直接嵌入到 SQL 查询字符串中。这样可以确保用户输入不会被误解为 SQL 代码,从而减少了 SQL 注入的风险。
const mysql = require('mysql2/promise');
const connection = await mysql.createConnection({/* connection config */});

const query = 'SELECT * FROM users WHERE username = ? AND password = ?';
const [rows, fields] = await connection.execute(query, [username, password]);

在上面的示例中,? 是占位符,后面的数组 [username, password] 是实际的参数值。mysql2 会在执行查询时自动将这些参数转义,以避免注入攻击。

  1. 输入验证与过滤:在将用户输入用作查询参数之前,进行必要的验证和过滤。验证可以确保输入符合预期的格式和类型,而过滤可以去除敏感字符或特殊字符,以防止恶意注入。

  2. 使用存储过程或函数:将复杂的查询逻辑封装在数据库的存储过程或函数中,并通过调用这些过程或函数来执行查询。这样可以减少直接在应用程序中编写 SQL 查询的机会,从而降低注入攻击的风险。

  3. 最小权限原则:为数据库用户分配最小的权限,只给予其执行必要操作的权限。这样即使发生注入攻击,攻击者也只能获得有限的权限,减少了损害的范围。

为了确保应用程序的安全性,还应该定期更新 mysql2 和其他相关库,并遵循最新的安全建议。

25.2 MongoDB

MongoDB 是一个流行的开源 NoSQL 数据库,它以文档的形式存储数据。在 Node.js 中操作 MongoDB 数据库通常使用 mongodb 或 mongoose 模块,这两个模块都可以用于在 Node.js 中连接和操作 MongoDB 数据库。下面我们将详细介绍如何使用 MongoDB 数据库的操作。

25.2.1 安装MongoDB驱动

Node.js 提供了许多模块来连接和操作 MongoDB 数据库,其中最常用的是官方的 MongoDB 驱动程序模块。

安装依赖

# 安装 mongodb 依赖包
npm install mongodb

25.2.2 MongoClient

MongoClient 是 MongoDB 官方提供的用于连接和与 MongoDB 数据库进行交互的驱动程序。它是 MongoDB Node.js 驱动程序的核心组件之一。

MongoClient 类提供了一组方法,使您能够在 Node.js 环境中连接到 MongoDB 数据库,并执行各种数据库操作,例如插入、查询、更新和删除文档等。

下面是 MongoClient 类的一些常用属性和方法:

属性

  • db 属性是一个函数,用于选择指定的数据库并返回一个 Db 对象。您可以在 Db 对象上执行各种数据库操作。

  • topology:topology 属性是一个表示 MongoDB 连接拓扑结构的对象。它提供了一些有用的信息,例如连接状态、服务器列表、拓扑结构类型等。您可以使用 topology 属性来了解 MongoDB 客户端与 MongoDB 服务器之间的连接情况。

方法

  • connect():连接到 MongoDB 数据库并返回一个 Promise 对象。

  • close(force?: boolean, callback?: function):关闭数据库连接。您可以选择是否强制关闭连接(即忽略未完成的操作)。如果不提供回调函数,则返回一个 Promise 对象。

  • db(dbName: string, options?: object):选择指定的数据库。这将返回一个 Db 对象,它表示对特定数据库的操作。您可以在 Db 对象上执行各种数据库操作。

    options参数说明:

    • authSource: 一个字符串,表示用于进行身份验证的数据库名称。如果 MongoDB 使用了身份验证,并且要连接的数据库与身份验证数据库不同,则需要设置此选项。

    • authMechanism: 一个字符串,指定要使用的身份验证机制。常用的机制包括 MONGODB-CR、SCRAM-SHA-1 和 SCRAM-SHA-256 等。默认情况下,驱动程序会自动选择适当的机制。

    • authMechanismProperties: 一个对象,包含要传递给身份验证机制的属性。这个选项通常与 authMechanism 一起使用。例如:{ SERVICE_NAME: ‘mongodb’, CANONICALIZE_HOST_NAME: true }。

    • readConcern: 一个对象,表示读取的一致性保证级别。常用的级别包括 local、majority 和 linearizable 等。

    • readPreference: 一个字符串或对象,表示读取偏好。常用的偏好类型包括 primary、secondary 和 nearest 等。

    • pkFactory: 一个对象,用于自定义主键生成算法。这个选项通常用于自定义主键字段的名称和值生成方法。

    • retryWrites:指定在写操作失败时是否自动重试,默认true。

    client.db(dbName) 返回的是一个 MongoDB 数据库对象(db)。这个对象代表了与指定数据库名称相对应的数据库。

    使用数据库对象后,您可以执行各种数据库操作,如创建集合、插入数据、查询数据等。

    以下是一些常用的数据库操作方法:

    • db.collection(collectionName):获取指定集合名称的集合对象。通过集合对象可以执行与该集合相关的操作,如插入文档、更新文档、删除文档等。
    • db.dropDatabase():删除当前数据库。谨慎使用此方法,因为删除数据库将会同时删除该数据库中的所有集合和数据。
    • db.listCollections():获取当前数据库中的所有集合列表。
    • db.createCollection(collectionName, options):创建一个新的集合。collectionName 参数指定要创建的集合的名称,options 是一个可选的配置对象,用于指定集合的创建选项,例如指定索引、存储引擎等。
  • startSession(options?: object):启动一个会话对象。会话对象可用于执行事务或使用事务相关的操作。

    options参数说明:

    • causalConsistency: 一个布尔值,表示会话是否支持因果一致性。默认情况下,会话是支持因果一致性的。启用因果一致性可以确保操作按照它们在应用程序中执行的顺序进行提交。

    • defaultTransactionOptions: 一个对象,表示会话的默认事务选项。事务选项用于指定事务的隔离级别、超时时间等参数。默认情况下,会话的默认事务选项为空对象。

    • snapshot: 一个布尔值,表示在会话中的读操作是否使用快照隔离。如果设置为 true,则读操作将在事务开始时获取快照,并在整个事务期间保持一致性。默认情况下,会话的读操作不使用快照隔离。

    • readConcern: 一个对象,表示会话的读取一致性级别。常用的级别包括 local、majority 和 linearizable 等。默认情况下,会话的读取一致性级别与数据库的默认读取一致性级别相同。

    • writeConcern: 一个对象,表示会话的写入一致性级别。常用的级别包括 majority、j 和 wtimeout 等。默认情况下,会话的写入一致性级别与数据库的默认写入一致性级别相同。

    • defaultTransactionOptions: 一个对象,表示会话的默认事务选项。事务选项用于指定事务的隔离级别、超时时间等参数。默认情况下,会话的默认事务选项为空对象。

  • withSession(callback: function):在一个新会话中执行给定的回调函数。此方法简化了使用会话对象的流程。

  • watch():方法用于实时监视 MongoDB 中的变化。它可以监听集合的插入、更新和删除操作,并在发生这些操作时触发回调函数。

其他方法:listDatabases()、listCollections() 等。

连接MongoDB数据库

new MongoClient(uri, options) 是使用 Node.js 客户端连接 MongoDB 数据库时创建 MongoClient 实例的方式之一。MongoClient 是 MongoDB 官方提供的用于连接和与 MongoDB 数据库进行交互的驱动程序。

options参数说明:

  • appname: 一个字符串,表示应用程序名称。在 MongoDB 4.4 及以上版本中引入,可以通过查看 system.sessions 集合来了解哪些应用程序正在访问数据库。

  • auth: 一个对象,包含要使用的身份验证凭证。常用的凭证类型包括 userpassword。例如:{ user: 'john', password: 'secret' }

  • authMechanism: 一个字符串,指定要使用的身份验证机制。常用的机制包括 MONGODB-CR、SCRAM-SHA-1 和 SCRAM-SHA-256 等。默认情况下,驱动程序会自动选择适当的机制。

  • authMechanismProperties: 一个对象,包含要传递给身份验证机制的属性。这个选项通常与 authMechanism 一起使用。例如:{ SERVICE_NAME: 'mongodb', CANONICALIZE_HOST_NAME: true }

  • authSource: 一个字符串,表示用于进行身份验证的数据库名称。如果 MongoDB 使用了身份验证,并且要连接的数据库与身份验证数据库不同,则需要设置此选项。

  • compressors: 一个字符串数组,表示要使用的压缩算法。可用的算法包括 snappy、zlib 和 zstd 等。默认情况下,驱动程序会自动选择适当的算法。

  • connectTimeoutMS: 一个整数,表示连接超时时间(毫秒)。如果在指定的时间内无法连接到 MongoDB 服务器,则会引发异常。

  • directConnection: 一个布尔值,表示是否直接连接 MongoDB 服务器。如果设置为 false,则使用 MongoDB 路由器进行连接。

  • family: 一个整数,表示要使用的 IP 地址族。可选值包括 4 和 6,分别表示 IPv4 和 IPv6。如果未指定,则自动检测。

  • ha: 一个布尔值,表示是否启用高可用性(HA)模式。如果设置为 true,则驱动程序会自动重试连接多个副本集成员或多个分片节点。

  • ignoreUndefined: 一个布尔值,表示是否忽略未定义的属性。如果设置为 true,则在插入文档时不会包含未定义的属性。

  • maxPoolSize:一个数字值,表示连接池中的最大连接数。默认情况下,其值为 100。当达到最大连接数时,新的连接请求将等待直到有可用的连接。增加 maxPoolSize 可以增加并发连接的能力,但也会增加系统资源的使用。

  • minPoolSize:一个数字值,表示连接池中的最小连接数。默认情况下,其值为 0。连接池会在启动时创建 minPoolSize 个连接,并保持这个数量的空闲连接。如果连接池中没有足够的空闲连接,则会按需创建新的连接,直到达到 maxPoolSize。

  • maxStalenessSeconds: 一个整数,表示最大允许的数据延迟时间(秒)。如果指定了此选项,则在查询时会优先选择数据比本地更新的节点。这个选项通常与读取偏好一起使用。

  • readConcern: 一个对象,表示读取的一致性保证级别。常用的级别包括 local、majority 和 linearizable 等。

  • readPreference: 一个字符串或对象,表示读取偏好。常用的偏好类型包括 primary、secondary 和 nearest 等。

  • replicaSet: 一个字符串,表示要连接的副本集名称。如果要连接的是分片集群,则不需要设置此选项。

  • serverSelectionTimeoutMS: 一个整数,表示选择服务器的超时时间(毫秒)。如果在指定的时间内无法选择服务器,则会引发异常。

  • socketTimeoutMS: 一个整数,表示套接字超时时间(毫秒)。如果在指定的时间内没有收到响应,则会引发异常。

  • ssl: 一个布尔值,表示是否使用 SSL/TLS 加密连接到 MongoDB 服务器。

  • tls: 一个布尔值或对象,表示是否使用 TLS 加密连接到 MongoDB 服务器。如果设置为 true,则使用默认的 TLS 设置。如果是一个对象,则可以指定自定义的 TLS 设置。

  • tlsAllowInvalidCertificates: 如果设置为 true,则在建立 TLS 连接时不会验证服务器证书。默认情况下,驱动程序会验证服务器证书,并在证书无效时引发异常。

  • tlsCAFile: 一个字符串,表示要使用的 CA 文件的路径。这个选项通常用于指定自签名证书的 CA。如果未指定,则使用操作系统的默认 CA。

  • tlsCertificateKeyFile: 一个字符串,表示要使用的客户端证书和私钥文件的路径。如果要使用 X.509 证书进行身份验证,则需要设置此选项。

  • tlsCertificateKeyFilePassword: 一个字符串,表示客户端证书和私钥文件的密码。如果证书和私钥文件加密了,则需要设置此选项。

  • w: 一个整数或字符串,表示写入的一致性保证级别。常用的级别包括 majority、j 和 wtimeout 等。

  • waitQueueTimeoutMS: 一个整数,表示等待队列的超时时间(毫秒)。如果等待队列已满,并且在指定的时间内没有可用连接,则会引发异常。

使用 MongoClient 类连接 MongoDB 的基本流程如下:

  1. 创建一个 MongoClient 实例,并传递 MongoDB 连接字符串和选项(可选)。

  2. 调用 connect() 方法来连接到 MongoDB 数据库。在成功连接后,您可以选择指定的数据库。

  3. 在所选数据库上执行各种数据库操作,例如插入、查询、更新和删除文档等。

  4. 使用 close() 方法关闭数据库连接。

下面是使用 new MongoClient 创建 MongoClient 实例并连接到 MongoDB 数据库的示例代码:

const { MongoClient } = require('mongodb');

const uri = 'mongodb://crm:123456@127.0.0.1:27017/db_crm?authSource=db_crm';

const client = new MongoClient(uri);

async function connectToMongo() {
  try {
    await client.connect();
    console.log('Connected to MongoDB');
    
    const db = client.db('db_crm');
	// 继续执行其他操作...
    
    // 关闭连接
    await client.close();
  } catch (err) {
    console.error('Failed to connect to MongoDB', err);
  }
}

connectToMongo();

在上述代码中,我们首先创建了一个 MongoClient 实例,将 MongoDB 连接 URL 和选项传递给构造函数。然后使用 connect 方法来连接到 MongoDB 数据库。在成功连接后,可以选择指定的数据库,并执行数据库操作。最后,使用 close 方法关闭数据库连接。

uri解析:

uri中的crm是用户名;:123456的密码是123456;@127.0.0.1:27017指定mongodb数据库服务器的地址;/db_crm就是数据库;authSource表示这个数据库将用于验证用户的凭证。

注意:authSource 是 MongoDB 连接选项中的一个参数,用于指定身份验证数据库。当使用 MongoDB 的身份验证功能时,需要在连接时指定身份验证的数据库。默认情况下,MongoDB 客户端会在 admin 数据库上进行身份验证。 通常情况下 authSource 默认为admin数据库。MongoDB 会将用户凭证存储在 admin 数据库中,如果您的用户凭证是在其他数据库,那么就必须明确指定 authSource

25.2.3 连接池

在使用 MongoClient 连接 MongoDB 数据库时,可以使用连接池来管理和重用数据库连接,以提高性能和效率。连接池可以在应用程序启动时创建一组初始连接,并在需要时将连接分配给请求,完成后将连接返回到连接池中。

以下是使用连接池的示例代码:

const { MongoClient } = require('mongodb');

const uri = 'mongodb://crm:123456@127.0.0.1:27017/db_crm?authSource=db_crm';
const client = new MongoClient(uri);
const options = {
  maxPoolSize: 50,   // 设置最大连接数为 50
  minPoolSize: 10    // 设置最小连接数为 10
};

const client = new MongoClient(uri, options);

async function connectToDatabase() {
  try {
    await client.connect();
    console.log('Connected to the database!');
  } catch (err) {
    console.error('Failed to connect to the database:', err);
  }
}

connectToDatabase();

在这个示例中,我们使用 MongoClient 创建了一个连接池,并设置了 maxPoolSizeminPoolSize 参数。然后我们通过调用 MongoClient.connect() 方法来获取一个 MongoClient 实例。

请注意,我们在 finally 代码块中使用 client.close() 方法关闭了 MongoClient 实例。这非常重要,因为它会释放连接池中的连接,以便其他客户端可以使用它们。

25.2.4 插入数据

需要通过 MongoClient.connect() 方法来连接到 MongoDB 服务器,并获取一个数据库实例。然后,使用数据库实例的 collection() 方法获取要插入数据的集合。

下面我们将详细介绍一些常用的插入数据的方法。

插入单个文档

如果您想插入单个文档(也就是一条记录),可以使用集合的 insertOne() 方法。这个方法接受一个文档对象作为参数,并返回一个 Promise,当 Promise 被解析时,表示插入操作成功。

const { MongoClient } = require('mongodb');

const uri = 'mongodb://crm:123456@127.0.0.1:27017/db_crm?authSource=db_crm';
const client = new MongoClient(uri);

async function insertDocument() {
  try {
    await client.connect();
    console.log('Connected to MongoDB');
	
	const db = client.db('db_crm');
    const collection = db.collection('student');
    const document = { name: 'John', age: 30 };
    const result = await collection.insertOne(document);
    console.log('Inserted document with _id:', result.insertedId);
  } catch (e) {
    console.error(e);
  } finally {
    await client.close();
  }
}

insertDocument();

在这个示例中,我们首先创建一个 MongoClient 实例,并调用 connect() 方法连接到 MongoDB 服务器。然后,我们使用 collection() 方法获取名为 mycollection 的集合。接下来,我们定义一个要插入的文档对象,并使用 insertOne() 方法将文档插入到集合中。最后,我们打印插入文档的 _id 值。

插入成功后返回id:

ydcq@ydcqdeMac-mini ~ % node /Users/ydcq/Desktop/web/node_demo/demo.js 
Connected to MongoDB
Inserted document with _id: new ObjectId('658bf88696586ce3528da6d4')
ydcq@ydcqdeMac-mini ~ % 

插入多个文档

如果您想一次性插入多个文档,可以使用集合的 insertMany() 方法。这个方法接受一个文档数组作为参数,并返回一个 Promise,当 Promise 被解析时,表示插入操作成功。

以下是一个示例:

const { MongoClient } = require('mongodb');

const uri = 'mongodb://crm:123456@127.0.0.1:27017/db_crm?authSource=db_crm';
const client = new MongoClient(uri);

async function insertDocuments() {
  try {
    await client.connect();
    console.log('Connected to MongoDB');
	
	const db = client.db('db_crm');
    const collection = db.collection('student');
    const documents = [
      { name: 'John', age: 30 },
      { name: 'Alice', age: 25 },
      { name: 'Bob', age: 35 }
    ];
    const result = await collection.insertMany(documents);
    console.log('Inserted', result.insertedCount, 'documents');
  } catch (e) {
    console.error(e);
  } finally {
    await client.close();
  }
}

insertDocuments();

在这个示例中,我们使用 insertMany() 方法将多个文档一次性插入到集合中。我们定义了一个包含多个文档的数组,并将其作为参数传递给 insertMany() 方法。最后,我们打印插入的文档数量。

成功后返回插入的数据个数:

ydcq@ydcqdeMac-mini ~ % node /Users/ydcq/Desktop/web/node_demo/demo.js
Connected to MongoDB
Inserted 3 documents
ydcq@ydcqdeMac-mini ~ % 

除了上述的 insertOne()insertMany() 方法之外,MongoDB 还提供了其他一些插入数据的方法,例如 insert()。这些方法具有相似的用法,但可能在细节上有一些差异。根据你的需求选择适合的方法即可。

需要注意的是,插入操作是原子的,要么全部成功,要么全部失败。如果插入过程中出现错误,整个操作将会回滚,并且不会插入部分数据。

25.2.5 查询

MongoClient 是 MongoDB 官方提供的 Node.js 驱动程序,用于与 MongoDB 数据库进行交互。通过 MongoClient 对象,您可以执行各种数据库操作,包括查询数据。

以下是一些常见的查询方法:

findOne()

如果您想查询集合中的单个文档(即一条记录),可以使用集合的 findOne(query, projection) 方法。它接受两个参数:query 是一个可选的查询条件对象,用于指定要查询的文档,projection 是一个可选的投影对象,用于指定要返回的字段,并返回一个 Promise,当 Promise 被解析时,表示查询操作成功。

以下是一个示例:

const { MongoClient } = require('mongodb');

const uri = 'mongodb://crm:123456@127.0.0.1:27017/db_crm?authSource=db_crm';
const client = new MongoClient(uri);

async function findOneDocument() {
  try {
    await client.connect();
    console.log('Connected to MongoDB');
	
	const db = client.db('db_crm');
    const collection = db.collection('student');
    const query = { name: 'John' };
    const document = await collection.findOne(query);
    console.log('Document=', document);
  } catch (e) {
    console.error(e);
  } finally {
    await client.close();
  }
}

findOneDocument();

在这个示例中,我们首先创建一个 MongoClient 实例,并调用 connect() 方法连接到 MongoDB 服务器。然后,我们使用 collection() 方法获取名为 mycollection 的集合。接下来,我们定义一个查询条件对象,并使用 findOne() 方法查找符合条件的文档。最后,我们打印查询到的文档对象。

find()

如果您想查询集合中的多个文档,可以使用集合的 find(query, projection) 方法。它接受两个参数:query 是一个可选的查询条件对象,用于指定要查询的文档,projection 是一个可选的投影对象,用于指定要返回的字段,并返回一个游标对象(cursor)。通过游标对象,您可以逐个遍历查询结果集中的文档。

以下是一个示例:

const { MongoClient } = require('mongodb');

const uri = 'mongodb://crm:123456@127.0.0.1:27017/db_crm?authSource=db_crm';
const client = new