一文搞懂 Node.js 中 require 和 import 的区别及最佳实践

为什么会有两套模块系统?

Node.js 诞生时,JavaScript 还没有官方的模块化标准,于是社区推出了 CommonJS 规范,也就是我们熟悉的 requiremodule.exports

后来,JavaScript 官方在 ES6 中推出了 ES Modules(ESM),也就是 importexport。但 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 吗?