Skip to content

Node.js - 快速指南

Node.js 是一个开源、跨平台的 JavaScript 运行时环境,构建于 Google Chrome 的 V8 JavaScript 引擎之上。它允许开发者在服务器端执行 JavaScript 代码,从而能够创建后端服务、API 和其他网络应用。你可以在 Node.js 官方网站上找到最新的发布版本和长期支持 (LTS) 版本。

Node.js® 是一个基于 Chrome V8 JavaScript 引擎构建的 JavaScript 运行时。Node.js 使用事件驱动、非阻塞 I/O 模型,使其轻量且高效。Node.js 的包生态系统 npm 是世界上最大的开源库生态系统。

本质上,Node.js 提供了一个运行时环境,允许 JavaScript 在 Web 浏览器之外执行,主要用于服务器端开发。这意味着你可以使用 JavaScript 进行前端和后端开发,从而带来更统一的开发体验。

Node.js 包含一组丰富的内置模块(如 http、fs、path)以及通过其包管理器 npm (Node Package Manager) 提供的庞大第三方包生态系统,这极大地简化了 Web 应用程序开发。

Node.js = V8 引擎 + Libuv + 核心模块 + npm

  • 异步和事件驱动: Node.js 中的大多数 API 都是异步的(非阻塞)。Node.js 服务器不会等待 API 调用返回。相反,它会继续执行下一个操作,并使用事件通知系统(事件循环 Event Loop)在之前的调用准备就绪时处理结果。这使得 Node.js 能够高效地处理许多并发连接。
  • 速度非常快: 构建于 Chrome 的 V8 JavaScript 引擎之上,Node.js 将 JavaScript 直接编译成原生机器码,从而实现快速执行。
  • 单线程但高度可伸缩: Node.js 使用单线程事件循环模型。虽然它在单个主线程中运行代码,但其非阻塞 I/O 操作使其能够高效地处理数千个并发连接,而无需像传统服务器(例如 Apache)那样管理多个线程的开销。对于 CPU 密集型任务,可以使用 Worker Threads。
  • 无缓冲(主要是流式处理): Node.js 应用程序有效地使用 Streams 处理数据,通常分块处理数据而无需将大量数据缓冲到内存中,这使其非常适合数据密集型应用程序。
  • 跨平台: Node.js 可以在 Windows、macOS 和 Linux 等各种平台上运行。
  • 庞大的包生态系统 (npm): npm 是 Node.js 的默认包管理器,提供对数十万个可重用代码包的访问,简化了开发。
  • 许可: Node.js 在 MIT 许可下发布。

Node.js 被各种规模的公司广泛采用,从初创企业到大型企业。知名用户包括 Netflix、Uber、PayPal、LinkedIn、eBay、NASA 等等。它用于构建 API、微服务、实时应用程序(如聊天应用和在线游戏)、命令行工具等。

你可以在网上找到使用 Node.js 的公司和项目列表,这些列表通常由社区或 Node.js 基金会维护。

理解以下概念对于有效使用 Node.js 至关重要:Event Loop(处理异步操作)、Modules(CommonJS 和 ES Modules 用于组织代码)、npm(包管理)、Asynchronous Programming(Callbacks, Promises, async/await)、Streams(高效数据处理)和 Buffers(处理二进制数据)。我们将在后续章节中探讨这些内容。

Node.js 在以下场景中表现出色:

  • I/O 密集型应用: 涉及频繁网络请求或数据库操作的应用(例如,API、Web 服务器)。
  • 实时应用: 聊天应用、在线游戏服务、实时协作工具(利用 WebSockets)。
  • 数据流应用: 视频和音频流媒体平台。
  • 微服务: 构建通过网络通信的小型独立服务。
  • 单页应用 (SPAs): 为现代前端框架提供后端 API。
  • 工具和命令行接口 (CLIs): 创建构建工具、脚本和开发实用程序。

虽然用途广泛,但 Node.js 可能不是处理繁重 CPU 密集型任务(如复杂计算、图像处理)的最佳选择,除非利用 Worker Threads,因为这些任务会阻塞单个主线程。对于此类任务,具有强大多线程能力的语言可能更适合。

虽然本指南侧重于本地搭建,但许多在线平台允许你直接在浏览器中尝试 Node.js,无需任何安装。像 Replit、CodeSandbox 或 Node.js 官方网站的 REPL 部分等平台提供了方便的方式来尝试代码片段。

例如,你可以运行这样的简单 JavaScript 代码:

console.log("Hello from Node.js!");

使用在线环境非常适合快速测试和学习基本语法。

