跳到主要内容

核心知识点

一、 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。
+-----------------------------------------------------+
| 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 (先进先出) 队列来存放待执行的回调。

    1. Timers (定时器): 此阶段执行由 setTimeout()setInterval() 调度的回调。
    2. Pending Callbacks (待定回调): 执行延迟到下一个循环迭代的 I/O 回调。例如,当 TCP socket 收到 ECONNREFUSED 时,某些系统希望等待报告错误,这类回调就在此阶段执行。
    3. Idle, Prepare: 仅 Node.js 内部使用。
    4. Poll (轮询): 这是最重要的阶段之一。
      • 计算应该阻塞和轮询 I/O 的时间。
      • 处理轮询队列 (Poll Queue) 中的事件(主要是 I/O 相关回调,如网络连接、文件读写完成)。
      • 如果轮询队列不为空,循环将遍历回调队列并同步执行它们,直到队列为空或达到系统相关的硬限制。
      • 如果轮询队列为空:
        • 如果之前有 setImmediate() 调度,则进入 Check 阶段。
        • 如果没有 setImmediate(),且有 Timers 到期,则回到 Timers 阶段执行到期的定时器回调。
        • 如果两者都没有,事件循环将在此阶段阻塞等待新的 I/O 事件进入,直到有事件或定时器到期。
    5. Check (检查): 此阶段执行由 setImmediate() 调度的回调。
    6. Close Callbacks (关闭回调): 执行一些关闭事件的回调,例如 socket.on('close', ...),确保资源被正确释放。
  • 微任务队列 (Microtask Queue): process.nextTick()Promise 回调

    • 说明: 微任务不属于事件循环的任何一个阶段。它们有自己独立的队列。微任务会在当前宏任务(即事件循环某个阶段中的一个回调函数)执行完毕后、下一个宏任务开始之前立即执行。如果在微任务执行期间又添加了新的微任务,它们也会被添加到队列末尾并在当前微任务轮次中执行。
    • process.nextTick(): 它有自己的队列,并且其优先级通常高于 Promise 的微任务队列。nextTick 队列会在当前操作完成后立即处理。
    • Promise Callbacks (.then(), .catch(), .finally()): 当 Promise 状态变为 fulfilled 或 rejected 时,其回调会被放入 Promise 的微任务队列。
  • 执行顺序总结:

    1. 执行当前宏任务中的同步代码。
    2. 执行所有 process.nextTick() 回调。
    3. 执行所有 Promise 微任务回调。
    4. 进入事件循环的下一个阶段(或下一个宏任务)。
  • 示例:详细执行顺序

    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 核心模块广泛遵循“错误优先”约定:回调函数的第一个参数始终是错误对象(如果操作成功,则为 nullundefined),后续参数才是成功的结果。

    • 缺点: 容易产生嵌套过深的回调(回调地狱),代码难以阅读和维护,错误处理分散。

    • 示例:

      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: 当前模块文件所在目录的绝对路径字符串。
  • 模块查找规则(简化):

    1. 核心模块? (如 fs, path) -> 直接返回核心模块。
    2. 路径以 ./, ../, / 开头? -> 按文件路径查找。
      • 尝试 id
      • 尝试 id.js
      • 尝试 id.json (会解析为 JS 对象)。
      • 尝试 id.node (二进制 C++ 插件)。
      • 如果 id 是目录,查找 id/package.json 中的 main 字段指定的文件,或查找 id/index.js, id/index.json, id/index.node
    3. 非路径,非核心模块? -> 在 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;。每个模块只能有一个默认导出。
    • 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 获取)。
  • 示例 (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 . vs npx 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。它会缓冲命令的输出,并在子进程完成后通过回调函数一次性返回 stdoutstderr。适合执行简单的命令,但对大量输出不友好(可能耗尽内存)。
    • 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 时终止进程。
  • 最佳实践: 尽量在代码的局部范围内处理错误(使用 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.jsnode --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 的关键。