CommonJS模块默认是单例,因Node.js首次require时执行并缓存module.exports,后续同路径require直接返回该缓存实例,内存地址一致,确保全局唯一性。

在 Node.js 的 CommonJS 模块系统中,日志模块天然就是全局单例——只要模块路径相同,require() 返回的始终是同一个模块实例。这正是 CommonJS 的缓存机制(require.cache)决定的,无需手动实现单例模式。
为什么 CommonJS 模块默认就是单例?
Node.js 在第一次 require('./logger') 时会执行模块代码、导出对象,并将该导出对象缓存在 require.cache 中;后续同路径的 require 直接返回缓存对象,内存地址完全一致。这意味着:
- 所有文件
require('./logger')得到的是同一份module.exports - 模块内部变量(如日志级别、格式器、写入流)只初始化一次
- 对导出对象的任何修改(如
logger.level = 'debug')全局可见
如何正确设计一个可复用的日志模块?
关键不是“实现单例”,而是避免意外打破单例性。推荐做法:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 导出已实例化的对象:不要导出类或工厂函数(除非显式需要多实例),直接导出配置好的 logger 实例
-
延迟初始化内部状态:把文件流、进程 ID 等依赖运行时环境的初始化放在首次调用方法时(如
logger.info()),而非模块顶层 -
避免模块顶层副作用污染:不直接调用
console.log或修改全局,确保模块加载时不触发实际日志输出 -
支持简单配置但不破坏单例:提供
logger.configure({ level: 'warn' })方法,只更新已有实例属性
一个安全的 logger.js 示例
(使用原生 fs.createWriteStream,无第三方依赖)
立即学习“Java免费学习笔记(深入)”;
// logger.js
const fs = require('fs');
const path = require('path');
let instance = null;
function createLogger() {
const stream = fs.createWriteStream(
path.join(__dirname, 'app.log'),
{ flags: 'a' }
);
return {
level: 'info',
configure(options) {
Object.assign(this, options);
return this;
},
log(level, msg) {
if (['debug', 'info', 'warn', 'error'].indexOf(level) <
['debug', 'info', 'warn', 'error'].indexOf(this.level)) {
return;
}
const line = `[${new Date().toISOString()}] ${level.toUpperCase()}: ${msg}\n`;
stream.write(line);
},
info(msg) { this.log('info', msg); },
warn(msg) { this.log('warn', msg); },
error(msg) { this.log('error', msg); }
};
}
// 模块顶层只导出 getter,确保单例
module.exports = {
get() {
if (!instance) {
instance = createLogger();
}
return instance;
}
};
// ✅ 使用方式(任意文件中):
// const logger = require('./logger').get();
// logger.info('server started');
常见陷阱与规避方式
-
错误:导出 class,每次 new 一个新实例 → 改为导出单例对象或用
get()工厂 -
错误:用绝对路径 + 时间戳拼接模块名(如
./logger?${Date.now()}) → 破坏缓存,生成多个实例 -
错误:在模块顶层执行异步初始化(如
await fs.access()) → CommonJS 不支持顶层 await,会报错或阻塞 -
注意:ESM(import/export)不共享缓存 → 若混用 ESM 和 CommonJS,需统一模块系统或通过
createRequire桥接

















