核心知识点
一、 Node.js 简介
1. 什么是 Node.js?
- 说明: Node.js 不是一门新的编程语言,也不是一个 Web 框架。它是一个基于 Chrome V8 引擎 的 JavaScript 运行时环境 (Runtime Environment)。它允许开发者使用 JavaScript 编写服务器端代码,以及构建各种命令行工具和桌面应用(通过 Electron 等)。Node.js 的出现极大地扩展了 JavaScript 的应用领域。
- 关键特性:
- 事件驱动 (Event-driven): Node.js 内部维护一个事件循环机制,通过监听事件来触发相应的回调函数,非常适合处理 I/O 密集型任务。
- 非阻塞 I/O (Non-blocking I/O): Node.js 的绝大部分 I/O 操作(如文件读写、网络请求)都是异步非阻塞的。发起 I/O 请求后,Node.js 不会等待结果返回,而是继续执行后续代码,当 I/O 操作完成时,通过事件和回调函数来处理结果。这使得 Node.js 在单线程模型下也能高效处理大量并发连接。
- 单线程: 指的是 JavaScript 的执行是单线程的。Node.js 通过事件循环机制避免了多线程编程中常见的锁和状态同步问题。但其底层的 libuv 库会使用线程池来处理那些无法完全异步化的阻塞操作(如部分文件系统操作、DNS 查询、部分加密操作),从而避免阻塞主线程。
- npm (Node Package Manager): 拥有全球最大的开源库生态系统,极大地简化了项目依赖管理和代码共享。
2. 为什么前端开发者需要了解 Node.js?
- 前端工程化基石: 现代前端开发流程严重依赖 Node.js。构建工具 (Webpack, Vite, Rollup, Parcel, esbuild)、脚手架 (Create React App, Vue CLI, Angular CLI)、CSS 预处理器/后处理器编译器 (Sass, Less, Stylus, PostCSS)、代码检查和格式化工具 (ESLint, Prettier) 等都需要 Node.js 环境来运行。
- 服务端渲染 (SSR) / 同构应用 / Universal Apps: 为了更好的 SEO 和首屏加载性能,许多现代框架(如 Next.js (React), Nuxt.js (Vue), SvelteKit, Angular Universal)依赖 Node.js 在服务器端执行 JavaScript,渲染页面内容后直接返回给浏览器。
- 开发 Mock API / BFF (Backend for Frontend): 在前后端分离的开发模式中,前端开发者可以使用 Node.js 快速搭建 Mock API 服务器来模拟后端接口,或者构建 BFF 层,聚合、裁剪后端微服务接口,为特定前端应用提供更友好的数据格式。
- 命令行工具 (CLI): 开发团队内部的脚手架、自动化部署脚本、项目管理工具等,Node.js 是非常合适的选择。
- 全栈开发能力: 掌握 Node.js 使前端开发者能够涉足后端开发,构建完整的 Web 应用。
- 新兴技术: WebAssembly、GraphQL 服务器、WebSocket 服务等也可以用 Node.js 来实现。
3. Node.js 架构核心:V8 与 libuv
- V8 引擎:
- 说明: Google 开源的高性能 JavaScript 和 WebAssembly 引擎,用 C++ 编写。负责解析 JavaScript 代码,将其编译成本地机器码执行。V8 提供了内存管理、垃圾回收、即时编译 (JIT) 等核心功能。Node.js 将 V8 嵌入其中,使其能在服务器环境运行。
- 作用: 执行 JavaScript 逻辑代码。
- libuv:
- 说明: 一个用 C 语言编写的多平台异步 I/O 库。它是 Node.js 实现事件循环和非阻塞 I/O 的关键。libuv 封装了不同操作系统(如 Linux 的 epoll, macOS 的 kqueue, Windows 的 IOCP)的底层异步机制,为 Node.js 提供了统一、跨平台的异步 API。
- 作用:
- 提供事件循环机制。
- 处理异步 I/O 操作(网络、文件系统、DNS 等)。
- 管理线程池以处理阻塞任务。
- 提供定时器、子进程、信号处理等功能。
- Node.js Bindings:
- 说明: 连接 V8、libuv 和 Node.js 核心库(用 C++ 编写)的桥梁。它允许 JavaScript 代码调用 C++ 实现的功能(如文件系统操作),并将 libuv 中的事件通知给 JavaScript 的事件循环。
- Node.js Core Library:
- 说明: 用 JavaScript 编写的一系列核心模块(如
http
,fs
,path
,events
),提供了开发者常用的功能 API。这些 JS 模块底层会调用 C++ Bindings 和 libuv。
- 说明: 用 JavaScript 编写的一系列核心模块(如
+-----------------------------------------------------+
| Your Node.js Application (JS) |
+-----------------------------------------------------+
| Node.js Core Library (JS) |
| (http, fs, path, events, etc.) |
+-----------------------------------------------------+
| Node.js Bindings (C++) |
+---------------------+-------------------------------+
| V8 Engine (C++)| libuv (C) |
| (JS Execution, GC) | (Event Loop, Async I/O, |
| | Thread Pool) |
+---------------------+-------------------------------+
| Operating System |
+-----------------------------------------------------+
二、 Node.js 核心概念
1. 事件循环 (Event Loop)
-
说明: Node.js 实现非阻塞异步模型的基石。它是一个在 Node.js 启动时初始化并在后台持续运行的循环。它的主要职责是接收、处理事件,并调用相关的回调函数。正是因为事件循环,Node.js 才可以在单线程中处理大量并发操作。
-
阶段 (Phases): 事件循环按特定顺序执行不同的阶段,每个阶段都有一个 FIFO (先进先出) 队列来存放待执行的回调。
- Timers (定时器): 此阶段执行由
setTimeout()
和setInterval()
调度的回调。 - Pending Callbacks (待定回调): 执行延迟到下一个循环迭代的 I/O 回调。例如,当 TCP socket 收到
ECONNREFUSED
时,某些系统希望等待报告错误,这类回调就在此阶段执行。 - Idle, Prepare: 仅 Node.js 内部使用。
- Poll (轮询): 这是最重要的阶段之一。
- 计算应该阻塞和轮询 I/O 的时间。
- 处理轮询队列 (Poll Queue) 中的事件(主要是 I/O 相关回调,如网络连接、文件读写完成)。
- 如果轮询队列不为空,循环将遍历回调队列并同步执行它们,直到队列为空或达到系统相关的硬限制。
- 如果轮询队列为空:
- 如果之前有
setImmediate()
调度,则进入 Check 阶段。 - 如果没有
setImmediate()
,且有 Timers 到期,则回到 Timers 阶段执行到期的定时器回调。 - 如果两者都没有,事件循环将在此阶段阻塞等待新的 I/O 事件进入,直到有事件或定时器到期。
- 如果之前有
- Check (检查): 此阶段执行由
setImmediate()
调度的回调。 - Close Callbacks (关闭回调): 执行一些关闭事件的回调,例如
socket.on('close', ...)
,确保资源被正确释放。
- Timers (定时器): 此阶段执行由
-
微任务队列 (Microtask Queue):
process.nextTick()
和Promise
回调- 说明: 微任务不属于事件循环的任何一个阶段。它们有自己独立的队列。微任务会在当前宏任务(即事件循环某个阶段中的一个回调函数)执行完毕后、下一个宏任务开始之前立即执行。如果在微任务执行期间又添加了新的微任务,它们也会被添加到队列末尾并在当前微任务轮次中执行。
process.nextTick()
: 它有自己的队列,并且其优先级通常高于 Promise 的微任务队列。nextTick
队列会在当前操作完成后立即处理。- Promise Callbacks (
.then()
,.catch()
,.finally()
): 当 Promise 状态变为 fulfilled 或 rejected 时,其回调会被放入 Promise 的微任务队列。
-
执行顺序总结:
- 执行当前宏任务中的同步代码。
- 执行所有
process.nextTick()
回调。 - 执行所有 Promise 微任务回调。
- 进入事件循环的下一个阶段(或下一个宏任务)。
-
示例:详细执行顺序
console.log("1 [sync]: Script Start");
// Timers queue
setTimeout(() => {
console.log("7 [timer]: setTimeout callback");
Promise.resolve().then(() =>
console.log("8 [promise]: Promise inside setTimeout")
);
process.nextTick(() =>
console.log("9 [nextTick]: nextTick inside setTimeout")
);
}, 0);
// Check queue
setImmediate(() => {
console.log("10 [immediate]: setImmediate callback");
Promise.resolve().then(() =>
console.log("11 [promise]: Promise inside setImmediate")
);
process.nextTick(() =>
console.log("12 [nextTick]: nextTick inside setImmediate")
);
});
// Promise microtask queue
Promise.resolve().then(() => {
console.log("4 [promise]: Promise 1 resolved");
process.nextTick(() =>
console.log("5 [nextTick]: nextTick inside Promise 1")
);
});
// nextTick queue
process.nextTick(() => {
console.log("2 [nextTick]: nextTick 1 callback");
Promise.resolve().then(() =>
console.log("3 [promise]: Promise inside nextTick 1")
);
});
// I/O operation (will likely complete after initial sync code and microtasks)
const fs = require("fs");
fs.readFile(__filename, () => {
console.log("13 [poll]: readFile callback (I/O)");
setTimeout(() => console.log("16 [timer]: setTimeout inside readFile"), 0);
setImmediate(() =>
console.log("17 [immediate]: setImmediate inside readFile")
);
process.nextTick(() =>
console.log("14 [nextTick]: nextTick inside readFile")
);
Promise.resolve().then(() =>
console.log("15 [promise]: Promise inside readFile")
);
});
// Sync code continues
queueMicrotask(() => {
// Another way to queue a microtask (like Promise)
console.log("6 [microtask]: queueMicrotask callback");
});
console.log("1.1 [sync]: Script End");
// 常见输出顺序 (I/O 回调时机不确定,但通常在初始同步和微任务之后):
// 1 [sync]: Script Start
// 1.1 [sync]: Script End
// 2 [nextTick]: nextTick 1 callback
// 4 [promise]: Promise 1 resolved
// 3 [promise]: Promise inside nextTick 1
// 5 [nextTick]: nextTick inside Promise 1
// 6 [microtask]: queueMicrotask callback
// --- Event Loop Starts ---
// (Poll phase might wait for I/O or check timers/immediates)
// --- Timer Phase ---
// 7 [timer]: setTimeout callback
// 9 [nextTick]: nextTick inside setTimeout
// 8 [promise]: Promise inside setTimeout
// --- Poll Phase (likely processes readFile callback now) ---
// 13 [poll]: readFile callback (I/O)
// 14 [nextTick]: nextTick inside readFile
// 15 [promise]: Promise inside readFile
// --- Check Phase ---
// 10 [immediate]: setImmediate callback
// 12 [nextTick]: nextTick inside setImmediate
// 11 [promise]: Promise inside setImmediate
// 17 [immediate]: setImmediate inside readFile
// --- Timer Phase (next loop iteration) ---
// 16 [timer]: setTimeout inside readFile注意:
setTimeout(..., 0)
并不保证立即执行,它只是将回调放入 Timers 队列,实际执行至少要等到下一次事件循环的 Timers 阶段。其与setImmediate
的执行顺序在某些情况下(如在 I/O 回调中)是确定的,但在顶层代码中可能受进程性能影响而不确定。
2. 非阻塞 I/O (Non-blocking I/O)
-
说明: 这是 Node.js 高性能的关键。当 Node.js 遇到 I/O 操作(如网络请求、数据库查询、文件读写)时,它不会停下来等待操作完成。相反,它会将操作交给底层的 libuv(通常使用操作系统的异步接口或线程池),然后继续执行后续的 JavaScript 代码。当操作完成时,libuv 会将结果和一个回调函数(如果提供了)放入事件队列,等待事件循环来处理。
-
优点:
- 高并发: 单个线程可以处理大量并发连接,因为线程不会因为等待 I/O 而阻塞。
- 资源高效: 相对于为每个连接创建一个线程的传统模型,Node.js 的事件驱动模型消耗更少的内存和 CPU 资源。
-
示例:Web 服务器处理请求
const http = require("http");
const fs = require("fs");
http
.createServer((req, res) => {
if (req.url === "/readfile") {
console.log("Received request for /readfile, starting async read...");
// 非阻塞文件读取
fs.readFile("large_file.txt", (err, data) => {
if (err) {
console.error("File read error:", err);
res.writeHead(500);
res.end("Server Error");
return;
}
console.log("/readfile request finished reading file.");
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("File content read successfully.");
});
console.log("readFile call initiated, server continues to listen..."); // 这会先打印
} else if (req.url === "/quick") {
console.log("Received request for /quick");
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("This response is quick!");
} else {
res.writeHead(404);
res.end("Not Found");
}
})
.listen(3001, () => {
console.log("Server listening on port 3001");
// 创建一个模拟的大文件
fs.writeFile(
"large_file.txt",
Buffer.alloc(10 * 1024 * 1024, "a"),
(err) => {
if (err) console.error("Failed to create large file:", err);
else console.log("Large file created for testing.");
}
);
});
// 同时发起两个请求:
// 1. curl http://localhost:3001/readfile
// 2. curl http://localhost:3001/quick
// 你会发现对 /quick 的请求几乎立刻返回,即使 /readfile 的请求正在进行(读取大文件需要时间)。
// 这证明了读取文件的操作没有阻塞服务器处理其他请求。
3. 异步编程模式
Node.js 大量使用异步操作,掌握处理异步的模式至关重要。
-
回调函数 (Callbacks) & 错误优先风格 (Error-first Callback):
-
说明: 这是 Node.js 早期最主要的异步处理方式。异步函数接受一个回调函数作为最后一个参数。当操作完成时,调用该回调函数。Node.js 核心模块广泛遵循“错误优先”约定:回调函数的第一个参数始终是错误对象(如果操作成功,则为
null
或undefined
),后续参数才是成功的结果。 -
缺点: 容易产生嵌套过深的回调(回调地狱),代码难以阅读和维护,错误处理分散。
-
示例:
const fs = require("fs");
function processUserData(userId, callback) {
// 模拟异步获取用户信息
fs.readFile(`./user_${userId}.json`, "utf8", (err, userDataStr) => {
if (err) {
// 错误优先:第一个参数传递错误
return callback(
new Error(`Failed to read user data for ${userId}: ${err.message}`)
);
}
try {
const userData = JSON.parse(userDataStr);
// 模拟异步获取订单信息
fs.readFile(
`./orders_${userId}.json`,
"utf8",
(orderErr, orderDataStr) => {
if (orderErr) {
return callback(
new Error(
`Failed to read orders for ${userId}: ${orderErr.message}`
)
);
}
try {
const orders = JSON.parse(orderDataStr);
// 成功:第一个参数为 null,后续为结果
callback(null, { user: userData, orders: orders });
} catch (parseOrderErr) {
callback(
new Error(
`Failed to parse order data: ${parseOrderErr.message}`
)
);
}
}
);
} catch (parseUserErr) {
callback(
new Error(`Failed to parse user data: ${parseUserErr.message}`)
);
}
});
}
// 调用 (可能形成回调地狱)
processUserData(123, (err, result) => {
if (err) {
console.error("Error processing user data:", err.message);
// 处理错误...
} else {
console.log("User Data:", result.user);
console.log("Orders:", result.orders);
// 可能还有后续的异步操作...
}
});
-
-
Promises:
-
说明: ES6 引入的标准异步解决方案。Promise 对象代表一个异步操作的最终完成(或失败)及其结果值。它解决了回调地狱问题,提供了更清晰的链式调用 (
.then()
) 和统一的错误处理 (.catch()
)。Node.js 的许多核心模块(如fs.promises
)都提供了基于 Promise 的 API。 -
状态: Pending (进行中), Fulfilled (已成功), Rejected (已失败)。状态一旦改变就不会再变。
-
组合:
Promise.all()
(等待所有 Promise 完成),Promise.race()
(等待第一个 Promise 完成),Promise.allSettled()
(等待所有 Promise 完成,无论成功或失败),Promise.any()
(等待第一个 Promise 成功)。 -
示例:
const fs = require("fs").promises; // 使用 Promise 版本的 fs
function getUserData(userId) {
return fs
.readFile(`./user_${userId}.json`, "utf8")
.then(JSON.parse) // 链式处理解析
.catch((err) => {
// 捕获读取或解析错误
throw new Error(
`Failed to get user data for ${userId}: ${err.message}`
);
});
}
function getOrderData(userId) {
return fs
.readFile(`./orders_${userId}.json`, "utf8")
.then(JSON.parse)
.catch((err) => {
throw new Error(`Failed to get orders for ${userId}: ${err.message}`);
});
}
function processUserDataPromise(userId) {
// 使用 Promise.all 并行获取用户和订单数据
return Promise.all([getUserData(userId), getOrderData(userId)])
.then(([userData, orderData]) => {
// 成功后组合结果
return { user: userData, orders: orderData };
})
.catch((err) => {
// 统一捕获 getUserData 或 getOrderData 中的错误
console.error("Error processing user data (Promise):", err.message);
// 可以选择性地处理或重新抛出错误
throw err; // 如果希望调用者也能捕获
});
}
// 调用
processUserDataPromise(123)
.then((result) => {
console.log("User Data (Promise):", result.user);
console.log("Orders (Promise):", result.orders);
})
.catch((err) => {
console.error("Final catch block:", err.message);
// 处理最终错误
});
-
-
Async/Await:
-
说明: ES2017 (ES8) 引入的语法糖,建立在 Promise 之上。它允许以一种看似同步的方式编写异步代码,极大地提高了代码的可读性和可维护性。
async
关键字用于声明一个函数是异步的(它总是返回一个 Promise),await
关键字用于暂停async
函数的执行,等待一个 Promise 被 resolved 或 rejected,然后恢复执行。await
只能在async
函数内部使用。 -
错误处理: 可以使用标准的
try...catch
语句来捕获await
等待的 Promise 可能 reject 的错误。 -
示例:
const fs = require("fs").promises;
async function getUserDataAsync(userId) {
try {
const userDataStr = await fs.readFile(`./user_${userId}.json`, "utf8");
return JSON.parse(userDataStr);
} catch (err) {
throw new Error(
`Failed to get user data for ${userId}: ${err.message}`
);
}
}
async function getOrderDataAsync(userId) {
try {
const orderDataStr = await fs.readFile(
`./orders_${userId}.json`,
"utf8"
);
return JSON.parse(orderDataStr);
} catch (err) {
throw new Error(`Failed to get orders for ${userId}: ${err.message}`);
}
}
async function processUserDataAsync(userId) {
try {
console.log(`Processing data for user ${userId}...`);
// 并行获取
const [userData, orderData] = await Promise.all([
getUserDataAsync(userId),
getOrderDataAsync(userId),
]);
// 或串行获取 (如果需要)
// const userData = await getUserDataAsync(userId);
// const orderData = await getOrderDataAsync(userId);
console.log("User Data (Async/Await):", userData);
console.log("Orders (Async/Await):", orderData);
return { user: userData, orders: orderData };
} catch (err) {
console.error(
`Error processing user data (Async/Await) for ${userId}:`,
err.message
);
throw err; // 将错误抛给调用者
}
}
// 调用 async 函数
(async () => {
// 使用 IIAFE (Immediately Invoked Async Function Expression)
try {
const result = await processUserDataAsync(123);
console.log("Processing finished successfully.");
// 使用 result ...
} catch (err) {
console.error("Error in main async execution flow:", err.message);
// 处理最终错误
}
})();
-
三、 Node.js 模块系统
Node.js 提供了强大的模块系统来组织和复用代码。
1. CommonJS (CJS)
-
说明: Node.js 最初且默认的模块系统。模块加载是同步的,发生在代码执行时。
-
核心 API/变量:
require(id)
: 用于导入其他模块。id
可以是核心模块名、相对路径 (./
,../
) 或绝对路径的文件/目录,或者是node_modules
中的包名。Node.js 会解析路径并查找模块。如果模块已被加载,会返回缓存的版本。module
: 代表当前模块的对象。它有一个exports
属性。module.exports
: 真正导出值的对象。当你require
一个模块时,你实际上得到的是那个模块的module.exports
对象。默认是一个空对象{}
。你可以给它赋任何值(对象、函数、类、字符串等)。exports
: 这是module.exports
的一个便捷引用,即exports = module.exports
。你可以给exports
添加属性 (exports.foo = 'bar'
),这等同于给module.exports.foo
赋值。但是,不能直接给exports
重新赋值 (exports = function() {}
),因为这会改变exports
变量的引用,使其不再指向module.exports
,导致模块无法正确导出。__filename
: 当前模块文件的绝对路径字符串。__dirname
: 当前模块文件所在目录的绝对路径字符串。
-
模块查找规则(简化):
- 核心模块? (如
fs
,path
) -> 直接返回核心模块。 - 路径以
./
,../
,/
开头? -> 按文件路径查找。- 尝试
id
。 - 尝试
id.js
。 - 尝试
id.json
(会解析为 JS 对象)。 - 尝试
id.node
(二进制 C++ 插件)。 - 如果
id
是目录,查找id/package.json
中的main
字段指定的文件,或查找id/index.js
,id/index.json
,id/index.node
。
- 尝试
- 非路径,非核心模块? -> 在
node_modules
目录中查找。- 从当前目录的
node_modules
开始查找。 - 如果没找到,向上级目录的
node_modules
查找,直到根目录。 - 全局
node_modules
目录(不推荐依赖)。
- 从当前目录的
- 核心模块? (如
-
示例:
// calculator.js (CommonJS Module)
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
const PI = 3.14159;
// 导出方式 1: 赋值给 module.exports (推荐导出多个值)
module.exports = {
add,
subtract,
PI,
description: "A simple calculator module",
};
// 导出方式 2: 给 exports 添加属性
// exports.add = add;
// exports.subtract = subtract;
// exports.PI = PI;
// exports.description = 'A simple calculator module';
// 导出方式 3: 导出一个单一实体 (如类或函数)
// class Calculator { ... }
// module.exports = Calculator;
// function createCalculator() { ... }
// module.exports = createCalculator;
console.log("Executing calculator.js"); // 模块代码只在第一次 require 时执行// main_app.js (Using the CJS module)
console.log("Starting main_app.js");
// 导入模块
const calc = require("./calculator"); // 导入本地模块
const os = require("os"); // 导入核心模块
const _ = require("lodash"); // 导入 node_modules 包
console.log("Calculator description:", calc.description);
console.log("Add 5 + 3 =", calc.add(5, 3));
console.log("Subtract 10 - 4 =", calc.subtract(10, 4));
console.log("PI:", calc.PI);
// 再次 require 同一个模块会从缓存读取
const calc2 = require("./calculator");
console.log("Is calc === calc2?", calc === calc2); // Output: true
console.log("OS Platform:", os.platform());
console.log("Random number from lodash:", _.random(1, 100));
console.log("Current file:", __filename);
console.log("Current directory:", __dirname);
console.log("Finishing main_app.js");
2. ES Modules (ESM)
-
说明: ECMAScript 官方标准模块系统,是 JavaScript 语言层面的模块化规范。ESM 加载是异步的(在浏览器中),并且是静态解析的(在编译/解析阶段确定导入导出关系),这使得 Tree Shaking(移除未使用的代码)等优化成为可能。Node.js 从 v13.2 开始正式支持 ESM。
-
在 Node.js 中启用 ESM:
- 方式一:文件扩展名: 将文件命名为
.mjs
。 - 方式二:
package.json
: 在项目的package.json
文件中添加顶层字段"type": "module"
。这样,所有.js
文件都会被 Node.js 视为 ESM。如果需要在此模式下使用 CommonJS 文件,可以将其重命名为.cjs
。
- 方式一:文件扩展名: 将文件命名为
-
核心语法:
export
: 用于从模块中导出变量、函数、类。- 命名导出 (Named Exports):
export const name = ...;
,export function func() {...};
,export class Cls {...};
,export { var1, var2 as alias };
。可以有多个命名导出。 - 默认导出 (Default Export):
export default expression;
。每个模块只能有一个默认导出。
- 命名导出 (Named Exports):
import
: 用于从其他模块导入。- 导入命名导出:
import { name1, name2 as alias } from './module.mjs';
- 导入默认导出:
import anyName from './module.mjs';
(名字anyName
是自定义的) - 导入默认和命名导出:
import defaultName, { named1, named2 } from './module.mjs';
- 整体导入:
import * as utils from './module.mjs';
(将所有命名导出收集到utils
对象中,默认导出在utils.default
) - 仅执行副作用:
import './module.mjs';
(只执行模块代码,不导入任何变量) - 动态导入
import()
:import('./module.mjs').then(module => {...});
返回一个 Promise,允许在运行时按需加载模块。可在async
函数中使用await import(...)
。
- 导入命名导出:
-
ESM 特有的元信息:
-
import.meta.url
: 提供当前模块文件的 URL (通常是file://
URL)。 -
在 Node.js 中获取
__filename
和__dirname
:import { fileURLToPath } from "url";
import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
-
-
与 CommonJS 的互操作性:
- 可以在 ESM 文件中使用
import
导入 CommonJS 模块。Node.js 会将 CJS 的module.exports
包装成 ESM 的默认导出。 - 不能在 CommonJS 文件中直接使用
import
语句(除非使用动态import()
)。 - 不能在 ESM 文件中使用
require()
,module.exports
,exports
,__filename
,__dirname
(需要通过import.meta.url
获取)。
- 可以在 ESM 文件中使用
-
示例 (
package.json
中"type": "module"
):// logger.js (ESM Module)
export function logInfo(message) {
console.log(`[INFO] ${new Date().toISOString()}: ${message}`);
}
export const LogLevel = {
INFO: "info",
WARN: "warn",
ERROR: "error",
};
function logError(message) {
// 未导出,模块内部使用
console.error(`[ERROR] ${new Date().toISOString()}: ${message}`);
}
export default class Logger {
constructor(level = LogLevel.INFO) {
this.level = level;
}
log(message) {
if (this.level === LogLevel.INFO) {
logInfo(message);
}
}
error(message) {
logError(message); // 调用内部函数
}
}
console.log("Executing logger.js (ESM)");// main_app.js (Using the ESM module)
import CustomLogger, { logInfo, LogLevel } from "./logger.js"; // 导入默认和命名导出
// import * as loggerModule from './logger.js'; // 整体导入
import os from "os"; // 导入核心模块 (ESM 风格)
import _ from "lodash"; // 导入 CJS 包 (Node.js 兼容处理)
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import { readFile } from "fs/promises"; // 使用 Promise API
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log("Starting main_app.js (ESM)");
logInfo("Application started."); // 使用命名导出的函数
console.log("Available log levels:", LogLevel);
const loggerInstance = new CustomLogger(LogLevel.INFO);
loggerInstance.log("This is logged by Logger instance.");
loggerInstance.error("This is an error log.");
// // 使用整体导入
// loggerModule.logInfo('Info via namespace');
// const loggerInstance2 = new loggerModule.default();
// loggerInstance2.log('Logged by instance 2');
console.log("OS Type:", os.type());
console.log("Capitalize using lodash:", _.capitalize("hello world"));
console.log("Current file (ESM):", __filename);
console.log("Current directory (ESM):", __dirname);
// 动态导入示例
async function loadMoment() {
try {
const moment = (await import("moment")).default; // moment 是 CJS,导入其 default
console.log(
"Moment loaded dynamically:",
moment().format("YYYY-MM-DD HH:mm:ss")
);
} catch (err) {
console.error("Failed to load moment:", err);
}
}
loadMoment();
// 异步读取文件
readFile(join(__dirname, "calculator.js"), "utf8") // 读取之前的 CJS 文件
.then((content) =>
console.log(
"\nRead calculator.js content (first 100 chars):",
content.substring(0, 100)
)
)
.catch((err) => console.error("Failed to read file:", err));
console.log("Finishing main_app.js (ESM)");
四、 NPM 与包管理 (内容已在上一轮提供,此处略作补充和调整)
1. NPM (Node Package Manager)
- 说明: Node.js 生态的核心,用于发现、安装、管理和发布可重用的 JavaScript 代码包(模块)。
- 核心文件:
package.json
: 项目清单文件,描述项目及其依赖。package-lock.json
: 锁定项目依赖树的确切版本,保证安装的一致性。必须提交到版本控制。node_modules/
: 存放实际下载的依赖包代码。通常不提交到版本控制 (在.gitignore
中忽略)。
2. package.json
详解
- 关键字段:
name
,version
: 包的唯一标识。description
,keywords
: 用于发现和描述包。main
: CommonJS 包的入口点。module
: (非官方但广泛使用) 指向 ESM 入口点,供 Webpack/Rollup 等打包工具使用。exports
: (Node.js 官方推荐) 更现代、更强大的方式来定义包的入口点和条件导出(区分 CJS/ESM、浏览器/Node 等环境)。可以精细控制哪些文件可以被外部访问。type
:"commonjs"
或"module"
,定义项目中.js
文件的默认模块类型。scripts
: 定义项目脚本 (start, test, build, lint 等)。dependencies
: 生产环境依赖。devDependencies
: 开发环境依赖。peerDependencies
: 表明包需要宿主环境(安装此包的项目)提供某个特定版本的依赖。如果版本不兼容,npm/yarn/pnpm 会给出警告。optionalDependencies
: 可选依赖,即使安装失败也不会导致整体安装失败。engines
: 指定项目运行所需的 Node.js 和 npm 版本范围。private
:true
可防止意外发布到 npm。repository
,bugs
,homepage
: 项目相关链接。author
,contributors
: 作者信息。license
: 开源许可证。
exports
字段示例:{
"name": "my-dual-package",
"version": "1.0.0",
"type": "module", // 项目默认是 ESM
"main": "./dist/index.cjs", // CJS 入口 (兼容旧 require)
"module": "./dist/index.js", // ESM 入口 (供打包工具)
"exports": {
".": {
// 包的主入口 (import pkg from 'my-dual-package')
"import": "./dist/index.js", // ESM 环境使用
"require": "./dist/index.cjs" // CommonJS 环境使用
},
"./feature": {
// 子路径导出 (import feature from 'my-dual-package/feature')
"import": "./dist/feature.js",
"require": "./dist/feature.cjs"
},
"./package.json": "./package.json" // 允许访问 package.json
}
// ... other fields
}
3. 版本管理 (SemVer)
- 说明: Semantic Versioning (语义化版本控制) 是一种广泛采用的版本号规范,格式为
主版本号.次版本号.修订号
(MAJOR.MINOR.PATCH)。- MAJOR: 当你做了不兼容的 API 修改。
- MINOR: 当你做了向下兼容的功能性新增。
- PATCH: 当你做了向下兼容的问题修正。
package.json
中的版本范围:^1.2.3
: 允许更新 PATCH 和 MINOR 版本 (>=1.2.3, <2.0.0)。这是npm install <pkg>
的默认行为。~1.2.3
: 只允许更新 PATCH 版本 (>=1.2.3, <1.3.0)。1.2.3
: 精确匹配版本。>=1.2.3
: 大于等于指定版本。*
或latest
: 匹配最新版本(不推荐用于生产依赖)。
4. npx
- 说明: npm 5.2+ 自带的命令,用于执行 npm 包中的可执行文件。
- 主要用途:
- 临时运行包命令: 无需全局或本地安装,直接运行包提供的命令(如
npx create-react-app my-app
)。npx 会检查本地node_modules/.bin
和环境变量 PATH,如果找不到命令对应的包,会临时下载并执行,执行完毕后不保留。 - 执行本地安装的命令: 可以方便地执行
devDependencies
中的命令,而无需配置npm scripts
或写完整路径 (./node_modules/.bin/eslint .
vsnpx eslint .
)。 - 切换 Node.js 版本执行命令:
npx -p node@16 npm install
(使用 Node 16 来执行npm install
)。
- 临时运行包命令: 无需全局或本地安装,直接运行包提供的命令(如
五、 Node.js 核心 API (内置模块) - 续
除了 fs
, path
, http
/https
, events
之外,还有其他重要的核心模块。
5. stream
- 流 (续)
-
为什么使用流?
- 内存效率: 处理大文件或大量数据时,无需将所有内容读入内存,只需处理一小块 (chunk) 数据。
- 时间效率: 数据可用时即可开始处理,不必等待整个资源加载完成。
- 可组合性: 可以通过
pipe()
将多个流操作(如读取、压缩、加密、写入)连接起来,形成高效的数据处理管道。
-
示例:使用流进行 Gzip 压缩
const fs = require("fs");
const zlib = require("zlib"); // Node.js 内置的压缩模块
const path = require("path");
const sourceFilePath = path.join(__dirname, "large_example.txt");
const gzipDestPath = path.join(__dirname, "large_example.txt.gz");
const readable = fs.createReadStream(sourceFilePath);
const gzip = zlib.createGzip(); // Transform stream
const writable = fs.createWriteStream(gzipDestPath);
console.log("Starting compression...");
// 创建处理管道: Read -> Gzip -> Write
readable.pipe(gzip).pipe(writable);
// 监听完成事件
writable.on("finish", () => {
console.log("File successfully compressed.");
});
// 错误处理很重要
readable.on("error", (err) => console.error("Read error:", err));
gzip.on("error", (err) => console.error("Gzip error:", err));
writable.on("error", (err) => console.error("Write error:", err)); -
示例:创建自定义可读流
const { Readable } = require("stream");
// 创建一个从 1 数到 10 的可读流
class CounterStream extends Readable {
constructor(options) {
super(options);
this._index = 1;
this._max = 10;
}
// 当流的消费者准备好接收数据时,会调用 _read()
_read(size) {
if (this._index > this._max) {
this.push(null); // 发送 null 表示流结束
} else {
const chunk = Buffer.from(String(this._index), "utf8");
this.push(chunk); // 推送数据块
this._index++;
}
}
}
const counter = new CounterStream();
// 消费流数据
counter.on("data", (chunk) => {
console.log("Received chunk:", chunk.toString());
});
counter.on("end", () => {
console.log("Counter stream ended.");
});
6. buffer
- 缓冲区
-
说明: Buffer 类用于在 Node.js 中处理二进制数据。JavaScript 语言本身没有读取或操作二进制数据流的机制。Buffer 实例类似于整数数组,但它对应 V8 堆外分配的固定大小的原始内存块。Buffer 是全局对象,无需
require
。 -
主要用途: 在 TCP 流、文件系统操作等场景中处理字节流。当需要与 C++ 插件交互或处理原始二进制协议时非常有用。
-
创建 Buffer:
Buffer.alloc(size[, fill[, encoding]])
: 创建一个指定大小(字节)的新 Buffer,并用fill
值(默认为 0)填充。速度较快,推荐用于新内存。Buffer.allocUnsafe(size)
: 创建一个指定大小的新 Buffer,但不进行初始化。内容是未知的(可能包含旧数据),速度更快,但有潜在安全风险,除非你立即完全覆盖它。Buffer.from(string[, encoding])
: 从字符串创建 Buffer。Buffer.from(array)
: 从字节数组 (Uint8Array
) 创建 Buffer。Buffer.from(buffer)
: 从另一个 Buffer 创建(复制)。Buffer.from(arrayBuffer[, byteOffset[, length]])
: 从ArrayBuffer
创建。
-
常用 API:
buf.length
: Buffer 的字节长度。buf.toString([encoding[, start[, end]]])
: 将 Buffer 解码为字符串(默认 'utf8')。buf.write(string[, offset[, length]][, encoding])
: 将字符串写入 Buffer。buf[index]
: 访问或设置指定索引处的字节(整数 0-255)。buf.slice([start[, end]])
: 创建一个指向原始 Buffer 相同内存区域的新 Buffer(浅拷贝)。修改 slice 会影响原始 Buffer。buf.copy(targetBuffer[, targetStart[, sourceStart[, sourceEnd]]])
: 将 Buffer 的内容复制到另一个 Buffer。Buffer.concat(list[, totalLength])
: 将 Buffer 数组连接成一个新的 Buffer。buf.equals(otherBuffer)
: 比较两个 Buffer 内容是否相同。Buffer.isBuffer(obj)
: 检查对象是否是 Buffer。Buffer.byteLength(string[, encoding])
: 计算字符串按指定编码转换后的字节长度。
-
示例:
// 创建 Buffer
const buf1 = Buffer.alloc(10); // 10 字节,初始化为 0
console.log("buf1:", buf1); // <Buffer 00 00 00 00 00 00 00 00 00 00>
const buf2 = Buffer.from("Hello", "utf8");
console.log("buf2:", buf2); // <Buffer 48 65 6c 6c 6f>
console.log("buf2 length:", buf2.length); // 5
console.log("buf2 toString:", buf2.toString()); // Hello
const buf3 = Buffer.from([0x68, 0x65, 0x6c, 0x6c, 0x6f]); // from byte array
console.log("buf3 toString:", buf3.toString()); // hello
// 写入和读取
const buf4 = Buffer.alloc(20);
buf4.write("Node.js Buffer");
console.log("buf4 toString:", buf4.toString()); // Node.js Buffer
// 超出 Buffer 大小的写入会被截断
buf4.write("!!", 14, 2, "utf8"); // 在偏移量 14 处写入 '!!'
console.log("buf4 modified:", buf4.toString()); // Node.js Buffer!!
console.log("Byte at index 0:", buf4[0]); // 78 (ASCII for N)
// Slice (浅拷贝)
const buf5 = Buffer.from("JavaScript");
const slice = buf5.slice(4, 10); // 从索引 4 到 10 (不含 10)
console.log("slice:", slice.toString()); // Script
slice[0] = 0x63; // 修改 slice 的第一个字节 (对应 'S' 改为 'c')
console.log("buf5 after slice modification:", buf5.toString()); // Javaccrip t
// Concat
const header = Buffer.from("HEADER:");
const body = Buffer.from("Some data");
const message = Buffer.concat([header, Buffer.from(" "), body]);
console.log("Concatenated message:", message.toString()); // HEADER: Some data
// 比较
const b1 = Buffer.from("abc");
const b2 = Buffer.from("abc");
const b3 = Buffer.from("def");
console.log("b1 equals b2?", b1.equals(b2)); // true
console.log("b1 equals b3?", b1.equals(b3)); // false
7. os
- 操作系统信息
-
说明: 提供与操作系统相关的实用方法。
-
常用 API:
os.EOL
: 操作系统特定的行末标志 (\n
on POSIX,\r\n
on Windows)。os.arch()
: 返回 CPU 架构 (e.g., 'x64', 'arm')。os.platform()
: 返回操作系统平台 (e.g., 'darwin', 'linux', 'win32')。os.cpus()
: 返回一个包含每个 CPU/核心信息的对象数组。os.freemem()
: 返回系统空闲内存量(字节)。os.totalmem()
: 返回系统总内存量(字节)。os.homedir()
: 返回当前用户的主目录路径。os.tmpdir()
: 返回操作系统的默认临时文件目录。os.hostname()
: 返回操作系统的主机名。os.networkInterfaces()
: 返回一个包含网络接口信息的对象。os.userInfo([options])
: 返回当前有效用户的信息。
-
示例:
const os = require("os");
console.log("OS Platform:", os.platform());
console.log("CPU Architecture:", os.arch());
console.log("End of Line marker:", JSON.stringify(os.EOL)); // Show invisible chars
console.log("Home Directory:", os.homedir());
console.log("Temp Directory:", os.tmpdir());
const cpus = os.cpus();
console.log(`Number of CPUs: ${cpus.length}`);
console.log("First CPU Model:", cpus[0].model);
const totalMemGB = (os.totalmem() / 1024 ** 3).toFixed(2);
const freeMemGB = (os.freemem() / 1024 ** 3).toFixed(2);
console.log(`Total Memory: ${totalMemGB} GB`);
console.log(`Free Memory: ${freeMemGB} GB`);
// console.log('Network Interfaces:', os.networkInterfaces());
8. url
- URL 处理
-
说明: 提供用于 URL 解析和处理的工具。Node.js 推荐使用 WHATWG URL API(与浏览器兼容),但也保留了旧版的
url.parse()
等 API。 -
WHATWG URL API (推荐):
new URL(input[, base])
: 解析 URL 字符串。如果input
是相对路径,需要提供base
URL。URL
实例属性:href
,protocol
,username
,password
,host
,hostname
,port
,pathname
,search
,searchParams
(URLSearchParams 对象),hash
。URLSearchParams
类: 用于处理 URL 查询字符串。get()
,set()
,append()
,delete()
,has()
,toString()
,forEach()
,keys()
,values()
,entries()
.
-
旧版 API (Legacy):
url.parse(urlString[, parseQueryString[, slashesDenoteHost]])
: 解析 URL 字符串为对象。url.format(urlObject)
: 将 URL 对象格式化回 URL 字符串。url.resolve(from, to)
: 解析相对 URL。
-
示例:
const { URL, URLSearchParams } = require("url"); // WHATWG API
// const urlLegacy = require('url'); // Legacy API
const myUrlString =
"https://user:pass@sub.example.com:8080/p/a/t/h?query=string&id=123#hash";
// 使用 WHATWG URL API (推荐)
try {
const myUrl = new URL(myUrlString);
console.log("--- WHATWG URL API ---");
console.log("href:", myUrl.href);
console.log("protocol:", myUrl.protocol); // 'https:'
console.log("username:", myUrl.username); // 'user'
console.log("password:", myUrl.password); // 'pass'
console.log("hostname:", myUrl.hostname); // 'sub.example.com'
console.log("port:", myUrl.port); // '8080'
console.log("pathname:", myUrl.pathname); // '/p/a/t/h'
console.log("search:", myUrl.search); // '?query=string&id=123'
console.log("hash:", myUrl.hash); // '#hash'
// 使用 URLSearchParams
console.log("\nSearch Params:");
console.log("query:", myUrl.searchParams.get("query")); // 'string'
console.log("id:", myUrl.searchParams.get("id")); // '123'
myUrl.searchParams.append("newParam", "value");
console.log("toString after append:", myUrl.searchParams.toString()); // query=string&id=123&newParam=value
console.log("New href:", myUrl.href); // URL 对象会自动更新 href
for (const [key, value] of myUrl.searchParams.entries()) {
console.log(` - ${key}: ${value}`);
}
} catch (err) {
console.error("Invalid URL:", err);
}
// // 使用 Legacy API (了解即可)
// const urlLegacy = require('url');
// const parsedLegacy = urlLegacy.parse(myUrlString, true); // true 解析 query
// console.log('\n--- Legacy url.parse() ---');
// console.log('hostname:', parsedLegacy.hostname);
// console.log('pathname:', parsedLegacy.pathname);
// console.log('query object:', parsedLegacy.query);
9. process
- 进程信息与控制
-
说明:
process
对象是一个全局对象(无需require
),提供有关当前 Node.js 进程的信息并对其进行控制。 -
常用属性/方法:
process.argv
: 返回一个数组,包含启动 Node.js 进程时的命令行参数。第一个元素是node
可执行文件路径,第二个元素是当前 JS 文件路径,后续元素是传递的其他参数。process.env
: 返回一个包含用户环境信息的对象(如PATH
,HOME
)。注意: 敏感信息(如 API 密钥)应通过更安全的方式管理,而不是直接硬编码或放在.env
文件中提交。process.cwd()
: 返回当前 Node.js 进程的工作目录。process.pid
: 当前进程的 ID。process.platform
: 返回运行进程的操作系统平台(同os.platform()
)。process.arch
: 返回运行进程的 CPU 架构(同os.arch()
)。process.version
: Node.js 版本字符串。process.versions
: 包含 Node.js 及其依赖(如 V8, libuv, OpenSSL)版本信息的对象。process.exit([code])
: 以指定的状态码code
退出当前进程(0 表示成功,非 0 表示错误)。process.nextTick(callback[, ...args])
: 将回调函数添加到nextTick
队列,在当前操作完成后、事件循环下一阶段开始前执行。process.on(eventName, listener)
: 监听进程事件,如:'exit'
: 进程即将退出时触发(在此回调中不能执行异步操作)。'uncaughtException'
: 当一个未被捕获的 JavaScript 异常冒泡回事件循环时触发。不推荐用它来恢复应用正常运行,主要用于执行同步清理或记录日志。'unhandledRejection'
: 当一个 Promise 被 reject 且没有catch
处理时触发。- 信号事件 (如
'SIGINT'
,'SIGTERM'
): 处理操作系统信号。
process.stdout
,process.stderr
,process.stdin
: 分别代表标准输出、标准错误、标准输入的流对象。console.log
内部就是使用process.stdout
。
-
示例:
// 获取命令行参数
console.log("Command line arguments:", process.argv);
// Run: node your_script.js arg1 arg2
// Output: [ '/path/to/node', '/path/to/your_script.js', 'arg1', 'arg2' ]
// 获取环境变量
console.log(
"Current PATH:",
process.env.PATH ? process.env.PATH.substring(0, 50) + "..." : "N/A"
);
console.log("NODE_ENV:", process.env.NODE_ENV || "development"); // 常用于区分环境
// 获取进程信息
console.log("Current working directory:", process.cwd());
console.log("Process ID:", process.pid);
console.log("Node.js version:", process.version);
// console.log('All versions:', process.versions);
// 监听退出事件
process.on("exit", (code) => {
// 只能执行同步操作
console.log(`\nProcess exiting with code: ${code}`);
});
// 监听未捕获异常 (应谨慎使用)
process.on("uncaughtException", (err, origin) => {
console.error(`\nCaught exception: ${err}\nException origin: ${origin}`);
// 在这里记录日志、清理资源,然后通常应该退出
process.exit(1); // 强制退出
});
// 监听未处理的 Promise rejection
process.on("unhandledRejection", (reason, promise) => {
console.error("\nUnhandled Rejection at:", promise, "reason:", reason);
// 记录日志,可能也需要退出
// process.exit(1);
});
// 模拟未捕获异常
// throw new Error('This is an uncaught exception!');
// 模拟未处理的 rejection
// Promise.reject(new Error('This is an unhandled rejection!'));
// 正常退出 (如果上面的模拟被注释掉)
// setTimeout(() => {
// console.log('\nExiting normally...');
// process.exit(0);
// }, 1000);
console.log("Script continues...");
10. child_process
- 子进程
-
说明: Node.js 是单线程的(指 JS 执行),为了充分利用多核 CPU 或执行外部命令,
child_process
模块允许你创建子进程。 -
创建子进程的方式:
child_process.exec(command[, options][, callback])
: 启动一个 shell 来执行command
。它会缓冲命令的输出,并在子进程完成后通过回调函数一次性返回stdout
和stderr
。适合执行简单的命令,但对大量输出不友好(可能耗尽内存)。child_process.execFile(file[, args][, options][, callback])
: 类似exec
,但不启动 shell,直接执行file
。更安全高效,适合执行特定可执行文件。child_process.spawn(command[, args][, options])
: 启动一个新进程来执行command
。它不缓冲输出,而是通过流(stdout
,stderr
)来处理输入输出。适合处理大量数据或需要与子进程持续交互的场景。返回一个ChildProcess
实例。child_process.fork(modulePath[, args][, options])
:spawn
的特殊形式,专门用于创建新的 Node.js 进程。父子进程之间可以通过send()
方法和message
事件建立 IPC (Inter-Process Communication) 通道进行通信。
-
ChildProcess
实例事件/属性:stdout
: 子进程的标准输出流 (Readable Stream)。stderr
: 子进程的标准错误流 (Readable Stream)。stdin
: 子进程的标准输入流 (Writable Stream)。pid
: 子进程的 PID。on('message', (message) => ...)
: (仅fork
) 接收子进程通过process.send()
发送的消息。on('error', (error) => ...)
: 进程无法被衍生或被杀死时触发。on('exit', (code, signal) => ...)
: 子进程退出时触发。on('close', (code, signal) => ...)
: 子进程所有 stdio 流都关闭后触发。send(message)
: (仅fork
) 向子进程发送消息。kill([signal])
: 向子进程发送信号(默认 'SIGTERM')。
-
示例:
const { exec, spawn } = require("child_process");
// 1. 使用 exec (简单命令,缓冲输出)
console.log("--- Using exec ---");
exec("ls -lh /usr", (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
if (stderr) {
console.error(`exec stderr: ${stderr}`);
}
console.log(
`exec stdout (first 200 chars):\n${stdout.substring(0, 200)}...`
);
});
// 2. 使用 spawn (流式处理,适合大数据/交互)
console.log("\n--- Using spawn ---");
const findProcess = spawn("find", [__dirname, "-type", "f"]); // 执行 find 命令
let fileCount = 0;
// 监听 stdout 流
findProcess.stdout.on("data", (data) => {
const lines = data
.toString()
.split("\n")
.filter((line) => line.length > 0);
fileCount += lines.length;
console.log(`spawn stdout chunk:\n${lines.join("\n")}`);
});
// 监听 stderr 流
findProcess.stderr.on("data", (data) => {
console.error(`spawn stderr: ${data}`);
});
// 监听错误事件
findProcess.on("error", (err) => {
console.error("Failed to start subprocess.", err);
});
// 监听退出事件
findProcess.on("exit", (code, signal) => {
if (code !== null) {
console.log(`\nspawn process exited with code ${code}`);
console.log(`Total files found: ${fileCount}`);
} else if (signal !== null) {
console.log(`\nspawn process killed with signal ${signal}`);
}
});
// // 3. 使用 fork (Node.js 子进程与 IPC 通信)
// console.log('\n--- Using fork ---');
// const worker = fork(require.resolve('./worker.js')); // 使用 require.resolve 获取绝对路径
// worker.on('message', (msg) => {
// console.log('Message from worker:', msg);
// if (msg.type === 'done') {
// worker.kill(); // 任务完成,结束子进程
// }
// });
// worker.on('exit', (code) => {
// console.log(`Worker process exited with code ${code}`);
// });
// worker.send({ type: 'start', payload: 10 }); // 向子进程发送消息
// // worker.js (子进程文件)
// process.on('message', (msg) => {
// console.log('[Worker] Received message:', msg);
// if (msg.type === 'start') {
// let result = 0;
// for (let i = 0; i < msg.payload * 100000000; i++) { // 模拟耗时计算
// result += i;
// }
// process.send({ type: 'done', result }); // 向父进程发送结果
// }
// });
六、 Node.js Web 开发基础 (以 Express 为例)
虽然 Node.js 提供了 http
模块,但直接使用它来构建复杂的 Web 应用比较繁琐。通常我们会使用框架来简化开发。Express 是目前最流行、最成熟的 Node.js Web 框架之一。
1. Express 简介
- 说明: 一个基于 Node.js
http
模块构建的、简洁、灵活的 Web 应用框架。它提供了一系列强大的特性,用于快速创建 Web 和 API 服务,如路由、中间件、模板引擎集成等。 - 核心概念:
- 路由 (Routing): 定义应用程序如何响应客户端对特定端点(URI)和特定 HTTP 方法(GET, POST 等)的请求。
- 中间件 (Middleware): 本质上是一个函数,可以访问请求对象 (req)、响应对象 (res) 和应用程序请求-响应周期中的下一个中间件函数 (
next
)。中间件可以:- 执行任何代码。
- 修改请求和响应对象。
- 结束请求-响应周期。
- 调用下一个中间件。
- 常用于日志记录、身份验证、数据解析、错误处理等。
- 请求对象 (req): Express 对 Node.js 原生
http.IncomingMessage
对象的增强,添加了req.params
,req.query
,req.body
,req.ip
,req.path
等有用属性。 - 响应对象 (res): Express 对 Node.js 原生
http.ServerResponse
对象的增强,提供了res.send()
,res.json()
,res.status()
,res.render()
,res.redirect()
等便捷方法。
2. 基本使用
-
安装:
npm install express
-
示例:一个简单的 Express 应用
const express = require("express");
const path = require("path");
const app = express(); // 创建 Express 应用实例
const port = 3002;
// --- 中间件 ---
// 1. 应用级中间件 (每次请求都会执行)
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next(); // 调用 next() 将控制权传递给下一个中间件或路由处理器
});
// 2. 内置中间件 - 解析 JSON 请求体
app.use(express.json()); // for parsing application/json
// 3. 内置中间件 - 解析 URL 编码请求体
app.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
// 4. 内置中间件 - 托管静态文件 (如 HTML, CSS, JS, 图片)
app.use(express.static(path.join(__dirname, "public"))); // 将 public 目录设为静态资源根目录
// --- 路由 ---
// GET 请求根路径
app.get("/", (req, res) => {
// res.send('<h1>Hello World from Express!</h1>'); // 发送 HTML
// 提供 public/index.html
res.sendFile(path.join(__dirname, "public", "index.html"));
});
// GET 请求 /about
app.get("/about", (req, res) => {
res.send("This is the About page.");
});
// GET 请求带路由参数 /users/:userId
app.get("/users/:userId", (req, res) => {
const userId = req.params.userId; // 获取路由参数
res.send(`User Profile for User ID: ${userId}`);
});
// GET 请求带查询参数 /search?q=keyword
app.get("/search", (req, res) => {
const query = req.query.q; // 获取查询参数
if (!query) {
return res.status(400).send('Missing search query parameter "q"');
}
res.send(`Searching for: ${query}`);
});
// POST 请求 /api/data
app.post("/api/data", (req, res) => {
const requestData = req.body; // 获取 JSON 或 URL 编码的请求体 (需要 express.json() 或 express.urlencoded() 中间件)
console.log("Received POST data:", requestData);
if (!requestData || Object.keys(requestData).length === 0) {
return res
.status(400)
.json({ error: "Request body is empty or invalid" });
}
res
.status(201)
.json({ message: "Data received successfully", data: requestData });
});
// --- 错误处理中间件 ---
// (通常放在所有路由和中间件之后)
// 它有四个参数 (err, req, res, next)
app.use((err, req, res, next) => {
console.error("Unhandled Error:", err.stack); // 记录错误堆栈
res.status(500).send("Something broke!");
});
// 404 处理 (如果没有任何路由匹配)
app.use((req, res, next) => {
res.status(404).send("Sorry, can't find that!");
});
// --- 启动服务器 ---
app.listen(port, () => {
console.log(`Express server listening at http://localhost:${port}`);
});需要创建一个
public
目录并在其中放入一个index.html
文件来测试静态文件服务。
七、 Node.js 错误处理
健壮的错误处理对于构建可靠的 Node.js 应用至关重要。
1. 同步错误处理 (try...catch
)
-
说明: 对于同步代码中可能抛出的异常,使用标准的
try...catch
块来捕获。 -
示例:
function parseJsonSync(jsonString) {
try {
const data = JSON.parse(jsonString);
console.log("JSON parsed successfully:", data);
return data;
} catch (error) {
// 捕获 JSON.parse 可能抛出的 SyntaxError
console.error("Failed to parse JSON:", error.message);
// 返回 null 或抛出自定义错误
return null;
}
}
parseJsonSync('{"name": "Node.js"}'); // Success
parseJsonSync('{"invalid json"'); // Error logged
2. 异步错误处理
- 错误优先回调: 回调函数的第一个参数用于传递错误对象。在使用回调风格的 API 时,必须检查第一个参数。
fs.readFile("nonexistent.txt", (err, data) => {
if (err) {
// 必须检查 err
console.error("readFile error:", err);
return; // 停止后续处理
}
// 处理 data
}); - Promises (
.catch()
): 使用 Promise 链时,可以在链的末尾添加.catch()
来捕获链中任何一个环节产生的 rejection。fs.promises
.readFile("file.txt")
.then((data) => processData(data)) // processData 也可能返回 Promise 或抛错
.then((result) => saveResult(result))
.catch((err) => {
// 捕获 readFile, processData, saveResult 中的错误
console.error("Promise chain error:", err);
}); - Async/Await (
try...catch
): 在async
函数中,使用try...catch
来包围await
表达式,捕获其等待的 Promise 被 reject 时抛出的错误。async function processFile() {
try {
const data = await fs.promises.readFile("file.txt");
const result = await processData(data); // await 会解包 Promise 或捕获同步错误
await saveResult(result);
console.log("Process completed.");
} catch (err) {
// 捕获任何 await 失败或 try 块中的同步错误
console.error("Async function error:", err);
}
} - EventEmitter (
'error'
事件): 对于继承自EventEmitter
的对象(如流、服务器),必须监听'error'
事件。如果触发了'error'
事件但没有监听器,Node.js 进程通常会崩溃。const readable = fs.createReadStream("nonexistent.txt");
readable.on("error", (err) => {
// 必须监听 error
console.error("Stream error:", err);
});
readable.on("data", (chunk) => {
/* ... */
});
3. 全局未捕获错误
process.on('uncaughtException', handler)
:- 说明: 当同步代码中抛出的异常没有被任何
try...catch
捕获,最终冒泡到事件循环时触发。 - 用途: 主要用于同步地清理资源、记录致命错误日志,然后退出进程 (
process.exit(1)
)。不应该试图用它来恢复应用的运行,因为进程可能处于不一致状态。
- 说明: 当同步代码中抛出的异常没有被任何
process.on('unhandledRejection', handler)
:- 说明: 当一个 Promise 被 reject,但在事件循环的当前轮次或微任务队列处理结束时,仍然没有
.catch()
处理程序附加到它上面时触发。 - 用途: 记录未处理的 Promise 错误,根据情况决定是否需要退出进程。Node.js 未来版本可能会默认在 unhandledRejection 时终止进程。
- 说明: 当一个 Promise 被 reject,但在事件循环的当前轮次或微任务队列处理结束时,仍然没有
- 最佳实践: 尽量在代码的局部范围内处理错误(使用
try...catch
,.catch()
,'error'
监听器)。全局处理器是最后的防线,主要用于日志记录和优雅关闭。
八、 Node.js 调试
console
模块:console.log()
: 打印普通信息。console.info()
,console.debug()
: 类似log
。console.warn()
: 打印警告信息。console.error()
: 打印错误信息(输出到 stderr)。console.table(data)
: 将数组或对象以表格形式打印。console.time(label)
,console.timeEnd(label)
: 测量代码块执行时间。console.trace()
: 打印当前位置的堆栈跟踪。console.assert(assertion, ...messages)
: 如果assertion
为 false,则打印消息并抛出 AssertionError。
- Node.js 内置调试器 (Legacy):
- 启动:
node inspect your_script.js
(或node debug ...
旧版)。 - 提供命令行调试接口 (cont, next, step, out, watch, repl 等)。使用较少。
- 启动:
- Chrome DevTools Inspector (推荐):
- 启动:
node --inspect your_script.js
或node --inspect-brk your_script.js
(-brk
会在第一行暂停)。 - Node.js 会输出一个
devtools://
URL。 - 在 Chrome 浏览器中打开
chrome://inspect
,点击 "Open dedicated DevTools for Node",或直接访问 Node.js 输出的 URL。 - 提供与前端调试类似的图形化界面:断点、单步执行 (step over, step into, step out)、查看变量、监视表达式、性能分析 (Profiler)、内存分析 (Memory)。
- 启动:
- VS Code Debugger:
- VS Code 内置了强大的 Node.js 调试支持。
- 在代码中设置断点。
- 配置
launch.json
文件(通常可以自动生成)来定义启动和附加调试会话的方式。 - 提供图形化调试界面,与 Chrome DevTools 类似。
九、 总结
Node.js 是一个功能强大且用途广泛的 JavaScript 运行时环境。对于前端开发者而言,掌握 Node.js 不仅是进行现代前端工程化的必备技能,也开启了通往服务端渲染、API 开发乃至全栈开发的大门。理解其事件驱动、非阻塞 I/O 的核心机制,熟练运用异步编程模式、模块系统、核心 API 以及 NPM 包管理,是高效使用 Node.js 的关键。