Node.js入门教程(二十):模块导入
一、模块系统概述
当你开始写 Node.js 项目时,最先遇到的问题之一就是——如何导入模块(module)。
在 Node.js 里,模块就是可以重复使用的 JavaScript 文件,它们之间通过导入(import)和导出(export)互相通信。
Node.js 生态系统主要支持两种模块系统:
| 模块系统 | 规范 | 文件扩展名 | 导入方式 | 导出方式 |
|---|---|---|---|---|
| CommonJS | Node.js 早期使用 | .js | require() | module.exports/exports |
| ES Module (ESM) | ECMAScript 标准 | .mjs或"type": "module" | import | export/export default |
二、CommonJS 模块系统
CommonJS 是 Node.js 最早也是最广泛使用的模块系统,它采用同步加载的方式,这意味着模块在加载时会阻塞程序的执行,直到加载完成。
导出模块
在 CommonJS 中,使用module.exports或exports对象来导出模块。
// math.js function add(a, b) { return a + b; } function subtract(a, b) { return a - b; } // 方式一:使用 module.exports 导出对象 module.exports = { add, subtract }; // 方式二:使用 exports 逐个导出 // exports.add = add; // exports.subtract = subtract;导入模块
使用require()函数来导入模块:
// app.js const math = require('./math.js'); console.log(math.add(2, 3)); // 5 console.log(math.subtract(5, 2)); // 3 // 也可以解构导入 const { add, subtract } = require('./math.js'); console.log(add(2, 3)); // 5module.exports 与 exports 的区别
// user.js const name = 'Alice'; const age = 30; // 方式一:使用 module.exports 导出单个对象 module.exports = { name: name, age: age, sayHello: () => { console.log(`Hello, my name is ${name}.`); } }; // 方式二:使用 exports 导出多个具名变量 // exports 是 module.exports 的一个引用 // exports.name = name; // exports.age = age; // exports.sayHello = () => { // console.log(`Hello, my name is ${name}.`); // };// main.js const user = require('./user.js'); console.log(user.name); // 输出: Alice user.sayHello(); // 输出: Hello, my name is Alice. // 也可以直接解构 const { name, age } = require('./user.js'); console.log(name); // 输出: AliceCommonJS 特点
| 特点 | 说明 |
|---|---|
| 同步加载 | 适合服务器端环境,模块通常都在本地文件系统中,加载速度快 |
| 运行时加载 | require()可以在代码的任何位置调用,支持条件动态加载 |
| 缓存机制 | require()加载的模块会被缓存,第二次导入时直接从缓存中读取,避免重复加载 |
三、ES Module(ESM)规范
ESM 是 JavaScript 官方的模块标准,它采用异步加载,是浏览器和现代 Node.js 应用的首选。
启用 ESM
要在 Node.js 中使用 ESM,有两种方式:
方式一:在package.json中添加"type": "module"
{ "type": "module" }方式二:将文件扩展名更改为.mjs
导出模块
ESM 支持两种导出方式:具名导出和默认导出。
// math.mjs // 具名导出 export function add(a, b) { return a + b; } export function subtract(a, b) { return a - b; } export const PI = 3.14159; // 默认导出 export default function multiply(a, b) { return a * b; }导入模块
// app.mjs // 导入具名导出 import { add, subtract, PI } from './math.mjs'; // 导入默认导出(可以自定义名称) import multiply from './math.mjs'; // 同时导入具名和默认导出 import multiply, { add, subtract } from './math.mjs'; console.log(add(2, 3)); // 5 console.log(subtract(5, 2)); // 3 console.log(PI); // 3.14159 console.log(multiply(2, 3)); // 6 // 导入所有具名导出,作为对象的属性 import * as math from './math.mjs'; console.log(math.add(2, 3)); // 5 console.log(math.default(2, 3)); // 6(默认导出作为 default 属性)ESM 特点
| 特点 | 说明 |
|---|---|
| 异步加载 | 默认是异步加载,不会阻塞主线程,更适合浏览器环境 |
| 静态分析 | import和export语句在代码执行前就可以确定模块的依赖关系,支持 Tree Shaking 等优化 |
| 严格模式 | ESM 模块默认在严格模式下运行 |
四、CommonJS 和 ESM 的对比
| 项目 | CommonJS | ESM |
|---|---|---|
| 语法 | require/module.exports | import/export |
| 加载机制 | 运行时同步加载 | 编译时静态加载 |
| 默认支持 | Node.js 默认支持 | 需.mjs或"type": "module" |
| 适合场景 | 后端脚本、老项目 | 前后端现代项目、Tree-shaking |
| 是否可混用 | 不能直接混用(需额外配置) | 不能直接混用(需额外配置) |
五、混合使用
Node.js 在较新版本中支持两种模块系统共存。可以在同一个项目中同时使用 CommonJS 和 ESM,但需要注意以下几点:
ESM 中不能直接使用
require()和module.exportsCommonJS 中不能直接使用
import和export
ESM 中导入 CommonJS 模块
ESM 会将 CommonJS 模块视为默认导出:
// commonjs_module.js module.exports = { data: 'hello' };// esm_module.mjs import commonModule from './commonjs_module.js'; console.log(commonModule.data); // 输出: hello
CommonJS 中导入 ESM 模块
需要使用动态import()函数:
// esm_module.mjs export const name = 'Bob'; export const age = 25;
// commonjs_module.js async function loadESM() { const { name, age } = await import('./esm_module.mjs'); console.log(name); // 输出: Bob console.log(age); // 输出: 25 } loadESM();六、路径解析规则
相对路径导入
使用./或../开头:
// 导入同目录下的模块 const mod = require('./myModule'); // 导入父目录下的模块 const mod = require('../lib/myModule');绝对路径导入
使用/开头(指向文件系统根目录):
const mod = require('/path/to/module');核心模块导入
直接使用模块名称:
const fs = require('fs'); const http = require('http');第三方模块导入
使用模块名称(从node_modules中查找):
const express = require('express'); const lodash = require('lodash');七、动态导入
CommonJS 中的动态导入
CommonJS 的require()本身就是动态的,可以在任何位置调用:
// 条件导入 if (condition) { const moduleA = require('./moduleA'); moduleA.doSomething(); } else { const moduleB = require('./moduleB'); moduleB.doSomething(); }ESM 中的动态导入
ESM 使用import()函数进行动态导入,返回一个 Promise:
// 动态导入 async function loadModule(condition) { if (condition) { const moduleA = await import('./moduleA.mjs'); moduleA.doSomething(); } else { const moduleB = await import('./moduleB.mjs'); moduleB.doSomething(); } }八、常见问题
1. Cannot find module 错误
当 Node.js 找不到模块时,会报Cannot find module错误。检查:
路径是否正确
模块是否已安装(第三方模块)
文件是否存在
2. 循环依赖问题
当两个模块相互引用时,可能导致其中一个模块获取到不完整的导出对象。建议:
避免循环依赖
如果无法避免,使用
module.exports提前导出
3. ESM 和 CommonJS 混用问题
在 ESM 中导入 CommonJS 模块时,CommonJS 的
module.exports会被视为默认导出在 CommonJS 中导入 ESM 模块时,必须使用动态
import()文件扩展名很重要:ESM 使用
.mjs,CommonJS 使用.js
九、本章小结
| 知识点 | 说明 |
|---|---|
| CommonJS | Node.js 默认模块系统,使用require()/module.exports,同步加载 |
| ES Module | JavaScript 官方标准,使用import/export,异步加载 |
| 启用 ESM | 使用.mjs扩展名或在package.json中设置"type": "module" |
| 具名导出 | export const name = ...,导入时需用{ name } |
| 默认导出 | export default ...,导入时可自定义名称 |
| 混用规则 | ESM 中可导入 CommonJS;CommonJS 中需用import()导入 ESM |
| 路径解析 | 核心模块直接名称、本地模块用./、第三方模块从node_modules查找 |
最佳实践:对于新的项目,强烈推荐使用 ES Modules (ESM)。它不仅是 JavaScript 的官方标准,而且与现代前端工具链(如 Vite、Next.js)兼容性更好,能够充分利用 Tree Shaking 等优化技术,减少打包后的代码体积。如果你正在维护一个老旧的 CommonJS 项目,并且不需要 ESM 的特性,可以继续使用 CommonJS。如果需要引入新的依赖或利用 ESM 的新特性,可以考虑逐步迁移,或者使用混合模式来过渡。