对于严肃的开发,搭建本地环境至关重要。你需要:

  1. Node.js 和 npm: 从官方网站 https://nodejs.org/ 下载并安装 Node.js。安装程序包含 npm (Node Package Manager)。强烈建议安装 LTS(长期支持)版本以获得稳定性。
  2. 版本管理器(可选但推荐): 像 macOS/Linux 上的 nvm (Node Version Manager) 或 Windows 上的 nvm-windows 等工具允许你轻松安装和切换多个 Node.js 版本。这在处理不同 Node.js 要求的项目时非常有用。
  3. 文本编辑器/IDE: 一个好的代码编辑器可以提高工作效率。流行的选择包括 Visual Studio Code (VS Code)、WebStorm、Sublime Text 或 Atom。VS Code 是一个免费、强大且广泛使用的选项,具有出色的 JavaScript 和 Node.js 支持。

你将使用文本编辑器编写 Node.js 代码。现代编辑器提供语法高亮、代码补全、调试集成和终端访问等功能,使开发更加顺畅。强烈推荐 VS Code,因为它功能丰富且社区支持良好。

Node.js 源文件通常具有 .js 扩展名(用于 CommonJS 或基本脚本)或 .mjs(用于 ES Modules)。

你的搭建核心是 Node.js 运行时本身。它负责解释和执行你的 JavaScript 代码。从 https://nodejs.org/en/download/ 下载适用于你操作系统的安装程序(Windows、macOS、Linux)。

  • Windows: 下载 .msi 安装程序并按照安装向导进行。它通常会安装 Node.js 和 npm,并将它们添加到系统的 PATH 环境变量中。
  • macOS: 下载 .pkg 安装程序并按照说明进行。另外,如果你使用 Homebrew,可以使用 brew install node 来安装 Node.js。
  • Linux: 你可以使用你的发行版包管理器(如 Debian/Ubuntu 的 apt 或 Fedora 的 yum),或者从 Node.js 网站下载预构建的二进制文件。在 Linux 和 macOS 上,使用 nvm 通常是更推荐的方法:curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.x/install.sh | bash(请检查 nvm 仓库以获取最新的安装脚本),然后 nvm install --lts。

安装完成后,打开你的终端或命令提示符,输入以下命令检查安装的版本:

# 检查 Node.js 版本
node -v
# 检查 npm 版本
npm -v

你应该会看到版本号打印出来,确认安装成功。现在,让我们运行一个简单的文件:

在你的项目目录下创建一个名为 hello.js 的文件:

hello.js
console.log("Hello, Node.js World!");

在终端中使用 node 命令运行该文件:

node hello.js

验证输出:

Hello, Node.js World!

让我们使用 Node.js 内置的 http 模块构建一个非常简单的 Web 服务器。典型的 Node.js 应用程序涉及以下步骤:

  • 导入所需的模块: 使用 require 关键字(用于 CommonJS 模块,Node.js 的传统方式)或 import(用于 ES Modules,现代标准)加载内置或第三方模块。
  • 创建服务器: 实例化一个服务器对象,它将监听传入的客户端请求(例如,来自 Web 浏览器)。
  • 处理请求并发送响应: 定义服务器处理传入 HTTP 请求并发送回相应 HTTP 响应的逻辑。

步骤 1:导入 http 模块

我们需要内置的 http 模块来创建一个 HTTP 服务器。

// 对于不会重新赋值的变量使用 const
const http = require('http');

步骤 2:创建服务器

使用 http.createServer() 方法。此方法接受一个回调函数(请求监听器),该函数针对每个传入请求执行。该函数接收两个参数:request(包含请求详细信息的对象)和 response(用于向客户端发送数据的对象)。

const server = http.createServer((request, response) => {
// 设置响应 HTTP 头部,包含 HTTP 状态码和内容类型
response.writeHead(200, {'Content-Type': 'text/plain'});
// 发送响应体
response.end('Hello World from Node.js Server!\n');
});

步骤 3:启动服务器并监听端口

告诉服务器监听特定端口上的连接(例如 3000)。我们还会添加一个日志消息,以便知道服务器何时运行。

const port = 3000;
const host = '127.0.0.1'; // localhost
server.listen(port, host, () => {
console.log(`Server running at http://${host}:${port}/`);
});

步骤 4:组合并保存代码

将完整的代码保存到一个名为 server.js 的文件中。

// File: server.js
const http = require('http');
const host = '127.0.0.1'; // localhost
const port = 3000;
const server = http.createServer((request, response) => {
// 设置响应 HTTP 头部,包含 HTTP 状态码和内容类型
response.writeHead(200, {'Content-Type': 'text/plain'});
// 发送响应体
response.end('Hello World from Node.js Server!\n');
});
server.listen(port, host, () => {
console.log(`Server running at http://${host}:${port}/`);
});

步骤 5:运行应用

打开你的终端,导航到你保存 server.js 的目录,然后运行:

node server.js

在终端中验证输出:

Server running at http://127.0.0.1:3000/

