跳到主要内容

Node.js模块系统

模块系统概述

Node.js的模块系统是其核心特性之一,它允许开发者将代码分割成独立、可复用的模块。Node.js采用CommonJS模块规范,通过require()函数导入模块,使用module.exportsexports导出模块。

模块的基本概念

  • 模块:一个独立的JavaScript文件,拥有自己的作用域
  • 模块标识:用于定位模块的标识符,可以是相对路径、绝对路径或模块名称
  • 模块加载:Node.js会缓存已加载的模块,避免重复加载

CommonJS规范

CommonJS是Node.js采用的模块规范,它定义了模块的导入和导出方式。

导出模块

使用module.exports

// math.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;

module.exports = {
add,
subtract
};

使用exports

// math.js
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;

注意exportsmodule.exports的一个引用,直接赋值exports = xxx会断开这个引用,导致导出失败。

导入模块

// app.js
const math = require('./math');
console.log(math.add(1, 2)); // 输出: 3
console.log(math.subtract(5, 3)); // 输出: 2

// 也可以解构导入
const { add, subtract } = require('./math');
console.log(add(1, 2)); // 输出: 3

模块的类型

内置模块

Node.js提供了一些内置模块,无需安装即可使用。

// 使用内置模块
const fs = require('fs'); // 文件系统模块
const http = require('http'); // HTTP模块
const path = require('path'); // 路径模块
const os = require('os'); // 操作系统模块
const util = require('util'); // 实用工具模块

第三方模块

通过NPM安装的模块,位于项目的node_modules目录下。

// 使用第三方模块
const express = require('express'); // Express.js框架
const axios = require('axios'); // HTTP客户端
const lodash = require('lodash'); // 实用工具库

自定义模块

开发者自己编写的模块。

// 导入自定义模块
const myModule = require('./my-module');

模块加载机制

Node.js的模块加载遵循以下规则:

  1. 如果模块标识是绝对路径或相对路径,则按照路径查找
  2. 如果模块标识不是路径,则先查找内置模块
  3. 如果不是内置模块,则在当前目录的node_modules目录中查找
  4. 如果未找到,则向上级目录的node_modules目录中查找,直到根目录

模块缓存

Node.js会缓存已加载的模块,当再次require时,会返回缓存的模块实例,而不是重新加载。

// 多次require同一个模块,返回的是同一个实例
const mod1 = require('./my-module');
const mod2 = require('./my-module');
console.log(mod1 === mod2); // 输出: true

模块包装器

Node.js在执行模块代码之前,会将模块包装在一个函数中,这就是模块包装器。

(function(exports, require, module, __filename, __dirname) {
// 模块代码
});

这样做的好处是:

  • 隔离模块作用域,避免全局变量污染
  • 提供moduleexportsrequire等对象
  • 提供__filename__dirname变量

__filename和__dirname

  • __filename:当前模块文件的绝对路径
  • __dirname:当前模块所在目录的绝对路径
console.log(__filename); // 输出: /path/to/current/file.js
console.log(__dirname); // 输出: /path/to/current

循环依赖

当两个或多个模块相互依赖时,就会形成循环依赖。Node.js通过部分加载来解决这个问题。

// a.js
console.log('a 开始');
const b = require('./b');
console.log('a 中 b 的值:', b);
module.exports = { value: 'a' };
console.log('a 结束');

// b.js
console.log('b 开始');
const a = require('./a');
console.log('b 中 a 的值:', a);
module.exports = { value: 'b' };
console.log('b 结束');

// main.js
console.log('main 开始');
const a = require('./a');
console.log('main 中 a 的值:', a);
console.log('main 结束');

运行main.js的输出结果会是:

main 开始
a 开始
b 开始
b 中 a 的值: {}
a 中 b 的值: { value: 'b' }
a 结束
main 中 a 的值: { value: 'a' }
main 结束

ES模块

从Node.js 12开始,Node.js支持ES模块(ECMAScript Modules)。要使用ES模块,需要:

  1. 将文件扩展名改为.mjs
  2. 或者在package.json中设置"type": "module"

ES模块使用importexport语法:

// math.mjs
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

export default {
add,
subtract
};

// app.mjs
import math, { add, subtract } from './math.mjs';
console.log(add(1, 2)); // 输出: 3
console.log(math.subtract(5, 3)); // 输出: 2

最佳实践

  • 每个模块只负责一个功能,保持模块的单一职责
  • 模块的命名要清晰、有意义
  • 合理组织模块结构,便于维护
  • 避免过多的模块依赖,特别是循环依赖
  • 对于公共API,使用文档注释说明使用方法
  • 考虑使用ES模块,特别是新项目