1780 字
9 分钟
CommonJS

CommonJS 模块系统详解#

CommonJS 是 Node.js 采用的模块系统,它定义了模块的加载、导出和依赖管理机制。本文将从 require() 函数开始,深入解析 CommonJS 的工作原理。

什么是 CommonJS#

CommonJS 是一个项目,旨在为 JavaScript 定义通用的模块和包规范。它最初是为服务器端 JavaScript 环境设计的,Node.js 是其最著名的实现。

核心特点#

  • 同步加载:模块在运行时同步加载
  • 单例模式:模块只会被加载一次,后续 require 返回缓存
  • 动态加载:可以在运行时根据条件加载模块
  • 文件作用域:每个文件都是一个独立的模块作用域

基本语法#

导出模块#

CommonJS 提供了多种导出方式:

// 1. module.exports 导出单个值
module.exports = function add(a, b) {
return a + b;
};
// 2. module.exports 导出对象
module.exports = {
add: function(a, b) { return a + b; },
subtract: function(a, b) { return a - b; }
};
// 3. exports 快捷方式(注意:不能重新赋值)
exports.multiply = function(a, b) {
return a * b;
};
exports.divide = function(a, b) {
return a / b;
};

导入模块#

// 1. 导入整个模块
const math = require('./math');
const add = math.add;
// 2. 解构导入
const { add, subtract } = require('./math');
// 3. 导入核心模块
const fs = require('fs');
const path = require('path');
// 4. 导入第三方模块
const lodash = require('lodash');
const express = require('express');

require() 函数详解#

模块解析规则#

require() 函数按照以下顺序解析模块:

  1. 核心模块:如 fspathhttp
  2. 文件模块:以 ./../ 开头的相对路径
  3. node_modules:在当前目录及父目录的 node_modules 中查找
// 核心模块
require('fs')
// 文件模块
require('./utils') // 当前目录
require('../config') // 父目录
require('/absolute/path') // 绝对路径
// node_modules 模块
require('express') // 第三方包

文件扩展名解析#

require() 会按顺序尝试以下扩展名:

require('./module')
// 依次尝试:
// 1. ./module.js
// 2. ./module.json
// 3. ./module.node
// 4. ./module/index.js
// 5. ./module/package.json 中的 main 字段

模块缓存机制#

moduleA.js
console.log('模块 A 被加载');
module.exports = { name: 'Module A' };
// main.js
const moduleA1 = require('./moduleA'); // 输出: 模块 A 被加载
const moduleA2 = require('./moduleA'); // 不会再次输出
console.log(moduleA1 === moduleA2); // true - 同一个对象引用

深入理解模块包装#

Node.js 会将每个模块包装在一个函数中:

// 你写的代码
const math = require('./math');
exports.calculate = function() {
return math.add(1, 2);
};
// Node.js 实际执行的代码
(function(exports, require, module, __filename, __dirname) {
const math = require('./math');
exports.calculate = function() {
return math.add(1, 2);
};
});

模块作用域变量#

每个模块都可以访问这些变量:

console.log(__filename); // 当前文件的绝对路径
console.log(__dirname); // 当前文件所在目录的绝对路径
console.log(module); // 当前模块对象
console.log(exports); // module.exports 的引用
console.log(require); // require 函数

exports vs module.exports#

这是 CommonJS 中容易混淆的概念:

// 正确使用 exports
exports.foo = 'bar';
exports.method = function() {};
// 错误!这会断开 exports 与 module.exports 的链接
exports = { foo: 'bar' }; // ❌
// 正确的做法
module.exports = { foo: 'bar' }; // ✅
// 理解原理
// exports 只是 module.exports 的引用
// exports = module.exports = {}

module.exports、exports 与 this 的关系#

在 CommonJS 模块系统中,这三者之间存在紧密的关系:

基本关系#

  1. module.exports - 真正的导出对象
  2. exports - module.exports的引用别名
  3. this - 在模块顶层指向exports
// 在模块顶层
console.log(this === exports); // true
console.log(this === module.exports); // true
console.log(exports === module.exports); // true

工作原理#

CommonJS实际上是这样包装模块的:

function(exports, require, module, __filename, __dirname) {
// 你的模块代码在这里
// this = exports = module.exports (初始状态)
}

使用区别#

正确用法:

// 方式1:使用 module.exports
module.exports = {
name: 'test',
fn: function() {}
};
// 方式2:向 exports 添加属性
exports.name = 'test';
exports.fn = function() {};
// 方式3:使用 this(不推荐)
this.name = 'test';

错误用法:

// ❌ 错误:重新赋值 exports 会断开引用
exports = {
name: 'test'
}; // 这不会导出任何东西
// ✅ 正确:应该使用 module.exports
module.exports = {
name: 'test'
};

