为什么会有两套模块系统?
Node.js 诞生时,JavaScript 还没有官方的模块化标准,于是社区推出了 CommonJS 规范,也就是我们熟悉的 require
和 module.exports
。
后来,JavaScript 官方在 ES6 中推出了 ES Modules(ESM),也就是 import
和 export
。但 Node.js 早期无法直接支持 ESM,直到 13.2 版本才默认支持。于是,两种模块系统“共存”至今。
require 和 import 的核心区别
加载方式:一个“同步”,一个“异步”
require
:同步加载模块,代码执行到这一行时,才会去读文件、解析依赖。// 可以动态加载 (比如根据条件判断) if (user.isVIP) { const vipModule = require('./vip.js'); }
import
:异步加载模块,代码在编译阶段就确定了依赖关系,路径必须是静态字符串。// 静态路径,不能写在条件语句里! import vipModule from'./vip.js'; // 动态加载需用 import() 函数 (返回 Promise) if (user.isVIP) { const vipModule = await import('./vip.js'); }
总结:require
更灵活,但 import
性能更好 (依赖预加载)。
导出和导入:写法完全不同
CommonJS(require):
// 导出 module.exports = { name: '张三' }; // 或 exports.name = '张三'; // 导入 const user = require('./user.js'); console.log(user.name); // 张三
ES Modules(import):
// 导出 export const name = '张三'; // 或默认导出 export default { name: '张三' }; // 导入 import user from'./user.js'; // 默认导出 import { name } from'./user.js'; // 命名导出
注意:ESM 的 export default
对应 CommonJS 的 module.exports
,而 export
对应 exports.xxx
。
模块是“拷贝”还是“引用”?
require
:导入的是模块的拷贝。如果原模块的值变了,导入的不会跟着变。// counter.js let count = 0; exports.increment = () => count++; // app.js const { increment } = require('./counter.js'); increment(); console.log(count); // 报错!count 是 counter.js 内部的变量
import
:导入的是模块的实时引用 (但变量本身是只读的)。// counter.mjs export let count = 0; export const increment = () => count++; // app.mjs import { count, increment } from'./counter.mjs'; increment(); console.log(count); // 1(实时更新)
文件扩展名和配置
require
:默认支持.js
和.cjs
(CommonJS 文件)。import
:需将文件后缀改为.mjs
,或在package.json
中设置:{ "type": "module" } // 所有 .js 文件视为 ESM
最佳实践:到底该用哪个?
新项目优先用 import(ESM)
- 优势:符合现代标准、静态分析友好、浏览器原生支持。
- 场景:前端项目、新 Node.js 服务、需要 Tree-Shaking 优化时。
旧项目继续用 require(CommonJS)
- 优势:兼容老代码、支持动态加载。
- 场景:维护旧 Node.js 服务、依赖未支持 ESM 的第三方库。
不要混用!
混用可能导致诡异问题。如果必须混用:
- 在 ESM 中引入 CommonJS:直接
import
,但需确保 CommonJS 模块有默认导出。 - 在 CommonJS 中引入 ESM:用
import()
动态加载 (返回 Promise)。
实战:用 import 写一个 Koa 服务
假设你已经安装了 Node.js v16+,跟着以下步骤操作:
初始化项目
mkdir koa-demo&& cd koa-demo
npm init -y
安装 Koa
先配置 npm 包安装源,新增 .npmrc
文件,内容如下
registry=https://registry.npmmirror.com
然后执行如下命令
npm install koa --save
创建 ES Modules 文件
在 package.json
中添加:
{ "type": "module" } // 启用 ESM
编写 src/app.js
import Koa from 'koa';
const app = new Koa();
// 中间件:记录请求耗时
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const duration = Date.now() - start;
ctx.set('X-Response-Time', `${duration}ms`);
});
// 路由
app.use(async (ctx) => {
ctx.body = 'Hello, 我是用 import 运行的 Koa 服务!';
});
// 启动服务
app.listen(3000, () => {
console.log('服务已启动:http://localhost:3000');
});
运行
node src/app.js
访问 http://localhost:3000
,你会看到欢迎信息!
总结
require
:传统、灵活,适合老项目。import
:现代、高效,未来趋势。- 选型建议:新项目无脑选
import
,旧项目按需迁移。
接下来,我将在 koa-demo 的基础上进行功能开发,新增通过 Markdown 发送邮件的功能。同时,我将使用 ESM 改造我的前端工具包 hui-vue
。你的项目还在使用 require
吗?