适用读者:Node.js 开发者、JavaScript 程序员、全栈工程师
目标:深入理解 Node.js 模块系统,掌握模块创建、导出和导入技术


1. 模块简介:Node.js 应用的构建块

1.1 什么是模块?

在 Node.js 中,模块是封装代码的基本单元,每个 JavaScript 文件都被视为一个独立的模块。模块系统使开发者能够将复杂应用拆分为可管理、可重用的组件。

Node.js 应用
模块 1
模块 2
模块 3
模块 N
函数
变量
对象
方法
数据
配置

1.2 模块系统的优势

  • 代码组织:将相关功能组织在一起,提高可读性
  • 封装性:隐藏实现细节,暴露公共接口
  • 重用性:在多个应用或项目中重用模块
  • 维护性:修改模块不影响其他代码部分
  • 命名空间:避免全局命名空间污染

1.3 Node.js 模块规范

Node.js 使用 CommonJS 模块规范,这不同于浏览器中的 ESM (ECMAScript Modules) 规范。CommonJS 使用 require() 导入模块和 module.exports 导出模块。

特性CommonJS (Node.js)ESM (ES6)
导入语法const mod = require('mod')import mod from 'mod'
导出语法module.exports = ...export default ...
加载方式运行时加载编译时加载
顶层 this不是 globalundefined
异步性质同步异步

2. 模块类型:核心模块与本地模块

2.1 核心模块

Node.js 内置的核心模块,无需安装即可使用:

模块名功能描述
http创建 HTTP 服务器和客户端
fs文件系统操作
path路径处理
os操作系统相关实用程序
events事件触发器
util实用函数
querystring查询字符串解析和格式化
urlURL 解析和操作
stream流处理
zlib压缩和解压缩
crypto加密和哈希功能

2.2 本地模块

开发者自己创建的模块,分为两种:

  • 文件模块:单个 JavaScript 文件
  • 文件夹模块:包含多个文件的目录,通常有 package.json 和入口文件

2.3 第三方模块

通过 NPM 安装的模块:

# 安装第三方模块
npm install express lodash moment

3. 核心模块详解

3.1 使用核心模块

使用 require() 函数导入核心模块:

// 导入 http 模块
const http = require('http');

// 导入文件系统模块
const fs = require('fs');

// 导入路径模块
const path = require('path');

// 导入事件模块
const EventEmitter = require('events');

// 导入操作系统模块
const os = require('os');

3.2 常用核心模块示例

3.2.1 HTTP 模块示例
const http = require('http');

// 创建服务器
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello, World!');
});

server.listen(3000, () => {
  console.log('Server running at http://localhost:3000/');
});
3.2.2 文件系统模块示例
const fs = require('fs');
const path = require('path');

// 读取文件
fs.readFile(path.join(__dirname, 'file.txt'), 'utf8', (err, data) => {
  if (err) throw err;
  console.log(data);
});

// 写入文件
fs.writeFile(path.join(__dirname, 'output.txt'), 'Hello, Node.js!', (err) => {
  if (err) throw err;
  console.log('File saved!');
});
3.2.3 路径模块示例
const path = require('path');

// 路径拼接
const fullPath = path.join(__dirname, 'src', 'app.js');
console.log(fullPath);

// 获取文件名
const filename = path.basename('/path/to/file.txt');
console.log(filename); // file.txt

// 获取扩展名
const ext = path.extname('/path/to/file.txt');
console.log(ext); // .txt

// 解析路径
const parsedPath = path.parse('/path/to/file.txt');
console.log(parsedPath);
/*
{
  root: '/',
  dir: '/path/to',
  base: 'file.txt',
  ext: '.txt',
  name: 'file'
}
*/

4. 创建和使用本地模块

4.1 创建简单模块

创建数学工具模块 math.js

// math.js
// 模块私有变量
const _factorialCache = {};

// 导出函数
module.exports = {
  // 加法函数
  add: (a, b) => a + b,
  
  // 减法函数
  subtract: (a, b) => a - b,
  
  // 乘法函数
  multiply: (a, b) => a * b,
  
  // 除法函数
  divide: (a, b) => {
    if (b === 0) throw new Error('Division by zero');
    return a / b;
  },
  
  // 带缓存的阶乘
  factorial: (n) => {
    if (n === 0 || n === 1) return 1;
    if (_factorialCache[n]) return _factorialCache[n];
    
    let result = 1;
    for (let i = 2; i <= n; i++) {
      result *= i;
    }
    
    _factorialCache[n] = result;
    return result;
  }
};

4.2 使用本地模块

创建 app.js 使用 math.js 模块:

// app.js
// 导入本地模块
const math = require('./math.js');

