人人都会AI编程

2.3 核心模块与模块化基础

更新时间:2026-07-11

在 2.2 节中我们搭建了 Node.js 的开发环境并掌握了包管理器的基本操作,接下来需要理解 Node.js 组织代码的根本方式——模块。无论是使用内置功能、加载第三方包,还是拆分自己的业务逻辑,都离不开模块系统。这一节我们梳理 Node.js 中模块的分类、各自的加载方式,并从一个最简程序开始,体会从编码到运行的完整闭环。

Node.js 默认使用 CommonJS 规范作为模块系统,每个 .js 文件都被视为一个独立的模块。从 v12 开始,ES Modules(ESM)也得到实验性及稳定支持,我们将在第 5 章深入两者原理,本节以 CommonJS 为主线说明日常开发中最常见的用法。

2.3.1 三类模块的划分

任何 Node.js 应用中的代码,都可以归类为以下三种模块:

  • 核心模块(内置模块):Node.js 二进制文件内置的模块,由 C++ 或 JavaScript 实现。例如 fspathhttp 等。无需安装,直接通过 require('模块名') 加载即可。
  • 第三方模块:来自 npm 注册表、安装到 node_modules 目录下的包。例如 expresslodashmysql2 等。需要使用包管理器(npm/yarn/pnpm)安装后,才可在代码中 require('包名') 加载。
  • 自定义模块:开发者自己在项目目录中创建的文件。例如 ./utils.js../config/app.js 等。加载时需使用相对路径或绝对路径,且可以省略 .js 后缀。

三者共同构成了 Node.js 项目的模块生态,下面分别说明。

2.3.2 核心模块:开箱即用的标准库

Node.js 提供了四十余个内置模块,覆盖文件操作、网络编程、进程管理、加密解密等基础领域。常用的核心模块包括:

| 模块名 | 主要能力 |
|-----------|----------------------------------------|
| fs | 文件系统读写、目录操作、文件监听等 |
| path | 路径拼接、解析、规范化,跨平台兼容 |
| http | 创建 HTTP 服务器与客户端 |
| https | 同上,增加 TLS/SSL 支持 |
| url | URL 解析与构造 |
| querystring | 查询字符串解析与序列化 |
| os | 获取操作系统信息、CPU 架构、内存等 |
| process | 进程信息、环境变量、命令行参数、标准 I/O|
| events | 事件发射与监听(EventEmitter 基类) |
| stream | 流式数据处理接口 |
| crypto | 加密、摘要、HMAC 等 |
| child_process | 创建并管理子进程 |
| cluster | 多进程负载均衡 |

加载核心模块非常简单,只需在代码中 require 模块名,Node.js 会优先从内置模块中查找:

const fs = require('fs');           // 文件系统
const path = require('path');       // 路径处理
const http = require('http');       // HTTP 服务

核心模块由 Node.js 编译进二进制包,因此加载速度极快,且无需担心版本冲突。在项目初期,可以多翻阅官方文档,熟悉哪些能力已经“自带”,避免重复造轮子。

2.3.3 第三方模块:npm 生态的力量

当核心模块不能满足需求时(例如需要一个 Web 框架,或更好的日期处理库),就可以引入第三方模块。典型的流程是:

  1. 在项目根目录下执行 npm install <包名> 安装。
  2. 包会被下载到 node_modules 目录,并在 package.json 中记录依赖。
  3. 在代码中通过 require('包名') 引用,Node.js 会按规则搜索 node_modules 目录。

例如安装 Express:

npm install express

然后在 app.js 中使用:

const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello from Express');
});

app.listen(3000, () => {
  console.log('服务已启动:http://localhost:3000');
});

第三方模块也可以按需安装为开发依赖(--save-dev),例如测试框架或代码校验工具,这些包只在本机开发阶段使用,不会打包进生产环境。

2.3.4 自定义模块:组织自己的代码

随着应用变大,将所有逻辑写在一个文件里显然不可维护。Node.js 允许我们将逻辑拆分成多个自定义模块,通过 require 互相导入。

导出模块的两种方式:

  1. 将需要暴露的成员挂载到 module.exports 对象上。
  2. 直接重写 module.exports 为一个新的对象、类或函数。

最常见的是导出对象、函数或类:

// math.js - 导出多个工具函数
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

