模块拆分需先确认项目类型(esm或commonjs),再按业务组件组织目录结构(apps/users/api/domain/data-access),导出模块时语法须匹配项目类型,跨组件调用必须通过接口契约抽象依赖。
☞☞☞AI 智能聊天, 问答助手, AI 智能搜索, 多模态理解力帮你轻松跨越从0到1的创作门槛☜☜☜

在Cursor中开发Node.js项目时,模块拆分直接影响代码可维护性、团队协作效率和后续测试覆盖。不按业务边界划分模块,会导致修改一个用户登录逻辑需要同时打开路由、控制器、服务、数据库模型四个文件,Git冲突频发,新人三天都找不到密码重置功能写在哪。
先确认项目类型再决定模块策略
打开终端,执行 cat package.json | grep type。如果输出 "type": "module",说明项目启用ES模块(ESM),必须用 import/export;若无此字段或值为 "commonjs",则沿用 require/module.exports。两种语法不可混用——【混用会导致运行时报错:ERR_REQUIRE_ESM 或 SyntaxError: Cannot use import statement outside a module】。
这一步必须做,否则后续所有模块拆分操作都会失败。
按业务组件组织目录结构
在项目根目录下创建 apps 文件夹,每个子文件夹代表一个自治业务单元:
mkdir -p apps/users apps/orders apps/payments
进入 apps/users,建立三层结构:
mkdir -p api domain data-access
其中:
• api 放 Express 路由和控制器(如 userRouter.js)
• domain 放纯业务逻辑(如 UserService.js,不含任何框架依赖)
• data-access 封装数据库操作(如 UserRepository.js,只暴露方法,不暴露 ORM 实例)
这种结构让 users 组件可独立测试、部署甚至抽离为微服务——只要接口契约不变,内部实现怎么改都不影响其他模块。
导出模块的两种写法及选择依据
方法一:ES模块写法(适用于 "type": "module" 项目)
在 apps/users/domain/UserService.js 中写:
将一个或多个视频上传至YouTube。当用户想“上传到YouTube”“发YouTube”“批量上传”“upload to YouTube”“post videos to YouTube”或发布到YouTube时使用此技能。
export function createUser(userData) { return db.insert(userData); }
export function findUserById(id) { return db.findById(id); }
在 apps/users/api/userRouter.js 中导入:
import { createUser, findUserById } from '../../domain/UserService';
方法二:CommonJS 写法(适用于传统 Node.js 项目)
在 apps/users/domain/UserService.js 中写:
const createUser = (userData) => db.insert(userData);
const findUserById = (id) => db.findById(id);
module.exports = { createUser, findUserById };
在 apps/users/api/userRouter.js 中导入:
const { createUser, findUserById } = require('../../domain/UserService');
注意:ES模块导入路径必须带扩展名或目录名(./UserService.js 或 ./UserService),CommonJS 可省略 .js 后缀。
跨组件调用必须通过显式接口
第一步:在 apps/users/api/index.js 中统一导出该组件对外能力:
export { userRouter } from './userRouter';
export { createUser, findUserById } from '../domain/UserService';
第二步:在 apps/orders/domain/OrderService.js 中需要查用户时,不能直接 require('../../users/domain/UserService') ——这会制造隐式强依赖。
正确做法是:定义接口契约 apps/lib/interfaces/UserServiceInterface.js:
export class UserServiceInterface { static async findUserById(id) { throw new Error('Not implemented'); }}
第三步:在 apps/orders/domain/OrderService.js 中依赖抽象:
import { UserServiceInterface } from '../../../lib/interfaces/UserServiceInterface';
第四步:启动时注入真实实现(例如在 app.js 中):
import { UserService } from '../apps/users/domain/UserService';UserServiceInterface.findUserById = UserService.findUserById;
这一步做完,orders 组件就不再耦合 users 的具体实现路径,未来换成 GraphQL 用户服务或远程 HTTP 调用,只需改注入点,不碰订单逻辑。






