3. 模块化初体验

134 阅读3分钟

一、模块化基础概念

1.1 什么是模块化

模块化是将程序分解为独立、可重用、可维护的代码单元的开发方式。Node.js 实现了完整的模块系统,解决了:

  • 变量污染问题
  • 代码组织问题
  • 依赖管理问题

1.2 Node.js 模块化发展历程

  1. CommonJS 规范(2009)
  2. ES Modules(2015)
  3. Node.js 对两种规范的支持演进

二、CommonJS 模块系统详解

2.1 核心机制

// 模块定义
module.exports = {
  // 导出内容
};

// 或
exports.key = value;

// 模块引入,推荐写相对路径
const mod = require('./module');

2.2 实现原理

Node.js 通过五个步骤处理模块:

  1. 路径解析:解析模块的绝对路径
  2. 缓存检查:检查 require.cache
  3. 文件加载:读取文件内容
  4. 包装执行:使用模块包装器
  5. 缓存存储:存入 require.cache

2.3 模块包装器

Node.js 实际执行的代码:

// 注意:require引入的是module.exports,所以使用exports = {}这种方式不对
// 为什么exports.key=value可以呢?因为module.exports和exports相等,
//所以exports.key=value把内容添加到了module.exports。
// 但是由于require就认module.exports,所以exports=这种写法不对
(function(module.exports, require, module, __filename, __dirname) {
  // 用户模块代码
});

2.4 完整加载流程

  1. 解析文件路径
  2. 检查核心模块
  3. 检查文件模块
  4. 检查目录模块
  5. 检查 node_modules
  6. 加载并执行

三、ES Modules 深度解析

3.1 基本语法

// 导出
export const name = 'value';
export default function() {};

// 导入
import { name } from './module.mjs';
import defExport from './module.mjs';

3.2 与 CommonJS 的本质区别

特性CommonJSES Modules
加载时机运行时动态加载编译时静态分析
绑定机制值拷贝实时绑定(live binding)
循环依赖处理部分支持完善支持
顶层 this指向 module.exportsundefined
解析方式同步异步

3.3 静态分析特性

ESM 的 import 语句:

  • 必须位于模块顶层
  • 不能动态生成路径
  • 会被 JavaScript 引擎静态分析

四、混合使用双模块系统

4.1 互操作方案

4.1.1 ESM 加载 CJS

// ESM 中
import cjsModule from './commonjs.cjs';
import { method } from './commonjs.cjs'; // 注意命名导入的限制

4.1.2 CJS 加载 ESM

// CJS 中必须使用动态导入
(async () => {
  const esmModule = await import('./esm.mjs');
})();

4.2 配置方案

4.2.1 package.json 配置

{
  "type": "module",  // 默认使用 ESM
  "main": "./index.cjs",  // CJS 入口
  "exports": {
    ".": {
      "require": "./index.cjs",  // CJS 入口
      "import": "./index.mjs"   // ESM 入口
    }
  }
}

4.2.2 文件扩展名策略

  • .js:根据 package.json 的 type 决定
  • .cjs:强制 CommonJS
  • .mjs:强制 ESM

五、高级模块特性

5.1 条件导出

{
  "exports": {
    ".": {
      "require": "./cjs/index.js",
      "import": "./esm/index.js",
      "node": "./node-specific.js",
      "default": "./fallback.js"
    }
  }
}

5.2 子路径导出

{
  "exports": {
    "./feature": "./src/feature.js"
  }
}

5.3 加载器钩子(实验性)

// 自定义模块加载行为
import { createRequire } from 'module';
const require = createRequire(import.meta.url);

六、模块解析算法详解

6.1 CommonJS 解析顺序

  1. 精确文件名匹配
  2. 尝试添加 .js
  3. 尝试添加 .json
  4. 尝试添加 .node
  5. 作为目录解析

6.2 ESM 解析差异

  • 必须明确文件扩展名
  • 支持 URL 风格的路径
  • 遵循浏览器解析规则

七、性能优化建议

  1. 模块缓存:理解 require.cache 机制
  2. 循环依赖:避免复杂的相互依赖
  3. 模块拆分:合理划分模块粒度
  4. 预加载:使用 --require 标志预加载模块

八、调试技巧

8.1 查看模块缓存

console.log(require.cache);

8.2 查看模块解析过程

node --inspect-brk app.js
# 然后调试 require 调用栈

8.3 查看实际加载模块

node -r trace-modules app.js

九、未来发展趋势

  1. ESM 成为标准:Node.js 正在向 ESM 迁移
  2. 导入映射(Import Maps) :浏览器风格的依赖解析
  3. Wasm 模块支持:更高效的模块加载
  4. 更细粒度的模块加载:按需加载模块部分内容

十、最佳实践总结

  1. 新项目优先使用 ESM
  2. 库开发者应提供双模式支持
  3. 保持模块单一职责
  4. 合理使用动态导入
  5. 注意浏览器兼容性需求
  6. 利用 tree-shaking 优化 ESM