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() 函数按照以下顺序解析模块:
- 核心模块:如
fs、path、http等 - 文件模块:以
./或../开头的相对路径 - 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 字段模块缓存机制
console.log('模块 A 被加载');module.exports = { name: 'Module A' };
// main.jsconst 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 中容易混淆的概念:
// 正确使用 exportsexports.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 模块系统中,这三者之间存在紧密的关系:
基本关系
module.exports- 真正的导出对象exports-module.exports的引用别名this- 在模块顶层指向exports
// 在模块顶层console.log(this === exports); // trueconsole.log(this === module.exports); // trueconsole.log(exports === module.exports); // true工作原理
CommonJS实际上是这样包装模块的:
function(exports, require, module, __filename, __dirname) { // 你的模块代码在这里 // this = exports = module.exports (初始状态)}使用区别
正确用法:
// 方式1:使用 module.exportsmodule.exports = { name: 'test', fn: function() {}};
// 方式2:向 exports 添加属性exports.name = 'test';exports.fn = function() {};
// 方式3:使用 this(不推荐)this.name = 'test';错误用法:
// ❌ 错误:重新赋值 exports 会断开引用exports = { name: 'test'}; // 这不会导出任何东西
// ✅ 正确:应该使用 module.exportsmodule.exports = { name: 'test'};关键点
exports只是module.exports的引用,重新赋值会断开引用- 最终导出的总是
module.exports的值 this在模块顶层等同于exports,但在函数内部可能指向其他对象
循环依赖处理
CommonJS 可以处理循环依赖,但需要注意执行顺序:
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.jsconsole.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.jsconst 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实战示例
创建一个简单的工具库
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.jsfunction 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.jsconst math = require('./math');const string = require('./string');
module.exports = { math, string};
// main.jsconst utils = require('./utils');const { math, string } = require('./utils');
console.log(utils.math.add(1, 2)); // 3console.log(math.factorial(5)); // 120console.log(string.capitalize('hello')); // Hello配置管理模块
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.jsconst config = require('./config');console.log(`Server running on port ${config.port}`);CommonJS vs ES Modules
| 特性 | CommonJS | ES Modules |
|---|---|---|
| 语法 | require/module.exports | import/export |
| 加载时机 | 运行时 | 编译时 |
| 加载方式 | 同步 | 异步 |
| 动态导入 | 支持 | 需要 import() |
| 浏览器支持 | 需要打包 | 原生支持 |
| Node.js | 默认支持 | 需要配置 |
// CommonJSconst fs = require('fs');if (condition) { const module = require('./conditional-module');}
// ES Modulesimport fs from 'fs';// 条件导入需要使用动态 importif (condition) { const module = await import('./conditional-module');}最佳实践
1. 模块设计原则
// 好的模块设计 - 单一职责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. 错误处理
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 应用程序。