
本文详解如何将 Express 应用从 CommonJS 迁移至 ES 模块(ESM),重点解决多路由导出失败、undefined 回调错误及控制器/路由模块化设计问题,提供可直接落地的代码结构与最佳实践。
本文详解如何将 express 应用从 commonjs 迁移至 es 模块(esm),重点解决多路由导出失败、`undefined` 回调错误及控制器/路由模块化设计问题,提供可直接落地的代码结构与最佳实践。
在 Node.js 中启用 ES 模块后,Express 应用的模块组织方式需同步调整——常见错误如 Route.get() requires a callback function but got a [object Undefined],本质源于 ESM 的导出/导入语义与 CommonJS 不同:ESM 不支持动态赋值导出,且 export default 仅能声明一次;多个路由必须导出同一个 Router 实例,而非多个独立路由方法。
✅ 正确的模块结构与导出方式
核心原则:路由文件应导出一个配置完成的 express.Router() 实例,而非单个路由句柄或多个未绑定的函数。
1. 控制器文件(controllers/shop.js):仅使用命名导出
// controllers/shop.js
export const getIndex = (req, res, next) => {
res.render('shop/index', {
pageTitle: 'Shop',
path: '/',
});
};
export const getProducts = (req, res, next) => {
res.render('shop/product', {
pageTitle: 'Products',
path: '/product',
});
};
// ❌ 删除 export default getIndex —— 命名导出已足够,重复默认导出会引发歧义2. 路由文件(routes/shop.js):配置 Router 并导出实例
// routes/shop.js
import express from 'express';
// 推荐显式导入(更清晰、利于 tree-shaking)
import { getIndex, getProducts } from '../controllers/shop';
const router = express.Router();
// 所有路由挂载到同一 router 实例
router.get('/', getIndex);
router.get('/product', getProducts);
router.get('/cart', (req, res) => res.render('shop/cart')); // 也可内联处理
// ✅ 关键:导出整个配置好的 router 实例
export default router;3. 入口文件(app.js):标准 ESM 导入与挂载
// app.js
import express from 'express';
import shopRouter from './routes/shop.js'; // 注意 .js 后缀(ESM 强制要求)
const app = express();
// 正确挂载:传入 router 实例
app.use(shopRouter);
// 其他中间件、端口监听等...
app.listen(3000, () => console.log('Server running on http://localhost:3000'));⚠️ 关键注意事项
- .js 后缀不可省略:ESM 在 import 语句中必须显式写出文件扩展名(如 ./routes/shop.js),否则会报 ERR_MODULE_NOT_FOUND。
- 禁用 export default 冗余导出:控制器中同时存在 export const getIndex 和 export default getIndex 会导致导入行为不一致(例如 import * as ctrl from ... 无法访问默认导出),应统一使用命名导出。
- *避免 ` as导入滥用**:虽语法合法,但会降低可读性与 IDE 支持;显式解构(import { getIndex } from ...`)更安全、更易维护。
-
确保 package.json 配置正确:
{ "type": "module", "engines": { "node": ">=18.0.0" } }Node.js ≥18 已默认支持 ESM,无需额外 flag。
✅ 总结:ESM 路由组织黄金法则
| 环节 | 正确做法 | 常见错误 |
|---|---|---|
| 控制器 | 仅用 export const fnName = (...) => {...} | 混用 export default + 命名导出 |
| 路由文件 | 创建 Router() → 链式注册所有路由 → export default router | 导出单个 router.get(...) 返回值(是 undefined) |
| 导入方 | 显式导入函数或 router,带 .js 后缀 | 省略后缀、误用 require() 或 import() 混用 |
遵循此结构,你不仅能解决多路由报错问题,还能构建出高内聚、低耦合、符合现代 JavaScript 规范的 Express 应用架构。











