☰
Node.js 导入导出实战:CommonJS 与 ES6 模块配置避坑指南
2026/9/27 22:43:47 网站建设 项目流程

1. 为什么你的 Node.js 项目总在 require 和 import 之间报错

刚接触 Node.js 模块化开发时,最容易卡住的地方不是写业务逻辑,而是文件之间的导入导出。你写了一个工具函数,想在其他文件里用,结果终端甩出一句Cannot use import statement outside a module,或者require is not defined in ES module scope,然后你就开始怀疑人生。

这个问题的根源在于 Node.js 同时存在两套模块系统:CommonJS 和 ES6(ECMAScript Modules,简称 ESM)。CommonJS 是 Node.js 早期的默认方案,用require()导入、module.exports导出;ES6 模块是 ECMAScript 标准方案,用import导入、export导出。两套系统语法不同、加载机制不同,甚至同一个.js文件在不同配置下会被当成不同的模块类型来解析。

适合阅读这篇文章的人:刚学 Node.js 后端开发、对模块化概念还比较模糊、遇到ERR_REQUIRE_ESM或Cannot use import statement outside a module不知道怎么排查的新手。我会用可复制的package.json配置和.mjs/.cjs文件骨架,把两种模块规范的导入导出方式讲清楚,再给出混用报错的逐步验证动作,让你一次跑通两种写法。

在开始之前,如果你需要调用大模型 API 来做一些模块化的 AI 功能测试,可以先用 TaoToken 的模型对话 快速验证接口返回格式,确认请求参数和响应结构没问题之后,再把它封装成 Node.js 模块集成到项目里。这样调试成本会低很多。

2. 先把环境配好:TaoToken 前置准备

在写模块化代码之前,如果你打算在项目里接入大模型能力(比如做一个翻译模块、摘要模块),需要先拿到 API Key。这一步很快,但后面所有请求都依赖它。

打开 TaoToken 控制台,注册或登录后进入 API Keys 管理页,创建一个新的 Key。建议按项目命名,比如node-module-demo,方便后续区分。

拿到 Key 之后,你的 Node.js 项目里可以通过环境变量读取,不要硬编码在源码里。下面是一个.env文件的示例:

TAOTOKEN_API_KEY=sk-你的实际Key TAOTOKEN_BASE_URL=https://taotoken.net/api

然后在代码里用process.env.TAOTOKEN_API_KEY读取。这样无论你用 CommonJS 还是 ESM,都能统一管理配置。

如果你后续要做长期编码或者 Agent 类项目,可以考虑 Coding Plan,它更适合需要持续调用模型的场景。接入细节可以参考 接入文档。

3. CommonJS 与 ES6 模块的可复制配置骨架

3.1 package.json 里的 type 字段决定一切

Node.js 判断一个.js文件用哪种模块系统,核心看package.json里的type字段。默认不写type,或者写"type": "commonjs",.js文件就按 CommonJS 解析;写"type": "module",.js文件就按 ES6 模块解析。

下面是一个完整的package.json骨架,你可以直接复制:

{ "name": "node-module-demo", "version": "1.0.0", "type": "commonjs", "scripts": { "start:cjs": "node src/cjs/app.js", "start:mjs": "node src/mjs/app.mjs" }, "dependencies": { "dotenv": "^16.4.5" } }

这里"type": "commonjs"表示默认走 CommonJS。如果你想整个项目走 ES6 模块,把值改成"module"即可。但更推荐的做法是:用文件扩展名来区分,.cjs强制 CommonJS,.mjs强制 ES6 模块,.js则跟随package.json的type字段。这样同一个项目里两种模块可以共存,不会互相干扰。

3.2 CommonJS 导出与导入写法

CommonJS 的导出核心是module.exports。它可以导出字符串、对象、函数,也可以挂载多个属性。

先看一个导出对象的例子:

// src/cjs/message.cjs module.exports = 'Hello World!';

导入时用require():