// 使用模块函数
console.log('5 + 3 =', math.add(5, 3));      // 8
console.log('10 - 7 =', math.subtract(10, 7)); // 3
console.log('4 * 6 =', math.multiply(4, 6));  // 24
console.log('8 / 2 =', math.divide(8, 2));     // 4

// 使用阶乘函数
console.log('5! =', math.factorial(5));        // 120

// 再次使用缓存
console.log('5! (cached) =', math.factorial(5)); // 120

4.3 模块文件夹结构

对于更复杂的模块,可以创建文件夹结构:

my-module/
├── package.json
├── index.js          # 主入口文件
├── lib/
│   ├── utils.js
│   └── constants.js
└── README.md

package.json 文件:

{
  "name": "my-module",
  "version": "1.0.0",
  "main": "index.js",
  "description": "A custom module for demonstration"
}

index.js 文件:

const utils = require('./lib/utils');
const constants = require('./lib/constants');

module.exports = {
  utils,
  constants,
  // 模块的公共 API
  doSomething: function() {
    return utils.process(constants.CONFIG);
  }
};

5. 导出和导入机制

5.1 module.exports 对象

每个模块都有一个 module.exports 对象,它代表模块的公共接口。当其他模块使用 require() 加载此模块时,将收到 module.exports 的内容。

5.2 导出方式

5.2.1 导出单个对象或函数
// 导出单个函数
module.exports = function() {
  return 'Hello, Module!';
};

// 导出对象
module.exports = {
  greet: function() {
    return 'Hello, World!';
  },
  
  farewell: function() {
    return 'Goodbye, World!';
  }
};
5.2.2 导出多个属性
// 方式 1: 向 module.exports 添加属性
module.exports.greet = function(name) {
  return `Hello, ${name}!`;
};

module.exports.farewell = function(name) {
  return `Goodbye, ${name}!`;
};

// 方式 2: 使用 exports 别名
exports.greet = function(name) {
  return `Hello, ${name}!`;
};

exports.farewell = function(name) {
  return `Goodbye, ${name}!`;
};

5.3 module.exports 与 exports 的区别

exportsmodule.exports 的引用,但它们之间存在重要区别:

// 正确使用 exports
exports.name = 'John Doe';
exports.age = 30;
exports.greet = function() {
  return 'Hello!';
};

// 错误使用 exports - 不会生效
exports = { name: 'John Doe' }; // exports 不再指向 module.exports

// 正确替换整个导出对象
module.exports = { 
  name: 'John Doe',
  age: 30,
  greet: function() {
    return 'Hello!';
  }
};
graph TD
    A[module.exports] -->|初始指向| B[空对象]
    C[exports] -->|初始指向| B[空对象]
    
    D[exports.x = 1] -->|修改| E[{x: 1}]
    A -->|依然指向| E
    
    F[exports = {y: 2}] -->|现在指向| G[{y: 2}]
    A -->|依然指向| E

5.4 导入机制详解

require() 函数的工作流程:

  1. 解析文件路径(核心模块、文件模块或 node_modules)
  2. 加载文件内容到内存
  3. 将代码包装在函数中执行
  4. 返回 module.exports 对象
// require 的基本语法
const module = require('module-name');

// 相对路径导入
const localModule = require('./localModule.js');
const utils = require('../lib/utils.js');

// 绝对路径导入
const config = require('/path/to/config.js');

// 不带扩展名的导入
const app = require('./app'); // Node.js 会尝试 .js, .json, .node

6. 模块包装函数

Node.js 在执行模块代码前,会将文件内容包装在一个函数中:

(function(exports, require, module, __filename, __dirname) {
  // 模块代码在这里执行
  // 例如:
  const fs = require('fs');
  module.exports = {
    // 导出内容
  };
});

这意味着在模块中,你可以访问以下变量:

  • exports:指向 module.exports,用于导出模块内容
  • require:用于导入其他模块的函数
  • module:当前模块的引用
  • __filename:当前模块文件的绝对路径
  • __dirname:当前模块所在目录的绝对路径

6.1 全局变量与模块作用域

在模块中,没有全局作用域,但有模块级作用域:

// 模块级变量,不属于全局
const moduleVariable = 'Module scope';

console.log(global.moduleVariable); // undefined
console.log(moduleVariable);       // 'Module scope'

// 声明变量时,默认在模块作用域中
notDeclaredWithVar = 'Global scope'; // 这会创建全局变量

6.2 立即执行函数模拟

我们可以模拟模块包装函数的行为:

// 准备模块内容
const moduleCode = `
  const message = 'Hello from module';
  module.exports.greet = function() {
    return message;
  }
`;

