CommonJS基础
CommonJS是Node.js采用的模块系统,也是JavaScript模块化历史上最重要的规范之一。虽然ES模块是现代标准,但理解CommonJS仍然至关重要,因为大量的Node.js代码和npm包仍在使用这种模块系统。
什么是CommonJS
背景和历史
CommonJS规范于2009年诞生,目标是为JavaScript提供一个服务器端的模块系统。在ES6模块出现之前,CommonJS是JavaScript模块化的事实标准。
// CommonJS的核心理念
// 1. 每个文件都是一个模块
// 2. 模块内的变量和函数默认是私有的
// 3. 通过module.exports导出
// 4. 通过require()导入
// 5. 同步加载模块
核心特性
- 同步加载: 模块在require时同步加载和执行
- 缓存机制: 模块只执行一次,后续require返回缓存结果
- 动态加载: 可以在运行时动态require模块
- 简单易用: 语法简洁,学习成本低
- Node.js原生支持: Node.js内置支持,无需额外配置
基本语法
1. 模块导出
// math.js - 基本导出示例
// 方式1: 直接给exports添加属性
exports.add = function(a, b) {
return a + b;
};
exports.subtract = function(a, b) {
return a - b;
};
exports.PI = 3.14159;
// 方式2: 使用module.exports
module.exports.multiply = function(a, b) {
return a * b;
};
// 方式3: 整体替换module.exports
module.exports = {
divide: function(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
},
power: function(base, exponent) {
return Math.pow(base, exponent);
}
};
// 注意: 一旦整体替换module.exports,之前的exports.*都会失效
2. 模块导入
// app.js - 基本导入示例
// 导入整个模块
const math = require('./math');
console.log(math.add(2, 3)); // 5
// 解构导入特定函数
const { add, subtract, PI } = require('./math');
console.log(add(5, 3)); // 8
console.log(PI); // 3.14159
// 导入并重命名
const { add: sum, subtract: diff } = require('./math');
console.log(sum(10, 5)); // 15
console.log(diff(10, 5)); // 5
// 导入内置模块
const fs = require('fs');
const path = require('path');
const http = require('http');
// 导入npm包
const lodash = require('lodash');
const express = require('express');
3. 不同的导出模式
// user.js - 类导出示例
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
getInfo() {
return `${this.name} <${this.email}>`;
}
}
// 导出类
module.exports = User;
// config.js - 对象导出示例
const config = {
database: {
host: 'localhost',
port: 5432,
name: 'myapp'
},
server: {
port: 3000,
env: process.env.NODE_ENV || 'development'
},
secrets: {
jwtSecret: process.env.JWT_SECRET || 'default-secret'
}
};
module.exports = config;
// utils.js - 函数集合导出
function formatDate(date) {
return date.toISOString().split('T')[0];
}
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function generateId() {
return Math.random().toString(36).substr(2, 9);
}
// 批量导出
module.exports = {
formatDate,
isValidEmail,
generateId
};
// logger.js - 单函数导出
function createLogger(level = 'info') {
return {
info: (message) => console.log(`[INFO] ${message}`),
warn: (message) => console.warn(`[WARN] ${message}`),
error: (message) => console.error(`[ERROR] ${message}`)
};
}
module.exports = createLogger;
exports vs module.exports
理解两者的关系
// 理解exports和module.exports的关系
// Node.js内部的模块包装器大致如下:
function wrapModule(moduleCode) {
return `
(function(exports, require, module, __filename, __dirname) {
${moduleCode}
return module.exports;
});
`;
}
// 初始状态下:exports === module.exports
console.log(exports === module.exports); // true
// exports是module.exports的引用
// 所以下面两种写法是等价的:
exports.hello = 'world';
module.exports.hello = 'world';
常见陷阱
// trap.js - 常见陷阱示例
// ❌ 错误用法1: 直接给exports赋值
exports = {
name: 'John',
age: 30
};
// 这样做只是改变了exports的指向,不会影响module.exports
// ❌ 错误用法2: 混用exports和module.exports
exports.method1 = function() { return 'method1'; };
module.exports = {
method2: function() { return 'method2'; }
};
// module.exports会覆盖exports的设置
// ✅ 正确用法1: 始终使用exports添加属性
exports.method1 = function() { return 'method1'; };
exports.method2 = function() { return 'method2'; };
// ✅ 正确用法2: 始终使用module.exports
module.exports = {
method1: function() { return 'method1'; },
method2: function() { return 'method2'; }
};
// ✅ 正确用法3: 先设置module.exports,再使用exports
module.exports = {};
exports.method1 = function() { return 'method1'; };
exports.method2 = function() { return 'method2'; };
模块加载机制
1. 同步加载
// sync-loading.js
console.log('Before require');
// require是同步的,会阻塞代码执行
const largeModule = require('./large-module');
console.log('After require');
// large-module.js
console.log('Large module loading...');
// 模拟大量计算
for (let i = 0; i < 1000000; i++) {
// 大量计算
}
console.log('Large module loaded');
module.exports = {
data: 'some data'
};
// 输出顺序:
// Before require
// Large module loading...
// Large module loaded
// After require
2. 模块缓存
// cache-demo.js
// 第一次require会执行模块代码
const module1 = require('./counter');
console.log('First require:', module1.getCount()); // 0
module1.increment();
console.log('After increment:', module1.getCount()); // 1
// 第二次require返回缓存的模块
const module2 = require('./counter');
console.log('Second require:', module2.getCount()); // 1 (不是0!)
console.log(module1 === module2); // true (同一个对象)
// counter.js
console.log('Counter module executing...');
let count = 0;
module.exports = {
increment() {
count++;
},
getCount() {
return count;
}
};
3. 清除模块缓存
// cache-management.js
// 查看模块缓存
console.log('Cached modules:', Object.keys(require.cache));
// 加载模块
const myModule = require('./my-module');
// 清除特定模块缓存
delete require.cache[require.resolve('./my-module')];
// 重新加载模块(会重新执行模块代码)
const reloadedModule = require('./my-module');
// 清除所有缓存(不建议在生产环境使用)
function clearAllCache() {
Object.keys(require.cache).forEach(key => {
delete require.cache[key];
});
}
// 安全的模块重载函数
function safeReload(modulePath) {
try {
// 解析模块路径
const resolvedPath = require.resolve(modulePath);
// 删除缓存
delete require.cache[resolvedPath];
// 重新加载
return require(modulePath);
} catch (error) {
console.error('Failed to reload module:', error);
return null;
}
}
模块解析算法
1. 解析规则
// Node.js模块解析算法示例
// 1. 核心模块(优先级最高)
const fs = require('fs'); // 直接加载Node.js内置模块
const path = require('path'); // 内置模块不需要路径
// 2. 相对路径模块
const myModule = require('./my-module'); // ./my-module.js
const subModule = require('./sub/module'); // ./sub/module.js
const parentModule = require('../parent'); // ../parent.js
// 3. 绝对路径模块
const absoluteModule = require('/home/user/app/module');
// 4. node_modules中的模块
const lodash = require('lodash'); // node_modules/lodash
const express = require('express'); // node_modules/express
// Node.js查找node_modules的顺序:
// ./node_modules/module-name
// ../node_modules/module-name
// ../../node_modules/module-name
// ... 一直到文件系统根目录
2. 文件扩展名解析
// extension-resolution.js
// Node.js会按以下顺序尝试扩展名:
// require('./module') 会依次尝试:
// 1. ./module.js
// 2. ./module.json
// 3. ./module.node
// 如果上述都不存在,会尝试目录:
// 4. ./module/package.json (查找main字段)
// 5. ./module/index.js
// 6. ./module/index.json
// 7. ./module/index.node
// 演示不同类型的模块加载
const jsModule = require('./utils'); // utils.js
const jsonConfig = require('./config'); // config.json
const packageModule = require('./my-package'); // my-package/index.js
// config.json
{
"name": "my-app",
"version": "1.0.0",
"database": {
"host": "localhost",
"port": 5432
}
}
// my-package/package.json
{
"name": "my-package",
"main": "lib/index.js"
}
3. package.json的作用
// package-resolution.js
// package.json示例
{
"name": "my-library",
"version": "1.0.0",
"main": "dist/index.js", // CommonJS入口
"module": "dist/index.esm.js", // ES模块入口
"exports": { // 新的导出字段
".": {
"require": "./dist/index.js",
"import": "./dist/index.esm.js"
},
"./utils": "./dist/utils.js"
},
"files": ["dist/"]
}
// 当require('my-library')时,Node.js会:
// 1. 查找node_modules/my-library/package.json
// 2. 读取main字段值:dist/index.js
// 3. 加载node_modules/my-library/dist/index.js
CommonJS的高级特性
1. 动态require
// dynamic-require.js
// 基于条件的动态加载
const environment = process.env.NODE_ENV || 'development';
let config;
if (environment === 'production') {
config = require('./config/production');
} else if (environment === 'test') {
config = require('./config/test');
} else {
config = require('./config/development');
}
// 基于用户输入的动态加载
function loadPlugin(pluginName) {
try {
const plugin = require(`./plugins/${pluginName}`);
return plugin;
} catch (error) {
console.error(`Failed to load plugin: ${pluginName}`, error);
return null;
}
}
// 批量加载模块
function loadModules(moduleNames) {
const modules = {};
moduleNames.forEach(name => {
try {
modules[name] = require(`./modules/${name}`);
} catch (error) {
console.warn(`Failed to load module: ${name}`);
}
});
return modules;
}
// 使用示例
const plugins = ['auth', 'logger', 'database'];
const loadedModules = loadModules(plugins);
2. 条件导出
// conditional-exports.js
// 基于环境的条件导出
if (process.env.NODE_ENV === 'development') {
// 开发环境:导出详细的调试版本
module.exports = {
log: console.log,
debug: console.debug,
warn: console.warn,
error: console.error,
trace: console.trace
};
} else {
// 生产环境:导出简化版本
module.exports = {
log: () => {}, // 空操作
debug: () => {},
warn: console.warn,
error: console.error,
trace: () => {}
};
}
// 基于功能检测的条件导出
const hasFileSystem = (() => {
try {
require('fs');
return true;
} catch {
return false;
}
})();
if (hasFileSystem) {
module.exports = require('./file-storage');
} else {
module.exports = require('./memory-storage');
}
3. 模块工厂模式
// factory-pattern.js
// 模块工厂:根据参数创建不同的实例
function createLogger(options = {}) {
const {
level = 'info',
prefix = '',
timestamp = true
} = options;
const levels = {
debug: 0,
info: 1,
warn: 2,
error: 3
};
const currentLevel = levels[level] || 1;
function log(targetLevel, message) {
if (levels[targetLevel] >= currentLevel) {
const time = timestamp ? new Date().toISOString() : '';
const fullMessage = `${time} ${prefix} [${targetLevel.toUpperCase()}] ${message}`;
console.log(fullMessage);
}
}
return {
debug: (msg) => log('debug', msg),
info: (msg) => log('info', msg),
warn: (msg) => log('warn', msg),
error: (msg) => log('error', msg)
};
}
module.exports = createLogger;
// 使用工厂模式
const logger = require('./logger-factory')({
level: 'debug',
prefix: '[MyApp]',
timestamp: true
});
logger.info('Application started');
4. 模块单例模式
// singleton-pattern.js
// 单例数据库连接
class Database {
constructor() {
if (Database.instance) {
return Database.instance;
}
this.connection = null;
this.connected = false;
Database.instance = this;
}
connect(connectionString) {
if (!this.connected) {
console.log('Connecting to database...');
this.connection = { connectionString };
this.connected = true;
}
return this.connection;
}
query(sql) {
if (!this.connected) {
throw new Error('Database not connected');
}
console.log(`Executing query: ${sql}`);
return Promise.resolve([]);
}
}
// 导出单例实例
module.exports = new Database();
// 在任何地方使用都是同一个实例
const db1 = require('./database');
const db2 = require('./database');
console.log(db1 === db2); // true
Node.js特有功能
1. 全局变量
// globals-demo.js
console.log('Module information:');
console.log('__filename:', __filename); // 当前文件的绝对路径
console.log('__dirname:', __dirname); // 当前目录的绝对路径
console.log('module.id:', module.id); // 模块标识符
console.log('module.filename:', module.filename); // 同__filename
console.log('module.loaded:', module.loaded); // 模块是否已加载完成
console.log('module.parent:', module.parent); // 父模块
console.log('module.children:', module.children); // 子模块列表
// require对象的属性
console.log('require.main:', require.main); // 主模块
console.log('require.cache keys:', Object.keys(require.cache)); // 模块缓存
console.log('require.resolve("./utils"):', require.resolve('./utils')); // 解析模块路径
// 实用的路径操作
const path = require('path');
// 获取当前模块所在目录的其他文件
const configPath = path.join(__dirname, 'config.json');
const utilsPath = path.join(__dirname, 'utils', 'helpers.js');
console.log('Config path:', configPath);
console.log('Utils path:', utilsPath);
2. require.resolve
// resolve-demo.js
// require.resolve返回模块的绝对路径,但不加载模块
const modulePath = require.resolve('./my-module');
console.log('Module path:', modulePath);
// 检查模块是否存在
function moduleExists(moduleName) {
try {
require.resolve(moduleName);
return true;
} catch (error) {
return false;
}
}
console.log('lodash exists:', moduleExists('lodash'));
console.log('non-existent exists:', moduleExists('non-existent-module'));
// 解析npm包的子模块
const lodashMapPath = require.resolve('lodash/map');
console.log('Lodash map path:', lodashMapPath);
// 动态加载可选依赖
function loadOptionalModule(moduleName) {
try {
const modulePath = require.resolve(moduleName);
return require(modulePath);
} catch (error) {
console.warn(`Optional module ${moduleName} not found`);
return null;
}
}
const optionalModule = loadOptionalModule('optional-dependency');
3. 模块热重载
// hot-reload.js
const fs = require('fs');
const path = require('path');
class ModuleHotReloader {
constructor() {
this.watchers = new Map();
}
watch(modulePath, callback) {
const absolutePath = require.resolve(modulePath);
if (this.watchers.has(absolutePath)) {
return;
}
const watcher = fs.watchFile(absolutePath, (curr, prev) => {
if (curr.mtime > prev.mtime) {
console.log(`Module ${modulePath} changed, reloading...`);
// 清除缓存
delete require.cache[absolutePath];
// 重新加载
try {
const reloadedModule = require(modulePath);
callback(reloadedModule);
} catch (error) {
console.error('Failed to reload module:', error);
}
}
});
this.watchers.set(absolutePath, watcher);
}
unwatch(modulePath) {
const absolutePath = require.resolve(modulePath);
const watcher = this.watchers.get(absolutePath);
if (watcher) {
fs.unwatchFile(absolutePath);
this.watchers.delete(absolutePath);
}
}
unwatchAll() {
this.watchers.forEach((watcher, path) => {
fs.unwatchFile(path);
});
this.watchers.clear();
}
}
// 使用示例
const reloader = new ModuleHotReloader();
reloader.watch('./config', (newConfig) => {
console.log('Config reloaded:', newConfig);
// 更新应用配置
});
性能优化
1. 延迟加载
// lazy-loading.js
// 延迟加载大型模块
class LazyLoader {
constructor() {
this._cache = new Map();
}
lazy(modulePath) {
return new Proxy({}, {
get: (target, prop) => {
if (!this._cache.has(modulePath)) {
console.log(`Lazy loading: ${modulePath}`);
this._cache.set(modulePath, require(modulePath));
}
const module = this._cache.get(modulePath);
return module[prop];
}
});
}
}
const loader = new LazyLoader();
// 只有在第一次访问时才会加载模块
const heavyModule = loader.lazy('./heavy-computation');
// 这行代码才会触发模块加载
console.log(heavyModule.calculate(100));
2. 模块预加载
// preload.js
class ModulePreloader {
constructor() {
this.preloaded = new Map();
}
preload(modules) {
modules.forEach(modulePath => {
setImmediate(() => {
try {
this.preloaded.set(modulePath, require(modulePath));
console.log(`Preloaded: ${modulePath}`);
} catch (error) {
console.warn(`Failed to preload: ${modulePath}`, error);
}
});
});
}
get(modulePath) {
if (this.preloaded.has(modulePath)) {
return this.preloaded.get(modulePath);
}
return require(modulePath);
}
}
// 应用启动时预加载常用模块
const preloader = new ModulePreloader();
preloader.preload([
'lodash',
'moment',
'./utils/helpers',
'./config/database'
]);
// 后续使用预加载的模块
setTimeout(() => {
const lodash = preloader.get('lodash');
const helpers = preloader.get('./utils/helpers');
}, 1000);
最佳实践
1. 模块导出策略
// export-strategies.js
// ✅ 推荐:明确的导出
module.exports = {
// 明确列出所有导出
createUser,
updateUser,
deleteUser,
UserValidationError
};
// ✅ 推荐:单一职责导出
module.exports = class UserService {
constructor(database) {
this.db = database;
}
async createUser(userData) {
// 实现
}
};
// ❌ 避免:混用exports和module.exports
exports.method1 = () => {};
module.exports.method2 = () => {}; // 不一致
// ❌ 避免:导出过多内容
module.exports = {
// 导出了太多不相关的内容
UserService,
ProductService,
OrderService,
DatabaseConnection,
Logger,
Config,
Utils
};
2. 错误处理
// error-handling.js
// ✅ 推荐:优雅的错误处理
function safeRequire(modulePath, fallback = null) {
try {
return require(modulePath);
} catch (error) {
if (error.code === 'MODULE_NOT_FOUND') {
console.warn(`Module not found: ${modulePath}`);
return fallback;
}
throw error; // 重新抛出其他类型的错误
}
}
// 使用示例
const optionalConfig = safeRequire('./optional-config', {});
const logger = safeRequire('./logger', console);
// ✅ 推荐:模块初始化错误处理
function createDatabaseModule() {
let connection = null;
function connect() {
if (!connection) {
try {
connection = require('./database-connection')();
} catch (error) {
console.error('Failed to initialize database:', error);
throw new Error('Database module initialization failed');
}
}
return connection;
}
return {
connect,
query: (sql) => connect().query(sql)
};
}
module.exports = createDatabaseModule();
3. 模块测试
// testable-module.js
// ✅ 可测试的模块设计
class UserService {
constructor(dependencies = {}) {
// 依赖注入,便于测试
this.database = dependencies.database || require('./database');
this.logger = dependencies.logger || require('./logger');
this.emailService = dependencies.emailService || require('./email-service');
}
async createUser(userData) {
try {
// 验证数据
this.validateUserData(userData);
// 创建用户
const user = await this.database.users.create(userData);
// 发送欢迎邮件
await this.emailService.sendWelcomeEmail(user.email);
this.logger.info(`User created: ${user.id}`);
return user;
} catch (error) {
this.logger.error('Failed to create user:', error);
throw error;
}
}
validateUserData(userData) {
if (!userData.email) {
throw new Error('Email is required');
}
// 更多验证逻辑
}
}
module.exports = UserService;
// user-service.test.js
const UserService = require('./user-service');
describe('UserService', () => {
it('should create user successfully', async () => {
// 模拟依赖
const mockDatabase = {
users: {
create: jest.fn().mockResolvedValue({ id: 1, email: 'test@example.com' })
}
};
const mockLogger = {
info: jest.fn(),
error: jest.fn()
};
const mockEmailService = {
sendWelcomeEmail: jest.fn().mockResolvedValue(true)
};
// 注入模拟依赖
const userService = new UserService({
database: mockDatabase,
logger: mockLogger,
emailService: mockEmailService
});
const userData = { email: 'test@example.com', name: 'Test User' };
const result = await userService.createUser(userData);
expect(result.id).toBe(1);
expect(mockDatabase.users.create).toHaveBeenCalledWith(userData);
expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith('test@example.com');
});
});
总结
CommonJS作为Node.js的原生模块系统,具有以下特点:
- ✅ 简单易用: 语法简洁,学习成本低
- ✅ 同步加载: 适合服务器端开发,模块加载可预期
- ✅ 动态特性: 支持运行时动态加载模块
- ✅ 成熟生态: 大量npm包基于CommonJS构建
- ✅ 工具支持: 丰富的开发工具和调试支持
理解CommonJS对于Node.js开发和理解JavaScript模块化演进历程都非常重要。虽然ES模块是未来趋势,但CommonJS仍在现代开发中扮演重要角色。
下一章: require与module.exports →