打开你的 Web 浏览器并导航到 http://127.0.0.1:3000/(或 http://localhost:3000/)。你应该会看到文本:

Hello World from Node.js Server!

这确认你的基本 Node.js 服务器正在运行!

REPL 代表 Read-Eval-Print Loop(读取-求值-打印-循环)。它是一个交互式编程环境,就像一个 shell 或控制台,你可以在其中输入命令,对其进行求值,查看打印的结果,然后重复此过程。Node.js 自带了一个内置的 REPL。

它执行以下任务:

  • Read (读取): 读取用户输入(JavaScript 代码)。
  • Eval (求值): 对输入的代码进行求值。
  • Print (打印): 打印求值结果。
  • Loop (循环): 等待下一次输入。

Node.js REPL 对于快速试验 Node.js 特性、测试小型代码片段以及交互式调试 JavaScript 代码非常有用。

你只需在终端或命令提示符中输入 node,不带文件名参数即可启动 REPL:

node

你会看到 REPL 提示符 (>):

Welcome to Node.js vXX.X.X.
Type ".help" for more information.
>

你可以输入任何有效的 JavaScript 表达式:

> 1 + 3
4
> 5 * (10 - 2)
40
> 'Hello' + ' ' + 'REPL'
'Hello REPL'

你可以使用 var、let 或 const 声明变量。如果你在没有 var/let/const 的情况下赋值,它会隐式地成为一个全局变量(在 REPL 上下文中),并且会打印该值。使用 var/let/const 不会立即打印值(它打印 undefined)。

> x = 10 // 隐式全局变量
10
> let y = 20
undefined
> const z = 5
undefined
> x + y - z
25
> console.log('Value of y:', y)
Value of y: 20
undefined