关键点#

  • exports只是module.exports的引用,重新赋值会断开引用
  • 最终导出的总是module.exports的值
  • this在模块顶层等同于exports,但在函数内部可能指向其他对象

循环依赖处理#

CommonJS 可以处理循环依赖,但需要注意执行顺序:

a.js
console.log('a starting');
exports.done = false;
const b = require('./b');
console.log('in a, b.done =', b.done);
exports.done = true;
console.log('a done');
// b.js
console.log('b starting');
exports.done = false;
const a = require('./a');
console.log('in b, a.done =', a.done);
exports.done = true;
console.log('b done');
// main.js
const a = require('./a');
const b = require('./b');
// 输出:
// a starting
// b starting
// in b, a.done = false
// b done
// in a, b.done = true
// a done

实战示例#

创建一个简单的工具库#

utils/math.js
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
module.exports = {
add,
multiply,
factorial
};
// utils/string.js
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function reverse(str) {
return str.split('').reverse().join('');
}
exports.capitalize = capitalize;
exports.reverse = reverse;
// utils/index.js
const math = require('./math');
const string = require('./string');
module.exports = {
math,
string
};
// main.js
const utils = require('./utils');
const { math, string } = require('./utils');
console.log(utils.math.add(1, 2)); // 3
console.log(math.factorial(5)); // 120
console.log(string.capitalize('hello')); // Hello

配置管理模块#

config.js
const path = require('path');
const config = {
development: {
port: 3000,
database: {
host: 'localhost',
port: 5432,
name: 'myapp_dev'
}
},
production: {
port: process.env.PORT || 8080,
database: {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
name: process.env.DB_NAME
}
}
};
const env = process.env.NODE_ENV || 'development';
module.exports = config[env];
// app.js
const config = require('./config');
console.log(`Server running on port ${config.port}`);

CommonJS vs ES Modules#

特性CommonJSES Modules
语法require/module.exportsimport/export
加载时机运行时编译时
加载方式同步异步
动态导入支持需要 import()
浏览器支持需要打包原生支持
Node.js默认支持需要配置
// CommonJS
const fs = require('fs');
if (condition) {
const module = require('./conditional-module');
}
// ES Modules
import fs from 'fs';
// 条件导入需要使用动态 import
if (condition) {
const module = await import('./conditional-module');
}

最佳实践#

1. 模块设计原则#

logger.js
// 好的模块设计 - 单一职责
const fs = require('fs');
function log(message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
fs.appendFileSync('app.log', logMessage);
}
module.exports = { log };
// 避免导出过多功能
// bad-module.js ❌
module.exports = {
log: () => {},
parseXML: () => {},
sendEmail: () => {},
validatePassword: () => {}
};

2. 避免全局状态#

// config-manager.js ✅
function createConfig(options = {}) {
return {
port: options.port || 3000,
host: options.host || 'localhost',
get: function(key) {
return this[key];
}
};
}
module.exports = { createConfig };
// 避免直接导出可变状态 ❌
let globalState = {};
module.exports = globalState;

3. 错误处理#

file-utils.js
const fs = require('fs');
function readFileSync(filePath) {
try {
return fs.readFileSync(filePath, 'utf8');
} catch (error) {
throw new Error(`Failed to read file ${filePath}: ${error.message}`);
}
}
function writeFileSync(filePath, data) {
try {
fs.writeFileSync(filePath, data, 'utf8');
} catch (error) {
throw new Error(`Failed to write file ${filePath}: ${error.message}`);
}
}
module.exports = {
readFileSync,
writeFileSync
};

调试技巧#

1. 查看模块缓存#

// 查看已加载的模块
console.log(Object.keys(require.cache));
// 清除模块缓存(开发环境)
delete require.cache[require.resolve('./my-module')];

2. 模块解析调试#

// 查看模块解析路径
console.log(require.resolve('./my-module'));
console.log(require.resolve.paths('my-module'));

总结#

CommonJS 是 Node.js 生态系统的基础,理解其工作原理对于 Node.js 开发至关重要。虽然 ES Modules 正在逐渐普及,但 CommonJS 在很长一段时间内仍将是 Node.js 的重要组成部分。

掌握 CommonJS 的关键点:

  • 理解 require() 的模块解析机制
  • 区分 exports 和 module.exports
  • 了解模块缓存和循环依赖处理
  • 遵循模块设计最佳实践

通过深入理解这些概念,你将能够更好地构建和维护 Node.js 应用程序。

CommonJS
https://fuwari.vercel.app/posts/commonjs/
作者
Lorem Ipsum
发布于
2025-09-23
许可协议
CC BY-NC-SA 4.0