// src/cjs/app.cjs const msg = require('./message.cjs'); console.log(msg); // 输出: Hello World!

如果导出多个属性,可以这样写:

// src/cjs/person.cjs module.exports.name = 'Sachin'; module.exports.age = 20; module.exports.greet = function (name) { console.log('Welcome', name); };

导入后直接访问属性或调用方法:

// src/cjs/use-person.cjs const person = require('./person.cjs'); console.log(person.name, ',', person.age); // Sachin , 20 person.greet('Sachin'); // Welcome Sachin

注意一个容易踩的坑:exports和module.exports初始指向同一个对象,但如果你直接给exports赋值一个新对象,它就不再和module.exports关联了。所以导出单个函数或对象时,统一用module.exports = ...最稳妥。

// src/cjs/area.cjs function area(x) { return x * x; } module.exports = { area };
// src/cjs/use-area.cjs const square = require('./area.cjs'); console.log(square.area(5)); // 25

3.3 ES6 模块导出与导入写法

ES6 模块用export导出,用import导入。它支持命名导出和默认导出两种方式。

命名导出:

// src/mjs/details.mjs export const FirstName = 'sachin'; export const LastName = 'sahara';

导入时用花括号解构:

// src/mjs/app.mjs import { FirstName, LastName } from './details.mjs'; console.log(FirstName); // sachin

默认导出:

// src/mjs/greeting.mjs export default function greet(name) { console.log('Welcome', name); }

导入默认导出不需要花括号:

// src/mjs/use-greeting.mjs import greet from './greeting.mjs'; greet('Sachin'); // Welcome Sachin

ES6 模块还支持export { msg1, msg2 }这种集中导出写法,适合一个文件里导出多个变量:

// src/mjs/messages.mjs const msg1 = 'Hello'; const msg2 = 'World'; export { msg1, msg2 };

导入时同样用解构:

// src/mjs/use-messages.mjs import { msg1, msg2 } from './messages.mjs'; console.log(msg1, msg2); // Hello World

3.4 两种模块系统的核心差异对照

对比项CommonJSES6 模块
导入语法require()import
导出语法module.exports/exportsexport/export default
文件扩展名.js(默认)/.cjs.mjs/.js(type=module)
加载时机运行时同步加载编译时静态分析
顶层 thismodule.exportsundefined
是否支持动态导入原生支持用import()动态导入

这张表建议收藏,遇到报错时先对照检查自己用的是哪套语法、文件扩展名和package.json配置是否匹配。

4. 验证请求:跑通两种模块并调用 API

4.1 先验证 CommonJS 模块能正常导入导出

在项目根目录执行:

node src/cjs/app.cjs

如果输出Hello World!,说明 CommonJS 模块配置正确。再执行:

node src/cjs/use-person.cjs

输出Sachin , 20和Welcome Sachin,说明属性导出和方法导出都没问题。

4.2 再验证 ES6 模块能正常导入导出

执行:

node src/mjs/app.mjs

输出sachin,说明命名导出和导入正常。再执行:

node src/mjs/use-greeting.mjs

输出Welcome Sachin,说明默认导出正常。

4.3 在模块中调用 TaoToken API 验证

下面写一个 CommonJS 版本的 API 调用模块,用来验证 Key 和接口是否可用:

// src/cjs/ai-client.cjs const https = require('https'); function chatCompletion(prompt) { return new Promise((resolve, reject) => { const data = JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }] }); const options = { hostname: 'taotoken.net', path: '/api/v1/chat/completions', method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.TAOTOKEN_API_KEY}` } }; const req = https.request(options, (res) => { let body = ''; res.on('data', (chunk) => body += chunk); res.on('end', () => resolve(JSON.parse(body))); }); req.on('error', reject); req.write(data); req.end(); }); } module.exports = { chatCompletion };

调用方式:

// src/cjs/test-api.cjs require('dotenv').config(); const { chatCompletion } = require('./ai-client.cjs'); chatCompletion('用一句话解释什么是模块化').then((res) => { console.log(res.choices[0].message.content); });

