阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > CommonJS模块系统

CommonJS模块系统

作者:陈川 阅读数:10408人阅读 分类: Node.js

CommonJS模块系统是Node.js中用于组织和管理代码的核心机制之一。它通过requiremodule.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按照以下顺序查找模块:

  1. 核心模块(如fs、path等)
  2. 文件模块(以./或../开头)
  3. 目录模块(查找package.json或index.js)
  4. 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仍然是主要模块系统:

  1. CommonJS是同步加载,ES模块是异步加载
  2. CommonJS使用require/module.exports,ES模块使用import/export
  3. 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);
});

性能优化技巧

  1. 合理组织模块结构,避免过深的依赖链
  2. 对于频繁使用的核心模块,可以提前缓存:
const _require = require;
const path = _require('path');
const fs = _require('fs');
  1. 避免在热路径中动态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加载模块的详细步骤:

  1. 解析模块路径
  2. 检查缓存
  3. 创建新模块实例
  4. 将模块实例存入缓存
  5. 加载模块内容
  6. 包装模块代码
  7. 执行模块代码
  8. 返回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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