// 模拟模块包装函数
function wrapModule(code) {
  const wrapper = `(function(exports, require, module, __filename, __dirname) { ${code} })`;
  const script = new Function('exports', 'require', 'module', '__filename', '__dirname', wrapper);
  
  return function(moduleExports, require, module, filename, dirname) {
    return script(module.exports, require, module, filename, dirname);
  };
}

// 使用模拟包装函数
const simulatedModule = { exports: {} };
const wrappedModule = wrapModule(moduleCode);

// 执行模拟模块
wrappedModule({}, require, simulatedModule, '/path/to/module.js', '/path/to');

// 检查结果
console.log(simulatedModule.exports.greet()); // 'Hello from module'

7. 模块加载与缓存

7.1 模块加载过程

Node.js 在加载模块时遵循以下步骤:

require() 调用 模块解析 模块加载 包装执行 返回导出 应用程序 require('module') 解析模块路径 检查缓存 返回缓存模块 读取文件内容 包装代码在函数中 执行函数 存储结果到缓存 返回新模块 alt [缓存存在] [缓存不存在] 返回 module.exports require() 调用 模块解析 模块加载 包装执行 返回导出 应用程序

7.2 模块缓存机制

Node.js 会缓存首次加载的模块,后续 require() 调用将返回缓存版本:

// counter.js
let count = 0;

module.exports = {
  increment: () => {
    count++;
    return count;
  },
  getCount: () => {
    return count;
  }
};

// app.js
const counter1 = require('./counter.js');
const counter2 = require('./counter.js');

console.log(counter1.increment()); // 1
console.log(counter2.increment()); // 2 (共享同一个模块实例)
console.log(counter1.getCount());  // 2

7.3 清除模块缓存

在某些情况下,可能需要清除模块缓存(如热重载):

// 清除特定模块的缓存
function clearModuleCache(moduleName) {
  const resolved = require.resolve(moduleName);
  delete require.cache[resolved];
}

// 使用示例
clearModuleCache('./counter.js');
const freshCounter = require('./counter.js');
console.log(freshCounter.increment()); // 1

8. 模块最佳实践

8.1 组织原则

  1. 单一职责:每个模块专注于单一功能
  2. 明确接口:清晰定义公共 API
  3. 封装实现:隐藏内部实现细节
  4. 合理命名:使用描述性的文件和目录名

8.2 模块结构示例

myapp/
├── lib/
│   ├── auth/          # 认证相关模块
│   │   ├── index.js
│   │   ├── local.js
│   │   └── strategies/
│   ├── database/      # 数据库模块
│   │   ├── index.js
│   │   ├── models/
│   │   └── queries/
│   ├── services/      # 业务逻辑模块
│   │   ├── email.js
│   │   └── payment.js
│   ├── utils/         # 工具函数
│   │   ├── string.js
│   │   └── date.js
│   └── index.js       # 模块入口点
├── index.js           # 应用入口点
└── package.json

8.3 模块设计模式

8.3.1 工厂模式
// logger.js
function createLogger(options = {}) {
  const { level = 'info' } = options;
  
  return {
    log: function(message) {
      console.log(`${level.toUpperCase()}: ${message}`);
    },
    
    error: function(message) {
      console.error(`ERROR: ${message}`);
    }
  };
}

module.exports = createLogger;

// app.js
const createLogger = require('./logger');
const logger = createLogger({ level: 'debug' });

logger.log('Application started');
logger.error('Something went wrong');
8.3.2 单例模式
// config.js
let instance = null;

function createConfig() {
  if (!instance) {
    instance = {
      get: function(key) {
        // 实际应用中可能从文件或环境变量读取
        const configData = {
          port: 3000,
          database: {
            host: 'localhost',
            user: 'admin'
          }
        };
        return key.split('.').reduce((obj, i) => obj[i], configData);
      }
    };
  }
  return instance;
}

module.exports = createConfig();

// app.js
const config = require('./config');
console.log(config.get('port'));           // 3000
console.log(config.get('database.host'));  // 'localhost'
8.3.3 中间件模式
// middleware.js
function logger(req, res, next) {
  console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
  next(); // 移交控制权给下一个中间件
}

function auth(req, res, next) {
  if (req.headers.authorization === 'secret-token') {
    req.user = { id: 1, name: 'Admin' };
    next();
  } else {
    res.status(401).send('Unauthorized');
  }
}

module.exports = {
  logger,
  auth
};

// app.js
const http = require('http');
const { logger, auth } = require('./middleware');

const server = http.createServer((req, res) => {
  // 应用中间件
  logger(req, res, () => {
    auth(req, res, () => {
      res.writeHead(200);
      res.end(`Hello, ${req.user.name}`);
    });
  });
});

server.listen(3000);

8.4 循环依赖问题

