CommonJS模块系统
CommonJS模块系统是Node.js中用于组织和管理代码的核心机制之一。它通过require
和module.exports
实现模块的导入和导出,解决了早期JavaScript缺乏原生模块化支持的问题。
CommonJS模块系统的基本概念
CommonJS规范最初是为服务器端JavaScript环境设计的,后来被Node.js采用并成为其模块系统的基础。每个文件在Node.js中都被视为一个独立的模块,模块内部的变量、函数和类默认是私有的,只有通过显式导出才能被其他模块访问。
模块系统的主要特点包括:
- 同步加载模块
- 模块缓存机制
- 清晰的依赖关系
- 简单的导入导出语法
模块导出机制
CommonJS提供了两种主要方式来导出模块内容:
使用module.exports
这是最基础的导出方式,可以导出任意类型的值:
// math.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
module.exports = {
add,
subtract
};
也可以直接导出单个函数或类:
// logger.js
module.exports = function(message) {
console.log(`[LOG] ${new Date().toISOString()}: ${message}`);
};
使用exports快捷方式
Node.js提供了exports
作为module.exports
的快捷方式:
// utils.js
exports.capitalize = str => str.charAt(0).toUpperCase() + str.slice(1);
exports.trim = str => str.trim();
需要注意的是,exports
只是module.exports
的一个引用,不能直接重写:
// 错误写法
exports = { foo: 'bar' }; // 这将不起作用
// 正确写法
module.exports = { foo: 'bar' };
模块导入机制
使用require
函数导入模块:
// app.js
const math = require('./math');
const logger = require('./logger');
const { capitalize } = require('./utils');
console.log(math.add(2, 3)); // 5
logger('Application started');
console.log(capitalize('hello')); // 'Hello'
require的查找规则
Node.js按照以下顺序查找模块:
- 核心模块(如fs、path等)
- 文件模块(以./或../开头)
- 目录模块(查找package.json或index.js)
- node_modules中的模块
// 导入核心模块
const fs = require('fs');
// 导入文件模块
const config = require('./config.json');
// 导入目录模块(自动查找index.js)
const models = require('./models');
// 导入node_modules中的模块
const express = require('express');
模块缓存机制
Node.js会对加载过的模块进行缓存,提高性能:
// moduleA.js
console.log('Module A loaded');
module.exports = { value: Math.random() };
// main.js
const a1 = require('./moduleA');
const a2 = require('./moduleA');
console.log(a1 === a2); // true
console.log(a1.value === a2.value); // true
循环依赖处理
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
console.log('main starting');
const a = require('./a');
const b = require('./b');
console.log('in main, a.done=', a.done, 'b.done=', b.done);
执行结果会显示模块加载的顺序和状态变化。
模块作用域
每个模块都有自己独立的作用域,不会污染全局:
// module1.js
var count = 0;
function increment() {
count++;
}
// module2.js
var count = 100; // 不会影响module1中的count
动态加载
虽然CommonJS主要是同步加载,但也可以实现动态加载:
// 动态加载示例
const moduleName = process.env.NODE_ENV === 'production'
? './prodModule'
: './devModule';
const dynamicModule = require(moduleName);
与ES模块的差异
虽然Node.js现在支持ES模块,但CommonJS仍然是主要模块系统:
- CommonJS是同步加载,ES模块是异步加载
- CommonJS使用
require
/module.exports
,ES模块使用import
/export
- CommonJS模块是运行时加载,ES模块是编译时静态解析
// CommonJS
module.exports = { foo: 'bar' };
const lib = require('./lib');
// ES模块
export const foo = 'bar';
import { foo } from './lib.mjs';
实际应用示例
配置文件管理
// config.js
const env = process.env.NODE_ENV || 'development';
const configs = {
development: {
apiUrl: 'http://localhost:3000',
debug: true
},
production: {
apiUrl: 'https://api.example.com',
debug: false
}
};
module.exports = configs[env];
// app.js
const config = require('./config');
console.log(`API URL: ${config.apiUrl}`);
中间件模式
// middleware.js
module.exports = function(req, res, next) {
console.log(`${req.method} ${req.url}`);
next();
};
// server.js
const express = require('express');
const logger = require('./middleware');
const app = express();
app.use(logger);
模块热替换
虽然CommonJS本身不支持热替换,但可以通过一些技巧实现:
// hotModule.js
module.exports = {
data: 'initial'
};
// watcher.js
const fs = require('fs');
const path = require('path');
function watchModule(modulePath, callback) {
const fullPath = require.resolve(modulePath);
fs.watch(fullPath, () => {
// 清除缓存
delete require.cache[fullPath];
const newModule = require(modulePath);
callback(newModule);
});
}
// 使用示例
watchModule('./hotModule', (module) => {
console.log('Module updated:', module.data);
});
性能优化技巧
- 合理组织模块结构,避免过深的依赖链
- 对于频繁使用的核心模块,可以提前缓存:
const _require = require;
const path = _require('path');
const fs = _require('fs');
- 避免在热路径中动态require
调试模块系统
可以通过module
全局对象查看模块信息:
console.log(module);
// 查看模块缓存
console.log(require.cache);
模块模式实践
CommonJS支持多种模块组织模式:
单例模式
// db.js
let instance = null;
class Database {
constructor() {
if (!instance) {
instance = this;
this.connection = null;
}
return instance;
}
connect() {
this.connection = 'Connected';
}
}
module.exports = new Database();
// app.js
const db1 = require('./db');
const db2 = require('./db');
console.log(db1 === db2); // true
工厂模式
// logger.js
module.exports = (prefix) => {
return {
log: message => console.log(`[${prefix}] ${message}`),
error: message => console.error(`[${prefix}] ERROR: ${message}`)
};
};
// app.js
const createLogger = require('./logger');
const dbLogger = createLogger('DB');
const apiLogger = createLogger('API');
dbLogger.log('Connection established');
apiLogger.error('Request failed');
模块版本管理
在大型项目中,可能需要管理不同版本的模块:
// v1/module.js
module.exports = function() {
console.log('This is version 1');
};
// v2/module.js
module.exports = function() {
console.log('This is version 2');
};
// app.js
const v1 = require('./v1/module');
const v2 = require('./v2/module');
v1(); // This is version 1
v2(); // This is version 2
模块测试技巧
测试CommonJS模块时可以利用其缓存机制:
// counter.js
let count = 0;
module.exports = {
increment: () => ++count,
getCount: () => count
};
// test.js
const counter = require('./counter');
// 在每个测试用例前重置模块状态
beforeEach(() => {
delete require.cache[require.resolve('./counter')];
});
test('increment should increase count', () => {
const counter = require('./counter');
counter.increment();
expect(counter.getCount()).toBe(1);
});
模块元信息
可以通过module
对象访问模块的元信息:
console.log('Module ID:', module.id);
console.log('Module filename:', module.filename);
console.log('Module loaded:', module.loaded);
console.log('Parent module:', module.parent);
console.log('Children modules:', module.children);
模块路径解析
Node.js提供了模块路径解析的工具方法:
const resolvedPath = require.resolve('./someModule');
console.log('Resolved path:', resolvedPath);
模块包装器
Node.js在执行模块代码前会将其包装在一个函数中:
(function(exports, require, module, __filename, __dirname) {
// 模块代码实际在这里执行
});
这解释了为什么模块中有这些变量可用:
console.log('File:', __filename);
console.log('Directory:', __dirname);
模块加载过程
Node.js加载模块的详细步骤:
- 解析模块路径
- 检查缓存
- 创建新模块实例
- 将模块实例存入缓存
- 加载模块内容
- 包装模块代码
- 执行模块代码
- 返回module.exports
模块与包的关系
在Node.js生态中:
- 模块(Module)是单个文件
- 包(Package)是包含package.json的目录
- 一个包可以包含多个模块
// 导入包中的主模块
const pkg = require('some-package');
// 导入包中的特定模块
const util = require('some-package/utils');
模块系统扩展
虽然CommonJS是Node.js默认的模块系统,但可以通过加载器扩展:
// 自定义JSON加载器
require.extensions['.json'] = function(module, filename) {
const content = fs.readFileSync(filename, 'utf8');
module.exports = JSON.parse(content);
};
// 现在可以直接require JSON文件
const data = require('./data.json');
注意:修改require.extensions已被官方标记为废弃,不推荐在生产环境使用。
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:单线程与事件循环
下一篇:Webpack核心知识点