Skip to content

Node.js 模块化编程梳理与总结

如果说浏览器端的模块化是为了"按需加载",那么 Node.js 的模块化则是为了"代码组织"。作为将 JavaScript 带入服务器端的先驱,Node.js 经历了从 CommonJS (CJS) 独霸天下到 ES Modules (ESM) 全面接入的范式转移。

在服务器端,模块化不仅仅是为了拆分代码,更是为了构建清晰的项目结构、管理依赖关系,以及实现代码的复用与维护。

1. CommonJS 与 ES Modules 双模共存

在现代 Node.js 开发中,我们经常面临两种规范交织的情况。这不仅是文件后缀的改变,更是同步与异步加载逻辑的博弈。

共存策略:

  1. 后缀决定论.mjs 始终被视为 ESM,.cjs 始终被视为 CJS。
  2. 包定义导向:在 package.json 中设置 "type": "module",则目录下所有 .js 均视为 ESM,反之默认为 CJS。
  3. 互操作性(Interop)
    • ESM 引用 CJS:直接 import 即可,Node 会将 module.exports 包装为默认导出。
    • CJS 引用 ESM:由于 ESM 是异步加载的,CJS 无法使用 require,必须使用 动态 import:const module = await import('./es-module.mjs')

代码示例

javascript
// package.json
{
  "type": "module"
}

// interop.js (ESM 视角)
import { foo } from './es-lib.js';
import cjsDefault from './legacy-lib.cjs'; 

// interop.cjs (CJS 视角)
(async () => {
  const { bar } = await import('./modern-lib.mjs'); // 必须动态导入
  const legacy = require('./old-lib.cjs');
})();

2. Node.js 模块加载机制

Node.js 的 requireimport 并不是简单的文件读取,它背后有一套精密的查找优先级逻辑。

Node.js 的模块加载机制遵循以下规则:

  1. 核心模块:优先加载 Node.js 内置的核心模块,如 fspath,直接从内存加载。
  2. 文件模块:根据文件路径加载模块(如 ./module.js/absolute/path/module.js
  3. 目录模块:加载目录下的 index.jspackage.json 中指定的入口文件
  4. 第三方模块:如果既不是核心模块也不是路径,Node 会沿目录树向上逐级查找 node_modules 目录,直到找到或到达根目录。

关于缓存: Node.js 会缓存已加载的模块,避免重复加载。Node.js 会在第一次加载模块后将其放入 require.cache。这意味着同一进程内多次引入同一文件,得到的是同一个单例对象。在开发热更新工具时,清除缓存(delete require.cache[path])是核心操作。

javascript
// 清除模块缓存
delete require.cache[require.resolve('./module.js')];

3. 包管理与模块化

Node.js 项目通常使用 npm、 yarn、pnpm 等包管理工具管理模块依赖。在大型工程中,我们不再满足于单一的包管理,而是倾向于将多个关联包放在一个仓库中。

package.json 的进阶配置: 现代包通过 exports 字段提供更精细的控制,可以针对不同的引入方式(require 或 import)返回不同的入口文件:

json
"exports": {
  ".": {
    "import": "./dist/index.mjs",
    "require": "./dist/index.cjs"
  },
  "./feature": "./dist/feature.js"
}

Monorepo 选型: 虽然 Lerna 曾是标配,但在现代工作流中,pnpm workspaces 凭借其极速的硬链接机制和简洁的配置,已成为 AI 时代开发者(AI-Augmented Builders)的首选。

yaml
# pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'apps/*'

通过 Monorepo,你可以轻松地在 apps/web 中引用 packages/shared-utils,而无需频繁发布 npm 包。

4. 模块化架构:分层设计与关注点分离

随着后端逻辑变厚,单纯的模块拆分已不够,我们需要引入分层架构(Layered Architecture)。

  • API / Controller 层:负责解析请求、参数校验,不触及核心业务。
  • Service 层:系统的灵魂。存放纯粹的业务逻辑,可被多个接口复用。
  • Repository / DAO 层:负责与数据库或外部 API 通信,将数据操作抽象化。
  • Constants, / Types 层:定义全域共用的常量与 TypeScript 契约。

这种设计的精髓在于:哪怕你从 MySQL 换到了 MongoDB,理论上你只需要修改 Repository 层,而 Service 层的业务代码纹丝不动。

5. 结语

Node.js 的模块化实践,是一场从“能跑就行”到“优雅治理”的进化。我们从 CJS 与 ESM 的混战中学会了兼容的艺术,从加载机制中理解了性能的边界,又从 Monorepo 和分层架构中习得了掌控复杂系统的能力。

模块化不应是开发的负担,而应是你的“工程外骨骼”。一个设计良好的 Node.js 架构,应当像是一组排列整齐、接口严密的精密齿轮——每一个模块都深藏功与名,只通过最精简的接口与外界对话。

在编写下一行 import 之前,不妨停顿一秒:这个模块的边界清晰吗?它是否承担了不该属于它的因果?当你开始思考这些问题时,你已经从一个“写 Node.js 的人”,进化成为了一个真正的“后端架构守望者”。