循环依赖(模块 A 依赖 B,模块 B 依赖 A)会导致问题:

// a.js
const b = require('./b.js');
module.exports.a = 'a';
console.log('a loaded, b.b =', b.b);

// b.js
const a = require('./a.js');
module.exports.b = 'b';
console.log('b loaded, a.a =', a.a);

// app.js
require('./a.js');
require('./b.js');

// 输出:
// b loaded, a.a = undefined
// a loaded, b.b = b

解决循环依赖的方法:

  1. 重构代码:消除循环依赖
  2. 事件传递:使用事件代替直接依赖
  3. 依赖注入:在运行时注入依赖
  4. 延迟加载:在函数内部加载依赖

9. 模块系统高级主题

9.1 ES 模块支持

Node.js 也支持 ECMAScript 模块 (ESM) 规范,有两种方式启用:

9.1.1 使用 .mjs 扩展名
// math.mjs
export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

// app.mjs
import { add, multiply } from './math.mjs';

console.log(add(5, 3));       // 8
console.log(multiply(4, 6));  // 24
9.1.2 设置 package.jsontype 字段
{
  "type": "module"
}

然后就可以在 .js 文件中使用 ES 模块语法:

// math.js
export function add(a, b) {
  return a + b;
}

// app.js
import { add } from './math.js';
console.log(add(2, 3));  // 5

9.2 在 CommonJS 中使用 ES 模块

// math.mjs
export function add(a, b) {
  return a + b;
}

// app.cjs
(async () => {
  const math = await import('./math.mjs');
  console.log(math.add(2, 3));  // 5
})();

9.3 在 ES 模块中使用 CommonJS

// math.cjs
module.exports.add = function(a, b) {
  return a + b;
};

// app.mjs
import { createRequire } from 'module';
const require = createRequire(import.meta.url);

const math = require('./math.cjs');
console.log(math.add(2, 3));  // 5

9.4 动态导入

Node.js 支持动态导入,可以在运行时加载模块:

// CommonJS 中的动态导入
async function loadModule() {
  const module = await import('./module.mjs');
  console.log(module.doSomething());
}

// ES 模块中的动态导入
async function loadModule() {
  const module = await import('./module.mjs');
  console.log(module.doSomething());
}

10. 总结与应用示例

10.1 关键概念回顾

  • 模块系统:Node.js 基于 CommonJS 的模块系统
  • 模块类型:核心模块、本地模块、第三方模块
  • 导入/导出:使用 require()module.exports
  • 模块缓存:首次加载后缓存模块实例
  • 模块范围:每个模块有自己的作用域

10.2 综合应用示例:电商应用模块结构

ecommerce-app/
├── app.js              # 应用入口
├── config/
│   └── index.js        # 配置模块
├── models/
│   ├── user.js         # 用户模型
│   ├── product.js      # 产品模型
│   └── order.js        # 订单模型
├── routes/
│   ├── users.js        # 用户路由
│   ├── products.js     # 产品路由
│   └── orders.js       # 订单路由
├── services/
│   ├── email.js        # 邮件服务
│   ├── payment.js      # 支付服务
│   └── auth.js         # 认证服务
├── middleware/
│   ├── auth.js         # 认证中间件
│   └── logger.js       # 日志中间件
├── utils/
│   ├── validators.js   # 验证工具
│   └── helpers.js      # 辅助函数
└── package.json
app.js
config
routes
middleware
models
services
email service
payment service
auth middleware
logger middleware
user model
product model
order model
email validator
payment validator

10.3 模块最佳实践清单

  • 单一职责:每个模块专注于一个明确的功能
  • 清晰命名:使用有意义、一致的命名约定
  • 最小公开:只暴露必要的 API,隐藏实现细节
  • 避免循环依赖:精心设计模块间的依赖关系
  • 使用缓存优:利用模块缓存但谨慎清除
  • 文档模块:提供使用示例和 API 文档
  • 错误处理:在模块内部妥善处理错误
  • 单元测试:为每个模块编写测试用例

10.4 进阶学习路径

  1. 模块打包工具:Webpack, Rollup, Parcel
  2. 模块联邦:微前端架构中的模块共享
  3. 动态模块加载:按需加载模块提高性能
  4. 模块安全性:防止恶意代码注入和依赖审计
  5. TypeScript 集成:类型定义和编译支持

10.5 资源推荐

最终建议:模块系统是 Node.js 的核心特性,也是构建可维护、可扩展应用的基础。掌握模块创建、导出和导入的技巧,遵循最佳实践设计模块结构,将使你的 Node.js 应用更加健壮、易于维护。随着项目复杂度的增长,良好的模块设计将成为项目成功的关键因素。持续探索和改进你的模块设计,让代码更优雅、更高效!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