module.exports = {
  add,
  subtract
};
// greet.js - 直接导出函数
module.exports = function greet(name) {
  return `Hello, ${name}!`;
};

导入自定义模块:

// main.js
const math = require('./math');          // 相对路径,可省略 .js
const greet = require('./greet');

console.log(math.add(2, 3));    // 5
console.log(greet('Node'));     // Hello, Node!

注意区分exportsmodule.exports 的引用,如果只给 exports 添加属性,效果和给 module.exports 添加属性相同。但若直接对 exports 赋值(如 exports = function(){}),则会切断引用,导致模块导出失败。稳妥起见,始终使用 module.exports 导出。

加载自定义模块时,Node.js 按照以下顺序解析路径(假设 require('./mymod')):

  1. 查找 ./mymod.js 文件。
  2. 查找 ./mymod.json 文件。
  3. 查找 ./mymod.node 编译好的二进制扩展。
  4. ./mymod 当作目录,查找目录下的 index.js / index.json / index.node,或依据该目录中 package.jsonmain 字段决定入口文件。

了解这些查找规则有助于在模块路径出错时快速定位问题。

2.3.5 第一个 Node.js 程序:从代码到运行

我们已经了解了模块的分类和引入方式,现在动手写一个最简单但结构完整的 Node.js 程序,串联核心模块、自定义模块和第三方模块。

项目结构:

my-node-app/
├── package.json
├── main.js
└── lib/
    └── helper.js

1. 初始化项目:

mkdir my-node-app && cd my-node-app
npm init -y                # 生成默认 package.json

2. 安装第三方模块(演示用):

npm install dayjs          # 轻量日期处理库

3. 编写自定义模块 lib/helper.js

const os = require('os');   // 核心模块

function systemInfo() {
  return {
    platform: os.platform(),
    hostname: os.hostname(),
    cpus: os.cpus().length
  };
}

module.exports = { systemInfo };

4. 编写主入口 main.js

const fs = require('fs');             // 核心模块
const path = require('path');         // 核心模块
const dayjs = require('dayjs');       // 第三方模块
const { systemInfo } = require('./lib/helper'); // 自定义模块

// 使用核心模块获取系统信息
const info = systemInfo();
console.log('系统信息:', info);

// 使用第三方模块格式化时间
const now = dayjs().format('YYYY-MM-DD HH:mm:ss');

// 将结果写入文件
const outputPath = path.join(__dirname, 'output.txt');
const content = `记录时间:${now}\n平台:${info.platform}\nCPU 核心数:${info.cpus}\n`;

fs.writeFile(outputPath, content, (err) => {
  if (err) {
    console.error('写入文件失败:', err);
    return;
  }
  console.log('结果已保存至 output.txt');
});

这个程序展现了三种模块如何分工协作:核心模块 fspath 处理文件与路径;第三方模块 dayjs 简化日期格式化;自定义模块 helper.js 封装业务相关的系统信息获取。

5. 运行程序:

在项目根目录下执行:

node main.js

终端会输出类似信息,并生成 output.txt 文件:

系统信息: { platform: 'darwin', hostname: 'MacBook-Pro', cpus: 8 }
结果已保存至 output.txt

如果代码有错误,终端会堆栈追踪,可以快速定位。对于更复杂的调试需求,2.4 节将详细介绍 VS Code 断点调试与 Chrome DevTools 等工具,但仅靠 console.log 和终端输出,已经足够完成大部分模块功能的初步验证。

2.3.6 小结

模块化是 Node.js 工程的基石。通过将代码合理划分为核心模块、第三方模块和自定义模块,我们能充分复用 Node.js 内置能力、集成社区贡献的成熟方案,并保持自身业务逻辑的清晰与可维护性。本节中从模块分类到实际运行一个简单程序,覆盖了日常开发中最常用的模式:

  • 核心模块用 require 直接加载,无需额外安装。
  • 第三方模块用 npm 安装后按包名加载,赋予应用无限的扩展能力。
  • 自定义模块通过相对路径导入,使用 module.exports 导出对外接口,注意不要混淆 exportsmodule.exports
  • 第一个程序的运行验证了从源码到输出的完整流程,为后续框架开发、工程化实践打下基础。

接下来,我们会深入了解 Node.js 的模块解析与缓存原理,以及 ESM 与 CommonJS 的互操作细节,这将帮助你写出更可靠、更高效的模块代码。