执行node src/cjs/test-api.cjs,如果返回一段关于模块化的解释,说明 API Key 和网络请求都正常。

ES6 版本写法类似,只是导入导出语法不同:

// src/mjs/ai-client.mjs export async function chatCompletion(prompt) { const res = await fetch('https://taotoken.net/api/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.TAOTOKEN_API_KEY}` }, body: JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }] }) }); return res.json(); }

调用:

// src/mjs/test-api.mjs import 'dotenv/config'; import { chatCompletion } from './ai-client.mjs'; const res = await chatCompletion('用一句话解释什么是模块化'); console.log(res.choices[0].message.content);

注意 ES6 模块里可以直接用顶层await,不需要包在 async 函数里。执行node src/mjs/test-api.mjs验证结果。

5. 本篇常见报错排查

5.1 Cannot use import statement outside a module

这个报错的意思是:你在一个被 Node.js 当成 CommonJS 的文件里写了import语句。排查步骤:

第一步,检查文件扩展名。如果是.js,去看package.json里type字段是不是commonjs或者没写。如果是,要么把文件改成.mjs,要么把type改成module。

第二步,检查package.json的位置。Node.js 会从当前文件向上逐级查找最近的package.json,如果子目录里有一个package.json覆盖了配置,也会导致解析行为不一致。

第三步,确认没有在 CommonJS 文件里混用import。CommonJS 只能用require()。

5.2 require is not defined in ES module scope

这个报错反过来:你在 ES6 模块里用了require()。ES6 模块没有require、__dirname、__filename这些 CommonJS 特有的变量。

解决办法有两种。第一种,改用import语法。第二种,如果必须用require,可以通过createRequire创建:

// src/mjs/use-require.mjs import { createRequire } from 'module'; const require = createRequire(import.meta.url); const pkg = require('./package.json'); console.log(pkg.name);

5.3 ERR_REQUIRE_ESM

当你用require()去加载一个 ES6 模块文件时,会报这个错。比如require('./details.mjs')就会触发。

解决方式:要么把被加载的文件改成 CommonJS(.cjs),要么在 CommonJS 里用动态import():

// src/cjs/load-esm.cjs import('./details.mjs').then((mod) => { console.log(mod.FirstName); });

注意import()返回的是 Promise,所以要用.then()或await。

5.4 找不到模块路径

require('./message')不写扩展名时,Node.js 会按.js、.json、.node的顺序查找。但在 ES6 模块里,import必须写完整扩展名,import './message'会直接报错,必须写成import './message.mjs'或import './message.js'。

另外,导入文件夹时,CommonJS 会找index.js,ES6 模块不会自动找index.js,需要显式写import './folder/index.mjs'。

5.5 混用导致的循环依赖问题

CommonJS 和 ES6 模块对循环依赖的处理方式不同。CommonJS 在循环依赖时可能拿到不完整的module.exports,而 ES6 模块通过静态分析能更好地处理。如果你的项目里两个模块互相导入,建议统一用一种模块系统,不要混用。

6. 继续深入:从模块化到实际项目接入

模块化配置跑通之后,下一步就是把它用到真实项目里。如果你在做 AI 相关的 Node.js 后端,建议把 API 调用封装成独立模块,CommonJS 和 ES6 各维护一份适配层,业务代码只依赖统一的接口。

需要长期编码或做 Agent 项目的,可以看看 Coding Plan,它更适合持续调用模型的场景。接入过程中遇到参数问题,可以对照 接入文档 排查。如果你只是想先验证模型返回格式,用 模型对话 快速试一下就行。

最后提醒一个实际开发中的小技巧:在package.json里加一个"engines"字段锁定 Node.js 版本,比如"node": ">=18.0.0",因为 ES6 模块的顶层await和fetch在低版本 Node.js 里不可用。这样团队协作时不会因为版本差异导致模块解析行为不一致。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询