REPL 支持多行表达式。当你开始一个代码块(例如使用 {、( 或 [)时,提示符会变为 ...,直到代码块关闭。

> let count = 0
undefined
> while (count < 3) {
... console.log(`Count is: ${count}`);
... count++;
... }
Count is: 0
Count is: 1
Count is: 2
undefined

特殊变量 _ 保存上一个求值表达式的结果。

> 5 * 8
40
> _ + 10
50
> const lastResult = _
undefined
> console.log(lastResult)
50
undefined

REPL 包含以点号 (.) 开头的特殊命令:

  • .help:显示此命令列表。
  • .exit:退出 REPL(与 Ctrl+D 相同)。
  • .break:中止多行输入。有时在你卡住时很有用。
  • .clear:将 REPL 上下文重置为空对象,并清除任何多行表达式。
  • .save <filename>:将当前 REPL 会话历史记录保存到文件。
  • .load <filename>:从文件加载代码到当前 REPL 会话。
  • .editor:进入编辑器模式,以便更容易进行多行编辑。
  • Ctrl + C:终止当前命令(按一次)或退出 REPL(按两次)。
  • Ctrl + D:退出 REPL。
  • 上/下箭头键:在命令历史记录中导航。
  • Tab 键:自动补全变量、关键字和对象属性。
> let message = 'Saving this';
undefined
> console.log(message);
Saving this
undefined
> .save my_session.js
Session saved to: my_session.js
> .clear
Clearing context...
> console.log(message);
Uncaught ReferenceError: message is not defined
> .load my_session.js
let message = 'Saving this';
console.log(message);
Saving this
undefined
> message
'Saving this'

npm 代表 Node Package Manager(Node 包管理器)。它是世界上最大的软件注册表,也是 Node.js 的默认包管理器。当你安装 Node.js 时,npm 会自动包含在内。

npm 主要有两个用途:

  • 在线注册表: 一个托管着数十万个免费、可重用 Node.js 包(也称为模块或库)的在线仓库 (https://www.npmjs.com/)。
  • 命令行工具 (CLI): 一个在终端中用于安装、更新、管理和发布项目 Node.js 包的工具。

你可以通过输入以下命令验证 npm 安装:

npm --version

此命令将显示已安装的 npm 版本。

每个 Node.js 项目在其根目录下都应该有一个 package.json 文件。此文件包含有关项目的元数据,包括:

  • 项目名称、版本、描述、作者、许可证。
  • 用于自动化任务的脚本(如启动应用、运行测试)。
  • Dependencies (依赖): 应用程序运行所需的包。
  • DevDependencies (开发依赖): 仅在开发过程中需要的包(如测试框架、构建工具)。

你可以在项目目录中运行 npm init 并按照提示创建 package.json 文件,或使用 npm init -y 接受默认设置。

当你使用 npm 安装包时,它们会被下载并放置在项目目录内的 node_modules 文件夹中。package.json 文件列出了这些依赖项,并且会自动生成一个 package-lock.json 文件,以锁定每个安装的依赖项的具体版本,确保在不同环境中安装的一致性。

本地安装(默认): 本地安装的包放置在当前项目的 node_modules 文件夹中,并列在 package.json 中。这是管理项目特定依赖项的标准方式。你在代码中使用 require() 或 import 来使用它们。

# 本地安装 'lodash' 并将其添加到 package.json 中的 dependencies
npm install lodash
# 或者显式地保存为依赖项(较旧的 npm 版本需要 --save)
npm install lodash --save
# 安装一个开发依赖项(例如,一个测试工具)
npm install jest --save-dev
# 或者使用简写方式
npm install -D jest

全局安装: 全局安装的包通常是命令行工具,你想在系统的任何地方使用它们。它们存储在系统范围内的位置(位置因操作系统而异)。使用 -g 标志。

# 全局安装 'nodemon'(一个在文件更改时重启应用的工具)
npm install -g nodemon

通常,项目依赖项优先选择本地安装,只有 CLI 工具才选择全局安装。

  • npm init [-y]:初始化一个新项目,创建 package.json 文件。
  • npm install <package_name>:本地安装一个包并将其添加到 dependencies 中。
  • npm install <package_name> -D 或 npm install <package_name> --save-dev:本地安装一个包并将其添加到 devDependencies 中。
  • npm install <package_name> -g:全局安装一个包。
  • npm install:安装 package.json 中列出的所有依赖项(根据 package-lock.json 读取特定版本)。在克隆项目后使用此命令。
  • npm ci:代表 ‘clean install’(干净安装)。它会删除 node_modules 并严格按照 package-lock.json 中指定的版本安装依赖项。对于自动化构建/CI 环境更快更可靠。
  • npm update [<package_name>]:将包更新到 package.json 中版本范围允许的最新版本,并更新 package-lock.json。
  • npm uninstall <package_name>:本地移除一个包并从 package.json 中删除。
  • npm uninstall <package_name> -g:移除一个全局安装的包。
  • npm list:列出已安装的本地包。
  • npm list -g --depth=0:列出全局安装的包(仅顶层)。
  • npm search <keyword>:在 npm 注册表中搜索包。
  • npm run <script_name>:执行 package.json 中 scripts 部分定义的脚本。
  • npx <command>:执行一个包提供的命令,而无需全局或本地安装该包。对于按需运行 CLI 工具很有用(例如,npx create-react-app my-app)。
  1. 在终端中导航到你的项目目录。

  2. 初始化项目(如果尚未初始化):

npm init -y
  1. 安装一个包,例如 lodash(一个流行的实用工具库):
npm install lodash

这将创建 node_modules(如果不存在),下载 lodash 及其依赖项到其中,并将 lodash 添加到 package.json 的 dependencies 部分。

  1. 在你的 Node.js 文件中使用该包(例如 app.js):
app.js
const _ = require('lodash');
const numbers = [1, 5, 8, 10, 1, 5];
const uniqueNumbers = _.uniq(numbers);
console.log('Original numbers:', numbers);
console.log('Unique numbers:', uniqueNumbers);
  1. 运行你的应用:
node app.js
输出:
Original numbers: [ 1, 5, 8, 10, 1, 5 ]
Unique numbers: [ 1, 5, 8, 10 ]

这是一个 package.json 文件的极简示例:

{
"name": "my-node-app",
"version": "1.0.0",
"description": "A simple Node.js application",
"main": "server.js", // 应用程序的主要入口文件
"scripts": {
"start": "node server.js", // 使用 'npm start' 运行
"test": "echo \"Error: no test specified\" && exit 1" // 使用 'npm test' 运行
},
"keywords": [
"node",
"example",
"server"
],
"author": "Your Name <your.email@example.com>",
"license": "ISC", // 或 'MIT' 等
"dependencies": {
"lodash": "^4.17.21" // 示例依赖,'^' 允许小版本和补丁更新
},
"devDependencies": {
"nodemon": "^2.0.15" // 示例开发依赖
}
}

name、version、main、scripts、dependencies 和 devDependencies 等关键字段对于有效管理你的项目及其依赖关系至关重要。

Node.js - 异步编程:Callbacks, Promises, Async/Await

Section titled “Node.js - 异步编程:Callbacks, Promises, Async/Await”

Node.js 最显著的特征之一是其异步、非阻塞的特性。这意味着像读取文件、发起网络请求或查询数据库等操作不会阻塞程序的执行。相反,Node.js 使用各种机制在后台处理这些操作,并在它们完成时通知你的代码。

从历史上看,Callbacks(回调函数)是处理异步操作的主要方式。虽然它是基础,但它们可能导致复杂的嵌套结构,称为“Callback Hell”(回调地狱)。现代 JavaScript 引入了 Promises 和 async/await 语法,它们提供了更清晰、更易于管理的方式来处理异步代码。

Callback 简单来说就是一个函数作为参数传递给另一个函数,然后在异步操作完成时被调用。在 Node.js 中,Callback 通常遵循错误优先约定(error-first convention):Callback 函数的第一个参数保留用于错误对象(如果没有错误则为 null),后续参数包含结果数据。

同步函数会阻塞执行,直到它们完成。readFileSync 就是一个例子。

// 创建一个名为 input.txt 的文件,包含一些文本。
// 例如:“Node.js is asynchronous!”
const fs = require('fs');
try {
// 这会阻塞执行直到文件被读取
const data = fs.readFileSync('input.txt', 'utf8');
console.log("File content (sync):", data);
} catch (err) {
console.error("Error reading file sync:", err);
}
console.log("Program Ended (after sync read)");
输出(假设 input.txt 存在):
File content (sync): Node.js is asynchronous!
Program Ended (after sync read)

非阻塞(异步)代码示例(使用 Callback):

Section titled “非阻塞(异步)代码示例(使用 Callback):”

异步函数如 readFile 不会阻塞。它们接受一个 Callback 函数,在操作完成后执行。

const fs = require('fs');
// 这行代码开始读取文件,但不会阻塞
fs.readFile('input.txt', 'utf8', (err, data) => {
// 这个 Callback 函数会在稍后执行,当读取完成或发生错误时
if (err) {
console.error("Error reading file async:", err);
return;
}
console.log("File content (async):", data);
});
// 这行代码几乎立即执行,可能在文件读取完成之前
console.log("Program Ended (likely before async read completes)");
典型输出:
Program Ended (likely before async read completes)
File content (async): Node.js is asynchronous!

注意 “Program Ended” 是如何先于文件内容输出的,这展示了非阻塞特性。

Promises 提供了一种更结构化的方式来处理异步操作。Promise 代表一个异步操作的最终结果(或失败)。它可以处于三种状态之一:pending(待定)、fulfilled(已完成/ resolved)或 rejected(已拒绝)。

Promises 使用 .then() 来处理成功的结果,使用 .catch() 来处理错误。许多现代 Node.js API(特别是 fs.promises 等模块)返回 Promises。

const fs = require('fs').promises; // 导入基于 Promise 的版本
fs.readFile('input.txt', 'utf8')
.then(data => {
// 如果 Promise 完成(读取成功)则执行
console.log("File content (Promise):", data);
})
.catch(err => {
// 如果 Promise 被拒绝(发生错误)则执行
console.error("Error reading file (Promise):", err);
})
.finally(() => {
// 无论成功还是失败都会执行(可选)
console.log("File read attempt finished (Promise).");
});
console.log("Program continues while promise is pending...");
典型输出:
Program continues while promise is pending...
File content (Promise): Node.js is asynchronous!
File read attempt finished (Promise).

async 和 await 是基于 Promises 的语法糖,它们使异步代码看起来和行为更像同步代码,提高了可读性。

  • async 关键字用于声明一个处理异步操作的函数。
  • await 关键字只能在 async 函数内部使用。它会暂停 async 函数的执行,直到它等待的 Promise 被解决(resolved)或拒绝(rejected)。

使用 async/await 和 fs.promises 的示例:

Section titled “使用 async/await 和 fs.promises 的示例:”
const fs = require('fs').promises;
// 声明一个 async 函数
async function readFileAsync() {
console.log("Attempting to read file...");
try {
// await 在这里暂停执行,直到 Promise 解决
const data = await fs.readFile('input.txt', 'utf8');
console.log("File content (async/await):", data);
} catch (err) {
// 可以使用 try...catch 捕获 await 的 Promise 中发生的错误
console.error("Error reading file (async/await):", err);
}
console.log("Async function finished.");
}
// 调用 async 函数
readFileAsync();
console.log("Program continues after calling async function...");
典型输出:
Attempting to read file...
Program continues after calling async function...
File content (async/await): Node.js is asynchronous!
Async function finished.

结论: 虽然 callbacks 是理解 Node.js 历史的基础,但 Promises 特别是 async/await 是编写更清晰、更易读、更易维护的异步代码的现代标准。

尽管 Node.js 主要采用单线程模型,但 Event Loop 是其非阻塞 I/O 和并发能力背后的秘密武器。它是一种机制,允许 Node.js 在可能的情况下,将非阻塞操作(如读取文件、网络请求)卸载到系统内核进行。

  1. 代码执行: 你的 JavaScript 代码在单个主线程中运行。
  2. 异步操作: 当调用异步操作(例如 fs.readFile)时,Node.js 不会等待。它会启动该操作(通常由底层的 C++ 代码和像 libuv 这样的库处理,它们可能使用系统线程)并注册一个 Callback 函数,以便稍后执行。
  3. 事件队列: 当异步操作完成时(例如,文件读取完成、网络响应到达),其相应的 Callback 函数被放入一个事件队列中(具体来说,不同类型的事件如 I/O、定时器等有不同的队列)。
  4. 事件循环: Event Loop 会持续检查主执行堆栈(运行同步代码的地方)是否为空。如果为空,Event Loop 会从相应的事件队列中取出下一个 Callback 函数,并将其推送到堆栈上执行。
  5. Callback 执行: Callback 函数运行,可能会执行更多的操作,其中一些本身也可能是异步的。
  6. 重复: 循环继续,只要有待处理的事件或者进程尚未退出,就会一直处理事件并执行 Callback 函数。

可视化比喻:餐厅服务员

想象一下在一家繁忙的餐厅(你的应用程序)里有一位服务员(Node.js 线程)。

  • 服务员点餐(发起一个异步操作,如数据库查询)。
  • 服务员不会在厨房里等待食物(阻塞),而是立即去接待下一位顾客点餐(执行下一行代码)。
  • 当厨房(系统内核/libuv)准备好食物(完成异步操作)时,会按铃(将 Callback 函数放入事件队列)。
  • 服务员(Event Loop)空闲时(调用堆栈为空),听到铃声,取走食物(执行 Callback 函数),然后端给相应的桌子。

这使得一个服务员能够并发地服务许多桌子,而不会因为等待任何一个订单而被卡住。

  • Event Loop 使 Node.js 能够以单个主线程处理高并发。
  • I/O 操作是非阻塞的;Callbacks/Promises/async 函数处理它们的完成。
  • CPU 密集型的同步代码 会 阻塞 Event Loop,可能降低性能。对于此类任务,应使用 Worker Threads。
  • 理解 Event Loop 有助于编写高效且响应迅速的 Node.js 应用程序。

如需深入了解,请查阅 Node.js 官方关于 Event Loop、Timers 和 process.nextTick() 的文档。

Node.js 的大部分核心异步功能都围绕着事件驱动架构构建。称为“触发器”(emitters)的对象会触发命名事件,这些事件会引起先前注册的“监听器”(listener)函数被调用。

events 模块中的 EventEmitter 类是这种模式的核心。许多内置的 Node.js 对象(如 HTTP 服务器、流 streams)都继承自 EventEmitter。

const EventEmitter = require('events');
// 创建一个自定义触发器实例
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
  • 触发事件: 使用 emitter.emit(eventName[, ...args]) 来触发一个事件。
  • 监听事件: 使用 emitter.on(eventName, listener) 或 emitter.addListener(eventName, listener) 注册一个函数 (listener),该函数将在 eventName 被触发时被调用。
  • 只监听一次: 使用 emitter.once(eventName, listener) 注册一个监听器,该监听器只在事件下次触发时被调用一次,之后会自动移除。
  • 移除监听器: 使用 emitter.removeListener(eventName, listener) 或 emitter.off(eventName, listener) 来停止监听。
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
// 监听器函数 1
const listener1 = (arg1, arg2) => {
console.log('Event occurred with args:', arg1, arg2);
};
// 监听器函数 2 (只会运行一次)
const listener2 = () => {
console.log('This listener runs only once.');
};
// 注册监听器
myEmitter.on('someEvent', listener1);
myEmitter.once('someEvent', listener2);
myEmitter.on('someEvent', () => {
console.log('Another listener for someEvent.');
});
console.log('Emitting event for the first time:');
myEmitter.emit('someEvent', 'Hello', 'World'); // 所有监听器运行
console.log('\nEmitting event for the second time:');
myEmitter.emit('someEvent', 'Second', 'Time'); // listener2 已经移除
// 移除 listener1
myEmitter.removeListener('someEvent', listener1);
console.log('\nEmitting event after removing listener1:');
myEmitter.emit('someEvent', 'Third', 'Time'); // 只有匿名监听器运行
// 检查监听器数量
console.log(`\nListeners for someEvent: ${myEmitter.listenerCount('someEvent')}`);
输出:
Emitting event for the first time:
Event occurred with args: Hello World
This listener runs only once.
Another listener for someEvent.
Emitting event for the second time:
Event occurred with args: Second Time
Another listener for someEvent.
Emitting event after removing listener1:
Another listener for someEvent.
Listeners for someEvent: 1
  • 'error': 一个特殊事件。如果一个 EventEmitter 触发了 'error' 事件,并且没有为它注册监听器,Node.js 将抛出错误,打印堆栈跟踪,并退出进程。务必注册错误处理程序!
  • 'newListener': 当添加监听器时触发。
  • 'removeListener': 当移除监听器时触发。

EventEmitter 模式对于在 Node.js 中创建模块化和解耦的组件至关重要,尤其是在处理流、网络操作和其他异步活动时。

JavaScript 传统上很好地处理 Unicode 字符串,但缺乏处理原始二进制数据的健壮机制。在处理网络流、文件系统操作或 TCP/IP 等协议时,需要直接处理原始字节序列(八位字节流)。Node.js 引入了 Buffer 类来直接处理二进制数据。

Buffer 对象类似于整数数组(表示 0-255 范围内的字节),但它们对应于在 V8 JavaScript 引擎堆外分配的固定大小的原始内存块。Buffer 的大小一旦创建就不能改变。

Buffer 类是全局可用的;你不需要 require 它。

旧的 new Buffer() 构造函数因安全漏洞已被弃用。应改用以下静态方法:

  • Buffer.alloc(size[, fill[, encoding]]): 创建一个指定 size(字节)的新的、已初始化(零填充) 的 Buffer。出于安全考虑,这是创建已知大小的新 buffer 的首选方法。
  • Buffer.allocUnsafe(size): 创建一个指定 size 的新的、未初始化 的 Buffer。比 Buffer.alloc() 快,但分配的内存段可能包含旧的、潜在的敏感数据。仅在你确定会立即完全覆盖整个 buffer 或性能至关重要时使用。
  • Buffer.from(data[, encoding]): 创建一个包含提供的 data 副本 的新 Buffer。data 可以是一个字符串、数组、Buffer、ArrayBuffer 或 TypedArray。
// 创建一个大小为 10 字节的零填充 Buffer
const buf1 = Buffer.alloc(10);
console.log('buf1 (alloc):', buf1);
// 创建一个大小为 10 字节的未初始化 Buffer (内容是不可预测的)
// 读取前必须填充!
const buf2 = Buffer.allocUnsafe(10);
console.log('buf2 (allocUnsafe):', buf2);
// 立即填充 buf2
buf2.fill(0);
console.log('buf2 (filled):', buf2);
// 从字符串创建 Buffer (UTF-8 是默认编码)
const buf3 = Buffer.from('Hello Buffer');
console.log('buf3 (from string):', buf3);
console.log('buf3 decoded:', buf3.toString());
// 从字节数组 (0-255) 创建 Buffer
const buf4 = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // 'Hello' 的 ASCII 码
console.log('buf4 (from array):', buf4);
console.log('buf4 decoded:', buf4.toString('ascii'));
// 从另一个 Buffer 创建 Buffer (创建副本)
const buf5 = Buffer.from(buf3);
console.log('buf5 (from buffer):', buf5);

主要方法是 buf.write():

语法:`buf.write(string[, offset][, length][, encoding])`
  • string:要写入的字符串。
  • offset:开始写入的索引(默认:0)。
  • length:要写入的最大字节数(默认:buf.length - offset)。
  • encoding:字符串编码(默认:‘utf8’)。

返回值

返回写入的字节数。如果 buffer 没有足够的空间容纳整个字符串(考虑到 offset 和 length),则只会写入部分字符串。

const buf = Buffer.alloc(20);
const len = buf.write('Node.js Buffers');
console.log(`Bytes written: ${len}`); // 输出:Bytes written: 15
console.log(buf.toString()); // 输出:Node.js Buffers
// Buffer 的其余部分保持零填充

使用 buf.toString() 将 Buffer 数据解码为字符串:

语法:`buf.toString([encoding][, start][, end])`
  • encoding:使用的编码(默认:‘utf8’)。常用编码:‘utf8’、‘ascii’、‘base64’、‘hex’。
  • start:开始读取的索引(默认:0)。
  • end:停止读取的索引(不包含,默认:buf.length)。

返回 buffer 数据(或其一部分)的解码字符串表示。

const buf = Buffer.alloc(26);
for (let i = 0; i < 26; i++) {
buf[i] = i + 97; // 填充 ASCII 'a' 到 'z'
}
console.log(buf.toString('ascii')); // 输出:abcdefghijklmnopqrstuvwxyz
console.log(buf.toString('ascii', 0, 5)); // 输出:abcde
console.log(buf.toString('utf8', 5, 10)); // 输出:fghij (这里 UTF8 和 ASCII 兼容)
console.log(buf.toString(undefined, 10, 15)); // 编码默认为 'utf8',输出:klmno

buf.toJSON() 方法提供适合序列化的 JSON 表示。

buf.toJSON()

返回一个对象 { type: 'Buffer', data: [byte_values_array] }。请注意,JSON.stringify() 在 stringify Buffer 实例时会隐式调用此方法。

const buf = Buffer.from('Hello');
const json = buf.toJSON();
console.log(json);
// 输出:{ type: 'Buffer', data: [ 72, 101, 108, 108, 111 ] }
const jsonString = JSON.stringify(buf);
console.log(jsonString);
// 输出:"{\"type\":\"Buffer\",\"data\":[72,101,108,108,111]}"

使用静态方法 Buffer.concat() 将多个 Buffers 合并为一个新的 Buffer。

Buffer.concat(list[, totalLength])
  • list:要连接的 Buffer 实例数组。
  • totalLength (可选):合并后 buffer 的总长度。提供此参数可以提高性能,因为它避免了额外的循环来计算长度。

返回一个包含 list 中 buffers 连接数据的新 Buffer。

const buffer1 = Buffer.from('Node.js ');
const buffer2 = Buffer.from('is awesome!');
const buffer3 = Buffer.concat([buffer1, buffer2]);
console.log("buffer3 content: " + buffer3.toString());
// 输出:buffer3 content: Node.js is awesome!

使用 buf.compare(otherBuffer) 按字典顺序比较两个 buffers 的内容。

buf.compare(otherBuffer);
  • otherBuffer:与 buf 进行比较的另一个 Buffer 实例。

返回:如果 buf 等于 otherBuffer,则返回 0;如果在排序时 buf 应该排在 otherBuffer 之后,则返回 1;如果在排序时 buf 应该排在 otherBuffer 之前,则返回 -1。

const buffer1 = Buffer.from('ABC');
const buffer2 = Buffer.from('ABCD');
const buffer3 = Buffer.from('ABC');
console.log(buffer1.compare(buffer2)); // 输出:-1 (buffer1 排在 buffer2 之前)
console.log(buffer2.compare(buffer1)); // 输出:1 (buffer2 排在 buffer1 之后)
console.log(buffer1.compare(buffer3)); // 输出:0 (buffer1 与 buffer3 相同)
// 可用于对 Buffer 数组进行排序
const arr = [buffer2, buffer1, buffer3];
arr.sort(Buffer.compare);
console.log(arr.map(b => b.toString())); // 输出:[ 'ABC', 'ABC', 'ABCD' ]

使用 buf.copy() 将数据从一个 buffer (buf) 复制到另一个 buffer (targetBuffer)。

buf.copy(targetBuffer[, targetStart][, sourceStart][, sourceEnd])
  • targetBuffer:目标 Buffer 对象。
  • targetStart:在 targetBuffer 中开始写入的索引(默认:0)。
  • sourceStart:在源 buf 中开始复制的索引(默认:0)。
  • sourceEnd:在源 buf 中停止复制的索引(不包含,默认:buf.length)。

返回复制的字节数。

const buffer1 = Buffer.from('Source Data');
const buffer2 = Buffer.alloc(15); // 目标 buffer
// 将 buffer1 的全部内容复制到 buffer2
buffer1.copy(buffer2);
console.log("buffer2 content: " + buffer2.toString()); // 输出:buffer2 content: Source Data
// 将 buffer1 的一部分 ('Data') 复制到 buffer2,从索引 3 开始
const buffer3 = Buffer.alloc(10).fill(' '); // 用空格填充
buffer1.copy(buffer3, 3, 7, 11); // targetStart=3, sourceStart=7, sourceEnd=11 ('Data')
console.log("buffer3 content: '" + buffer3.toString() + "'"); // 输出:buffer3 content: ' Data '

使用 buf.slice() 创建一个新的 Buffer,它引用原始 buffer 内存的一部分。这是一种浅拷贝——对切片的修改会影响原始 Buffer,反之亦然。

buf.slice([start][, end])
  • start:切片开始的索引(包含,默认:0)。
  • end:切片结束的索引(不包含,默认:buffer.length)。

返回一个表示原始 buffer 内存指定部分的新 Buffer 实例。

const buffer1 = Buffer.from('Node.js Buffers');
// 创建一个包含 'Buffers' 的切片
const buffer2 = buffer1.slice(8, 15);
console.log("buffer2 content: " + buffer2.toString()); // 输出:buffer2 content: Buffers
// 修改切片
buffer2[0] = 0x58; // 将 'B' (0x42) 改为 'X' (0x58)
console.log("Modified slice: " + buffer2.toString()); // 输出:Modified slice: Xuffers
// 原始 buffer 也被修改了!
console.log("Original buffer after slice mod: " + buffer1.toString()); // 输出:Original buffer after slice mod: Node.js Xuffers

访问 buf.length 属性获取 buffer 分配的大小(字节)。

buf.length;

返回为 Buffer 实例分配的字节数。

const buffer = Buffer.from('Example');
// length 是字节数,不一定是字符数
console.log("buffer length: " + buffer.length); // 输出:buffer length: 7
const bufAlloc = Buffer.alloc(1024);
console.log("bufAlloc length: " + bufAlloc.length); // 输出:bufAlloc length: 1024

Buffer 类还提供了许多其他方法用于读取和写入特定数字类型(整数、浮点数、双精度数)以及不同字节顺序(endianness)、填充 buffers、检查编码等。查阅 Node.js 官方 Buffer 文档以获取完整的列表和详细解释。