Node.js 基础与模块:CommonJS 与 ES 模块系统

FreeGuideOnline 最新 2026-06-15

Node.js 入门

什么是 Node.js?

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,它让 JavaScript 脱离了浏览器的限制,可以在服务器端运行。通过事件驱动、非阻塞 I/O 模型,Node.js 非常适合构建高并发、实时性的网络应用,如 API 服务、聊天工具或命令行工具。它拥有庞大的包生态系统(npm),是全球最大的开源库集合。

安装与环境配置

访问 Node.js 官网 下载对应操作系统的 LTS(长期支持)版本。安装完成后,打开终端(或命令提示符)输入以下命令验证安装:

node -v
npm -v

看到版本号即表示安装成功。建议同时安装一个代码编辑器(如 VS Code)并熟悉终端的基本操作。

你的第一个 Node.js 程序

创建一个名为 app.js 的文件,输入以下代码:

// app.js
const message = 'Hello, Node.js!';
console.log(message);

在终端中执行 node app.js,你将看到打印出的问候语。这看似简单,但背后体现了 Node.js 直接执行 JavaScript 的能力。接下来,我们将深入理解 Node.js 的核心——模块系统。

Node.js 模块系统详解

为什么需要模块化?

在大型项目中,将所有代码写在一个文件里会导致难以维护、命名冲突和复用性差。模块化将代码分割成独立的功能单元,每个模块拥有自己的作用域,通过接口暴露需要共享的部分。Node.js 原生支持两种模块系统:CommonJS(历史默认)和 ES 模块(现代标准)。

CommonJS 模块规范

CommonJS 是 Node.js 从诞生起就采用的模块规范,主要使用 require() 导入和 module.exportsexports 导出。它是同步加载的,适合服务器环境,因为文件就在本地磁盘,加载速度很快。每个文件都被视为一个独立的模块。

ES 模块 (ECMAScript Modules)

ES 模块是 JavaScript 官方标准(ES2015+),使用 importexport 语法。它在浏览器和现代 Node.js 中均受支持。ES 模块静态结构允许在编译时确定依赖关系,支持异步加载和高级优化(如 tree-shaking)。Node.js 从 v12 起稳定支持该特性。

CommonJS 实战

使用 require 加载模块

在 CommonJS 中,通过 require 函数引入模块。可以加载内置模块、第三方包或自定义文件模块。加载文件时需要指定路径,可省略 .js 后缀。

// 加载内置 fs 模块
const fs = require('fs');

// 加载自定义模块(路径省略 .js)
const math = require('./math');
console.log(math.add(2, 3));

require 是同步的,它会读取文件、执行文件代码并返回模块的 exports 对象。模块第一次加载后会缓存,多次引用同一个模块会直接返回缓存结果。

使用 module.exports 导出内容

每个模块中 module 对象有一个 exports 属性,该属性就是模块对外暴露的接口。可以直接给 module.exports 赋值,也可以使用快捷方式 exports 添加属性。

// math.js
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;

// 方式一:直接赋值(推荐,清晰)
module.exports = { add, multiply };

// 方式二:逐个添加属性(注意:不能对 exports 直接赋值,会切断引用)
// exports.add = add;
// exports.multiply = multiply;

若使用 exports = { add } 是错误的,因为它仅仅改变了 exports 变量的指向,不会影响 module.exports,最终导出的仍为空对象。始终记住:真正导出的是 module.exports

模块缓存与循环依赖

Node.js 会在第一次 require 时执行模块代码并缓存其结果对象。如果出现模块 A 引用模块 B,模块 B 又引用模块 A 的循环依赖,Node.js 会返回尚未执行完的模块 A 当前已导出的部分(可能是不完整的对象),从而避免死循环。应当尽量避免循环依赖,保持单向依赖是最佳实践。

ES 模块实战

在 Node.js 中启用 ES 模块

Node.js 默认将 .js 文件视为 CommonJS 模块。要使用 ES 模块,有三种方式:

  • 将文件扩展名改为 .mjs
  • package.json 中设置 "type": "module",此时该目录下的 .js 文件将被解释为 ES 模块
  • 在命令行使用 --input-type=module 标志(适用于字符串输入)

推荐在项目根目录的 package.json 中添加 "type": "module",统一使用 ES 模块标准。

// package.json
{
  "name": "my-node-app",
  "version": "1.0.0",
  "type": "module"
}

命名导出与默认导出

ES 模块提供两种导出方式:命名导出(可以多个)和默认导出(每个模块一个)。

// utils.mjs
// 命名导出
export function formatDate(date) { /* ... */ }
export const PI = 3.14159;

// 默认导出(通常用于导出类、函数或主要对象)
export default class HttpClient { /* ... */ }

导入时,命名导出需要使用解构语法 { },默认导出可以任意命名。

import HttpClient, { formatDate, PI } from './utils.mjs';
// HttpClient 为默认导出,名称可自定义
// formatDate, PI 必须使用花括号并保持名称一致,除非使用 `as` 重命名

动态导入 import()

ES 模块的 import 声明是静态的,必须位于模块顶层。若想在运行时按需加载模块,可以使用动态 import(),它返回一个 Promise。

// 按需加载模块
if (condition) {
  import('./heavy-module.mjs')
    .then(module => module.doWork())
    .catch(err => console.error('加载失败', err));
}

这在代码拆分、懒加载场景中非常有用,且兼容 CommonJS 模块。

CommonJS 与 ES 模块对比

语法差异与互操作性

特性 CommonJS ES 模块
加载语法 const mod = require('./mod') import mod from './mod.mjs'
导出语法 module.exports = { ... } export default ... / export { ... }
加载时机 同步,运行时加载 静态解析,编译时确定依赖;支持异步动态导入
值绑定 输出值的拷贝(原始类型),对象是引用 输出实时只读引用(live bindings)
顶层 await 不支持 支持(仅在 ES 模块中可用)
文件扩展名 通常 .js,可省略 .mjs.js(需 "type": "module"

在 Node.js 中,ES 模块可以导入 CommonJS 模块(当做默认导出),但反之不行——CommonJS 不能直接同步 require 一个 ES 模块,需要使用动态异步 import()

何时使用哪一种?

  • CommonJS:成熟的旧项目、许多 npm 包仍默认采用此规范,同步加载适合简单的服务器脚本。
  • ES 模块:新项目的首选标准,更好的静态分析能力,前端工程化统一(如 Webpack、Vite 深度支持),支持 顶层 await,是现代 JavaScript 的发展方向。

目前许多类库已同时提供两种导出方式。建议新应用从一开始就采用 ES 模块,并了解两者的转换限制。

总结与最佳实践

  • 理解 Node.js 运行时和模块化思想是构建一切应用的基础。
  • 掌握 CommonJS 的 require/module.exports 机制,尤其注意 exports 指向问题。
  • 熟练运用 ES 模块的 import/export,合理搭配命名与默认导出。
  • 在新项目中通过 package.json"type": "module" 明确使用 ES 模块;处理旧代码时注意互操作性。
  • 利用动态 import() 实现按需加载,优化应用启动性能。

模块系统的选择直接影响代码结构与维护成本。现在你已经掌握了 Node.js 模块的核心知识,可以动手拆分自己的代码,体验模块化带来的清晰与高效。