新闻详情

新闻详情

首页 / 资讯中心 / 详情

Node.js模块化实战:CommonJS导入导出与Express项目代码拆分

发布时间:2026/9/7 17:26:44来源:尧图网络
Node.js模块化实战:CommonJS导入导出与Express项目代码拆分
Node.js 的模块化是所有做 Express.js 开发的人迟早要正面面对的问题。前面几篇我们写过路由、中间件、静态资源托管但多数示例代码都堆在一个app.js里。做小 Demo 没问题一旦功能变多一个文件几百行改一个接口要上下翻半天这时候就该重新组织代码了。这一篇我们专门把“Node.js 模块化”讲透。它的核心价值就三个拆文件、复用代码、管理依赖。拆文件解决“代码都挤在一起”的问题复用解决“同一个逻辑写好几遍”的问题依赖管理解决“谁用谁、谁先加载”的问题。Express.js 项目里的app.js、路由文件、数据库模型、工具函数全部依赖模块化机制来协作。本文会从环境准备开始把 CommonJS 的导入导出、模块加载规则、目录加载逻辑全部过一遍然后用一个真实场景把一个单文件 Express 应用改造成多模块工程。看完之后你不仅知道module.exports和exports有什么区别还能独立把一个 Express 项目拆成清晰的目录结构。1. 模块化核心能力速览能力项说明核心机制CommonJSNode.js 默认的模块系统导出方式module.exports、exports.xxx导入方式require()文件类型.js、.json、.node、目录模块缓存首次加载后缓存重复require返回同一对象目录加载自动查找package.json的main字段或index.js在 Express 中的用途拆分路由、控制器、服务层、工具函数、数据库模型现代趋势ES Modulesimport/export逐步迁移中这张表先建立一个整体印象。接下来我们逐项展开。2. 适用场景与使用边界模块化不是某个框架专属的东西它是 Node.js 平台本身提供的代码组织能力。它解决的核心问题代码组织把路由、业务逻辑、数据处理拆到独立文件维护成本直线下降。代码复用工具函数、校验逻辑、数据库连接可以在多个路由之间重复引用。命名隔离模块内部的作用域是独立的不会污染全局变量避免多人协作时变量名冲突。依赖关系清晰每个文件顶部的require就是它的依赖清单别人拿到项目看文件头部就明白模块之间的关系。适合用模块化的场景非常明确Express.js 接口开发、Node.js 脚本工具、CLI 工具、爬虫任务、定时任务。这些场景的共同特征是有多个功能点需要拆分和维护。不适合单文件硬拆的场景也有。一个非常小的脚本总共 20 行硬拆成三个文件反而增加成本。另一个情况是临时验证某个功能直接写在app.js里跑通即可。模块化要服务于可维护性不是为了“看起来工程化”而拆。使用边界方面需要注意几点。第一模块内部定义的变量默认不对外可见必须通过module.exports显式暴露这种设计保证了隔离性但新手容易忘记导出导致require拿到空对象。第二require是同步加载的如果模块内部有耗时操作会在加载时阻塞主流程因此不要把大量初始化逻辑直接写在模块顶层。第三循环引用会导致部分模块获取到不完整的导出对象项目复杂后要主动避免 A 依赖 B、B 又依赖 A 的结构。3. Node.js 运行环境准备写模块化代码之前先把 Node.js 环境准备好。模块化能力是 Node.js 平台自带的不需要额外安装任何包但前提是你得有一个能正常运行的 Node.js。3.1 安装 Node.js到 Node.js 官网下载对应操作系统的安装包。建议选择 LTS 版本因为 LTS 版本会获得更长时间的维护更新。从当前社区使用情况看Node.js 18 及以上版本比较稳妥如果需要使用较新的语法或工具链可以选择 Node.js 20 或 22 的 LTS 版本。Windows 用户直接下载.msi安装包一路下一步即可。macOS 用户可以使用安装包也可以通过 Homebrew 安装。Linux 用户可以从源码编译也可以使用包管理器安装但更推荐通过 nvmNode Version Manager管理多个 Node.js 版本。3.2 验证安装安装完成后打开终端执行以下命令node -v npm -v如果能看到版本号输出说明安装成功。例如v22.14.0 10.9.2需要特别留意的是有些环境会报类似错误报错提示 node.js v24.20.0 is not yet released or is not available这种问题通常是安装包版本号不对或者安装源没有同步最新版本。解决方案是换用 LTS 版本下载或者使用 nvm 安装指定版本。3.3 初始化项目进入你的项目目录执行npm init -y这会生成一个package.json文件记录项目元数据和依赖信息。在没有package.json的项目里执行npm install安装 Express 时npm 也会自动帮你生成这个文件。下面是一个典型的初始化结果{ name: express-module-demo, version: 1.0.0, description: , main: index.js, scripts: { test: echo \Error: no test specified\ exit 1 }, keywords: [], author: , license: ISC }注意main字段默认指向index.js。这个字段在目录加载规则中非常关键后面会详细说明。4. CommonJS 模块化的三种导入导出方式Node.js 默认使用 CommonJS 模块规范。每个文件都是一个模块文件内部定义的变量、函数默认是私有的外部拿不到。想对外暴露必须用module.exports或exports想引入其他模块用require()。下面分别介绍。4.1 module.exports 导出对象创建math.js// math.js function add(a, b) { return a b; } function subtract(a, b) { return a - b; } module.exports { add: add, subtract: subtract };创建app.js// app.js const math require(./math); console.log(math.add(2, 3)); // 5 console.log(math.subtract(5, 2)); // 3这种方式最直观整个模块暴露一个对象对象上挂函数。需要哪个功能就从math.add或math.subtract取。4.2 exports.xxx 导出属性exports是module.exports的引用别名。可以直接在exports上挂属性// logger.js exports.info function (message) { console.log([INFO], message); }; exports.error function (message) { console.log([ERROR], message); };引入方式const logger require(./logger); logger.info(服务启动中); logger.error(数据库连接失败);4.3 exports 与 module.exports 的区别这是新手最容易踩坑的地方。核心结论exports只是module.exports的一个引用。如果你给exports重新赋值切断了它与module.exports的关联外部拿到的是空对象。错误示范// 错误写法 exports { foo: bar };这样写外部require(./someModule)拿到的仍然是空对象。正确写法是直接使用module.exports// 正确写法 module.exports { foo: bar };什么时候用exports.xxx什么时候用module.exports当你要导出多个属性时两种都可以。当你需要导出一个类、一个函数、或者一个需要整体替换的对象时必须使用module.exports。4.4 module.exports 导出函数或类处理函数式逻辑时直接导出函数很常见// greet.js function greet(name) { return Hello, ${name}; } module.exports greet;引入const greet require(./greet); console.log(greet(Node.js));导出类的方式类似// user.js class User { constructor(name) { this.name name; } getProfile() { return { name: this.name }; } } module.exports User;引入const User require(./user); const user new User(张三); console.log(user.getProfile());5. 模块加载规则与查找顺序require(./math)里的路径不是随便写的。Node.js 有一套固定的查找规则理解之后排查依赖问题会轻松很多。5.1 文件模块加载当require后面跟一个相对路径或绝对路径时Node.js 会按以下顺序解析。1. 精确文件名 2. 文件 .js 3. 文件 .json 4. 文件 .node 5. 目录解析比如require(./data)会先找data、data.js、data.json。如果都找不到会把data当目录处理进入目录加载逻辑。.json文件不需要手动导出Node.js 会直接解析 JSON 内容作为模块对象// config.json { port: 3000, env: development }const config require(./config.json); console.log(config.port); // 30005.2 目录加载规则当require(./utils)而utils是一个目录时Node.js 会查找1. utils/package.json 的 main 字段 2. utils/index.js 3. utils/index.json 4. utils/index.node所以下面这种结构很常见utils/ ├── package.json └── index.js其中package.json里写{ main: index.js }如果不写main字段Node.js 默认找index.js。5.3 node_modules 查找当require(express)这种不带相对路径的写法出现时Node.js 会从当前目录开始逐级向上查找node_modules目录。查找顺序是当前目录/node_modules/express 上一级目录/node_modules/express 再上一级目录/node_modules/express ... 直到磁盘根目录这也是为什么npm install安装在项目的node_modules里你在项目任意子目录都能require到的原因。6. 实战用模块化重构一个 Express 应用现在进入实战环节。这个例子会完整演示如何把一个单文件 Express 应用拆分成多个模块。6.1 原始单文件版本先看一个典型的单文件app.js// app.js 单文件版本 const express require(express); const app express(); app.use(express.json()); function logRequest(req, res, next) { console.log([${req.method}] ${req.url}); next(); } app.use(logRequest); app.get(/users, (req, res) { res.json([ { id: 1, name: 张三 }, { id: 2, name: 李四 } ]); }); app.get(/users/:id, (req, res) { res.json({ id: Number(req.params.id), name: 测试用户 }); }); app.post(/users, (req, res) { const { name } req.body; res.status(201).json({ id: Date.now(), name }); }); app.listen(3000, () { console.log(server running at http://localhost:3000); });这个文件功能完整但所有代码挤在一起。如果继续加接口文件很快会膨胀。下面把它模块化。6.2 拆分后的目录结构express-module-demo/ ├── package.json ├── app.js ├── server.js ├── routes/ │ └── userRoutes.js ├── controllers/ │ └── userController.js ├── utils/ │ └── logger.js └── config/ └── index.js各文件职责app.js创建应用挂载中间件和路由。server.js启动服务监听端口。routes/userRoutes.js定义用户相关的路由映射。controllers/userController.js处理具体业务逻辑。utils/logger.js日志工具函数。config/index.js导出端口配置。6.3 工具模块 utils/logger.js// utils/logger.js function logRequest(req, res, next) { console.log([${req.method}] ${req.url}); next(); } function logMessage(level, message) { console.log([${level}] ${message}); } module.exports { logRequest, logMessage };6.4 配置模块 config/index.js// config/index.js const config { port: process.env.PORT || 3000, env: process.env.NODE_ENV || development }; module.exports config;这里演示了读取环境变量的方式也是模块化的好处之一配置集中管理。6.5 控制器模块 controllers/userController.js// controllers/userController.js function getUsers(req, res) { res.json([ { id: 1, name: 张三 }, { id: 2, name: 李四 } ]); } function getUserById(req, res) { const id Number(req.params.id); res.json({ id, name: 测试用户 }); } function createUser(req, res) { const { name } req.body; if (!name) { return res.status(400).json({ message: name is required }); } res.status(201).json({ id: Date.now(), name }); } module.exports { getUsers, getUserById, createUser };6.6 路由模块 routes/userRoutes.js// routes/userRoutes.js const express require(express); const router express.Router(); const userController require(../controllers/userController); router.get(/, userController.getUsers); router.get(/:id, userController.getUserById); router.post(/, userController.createUser); module.exports router;这里的Router是 Express.js 提供的模块化路由机制。一个文件导出一个路由实例其他模块通过require挂载使用。6.7 应用入口 app.js// app.js const express require(express); const userRoutes require(./routes/userRoutes); const { logRequest } require(./utils/logger); const app express(); app.use(express.json()); app.use(logRequest); app.use(/users, userRoutes); module.exports app;6.8 服务启动文件 server.js// server.js const app require(./app); const config require(./config); app.listen(config.port, () { console.log(server running at http://localhost:${config.port}); });到这里重构完成。原来的几十行代码被拆分到五个模块里每个模块职责单一可读性和可维护性明显提升。7. 接口联调与路由模块测试代码改完了需要验证接口是否正常工作。启动方式和平时启动 Express 应用一样node server.js启动成功后控制台输出server running at http://localhost:30007.1 测试用户列表接口打开另一个终端用 curl 测试curl http://localhost:3000/users预期返回[{id:1,name:张三},{id:2,name:李四}]7.2 测试用户详情接口curl http://localhost:3000/users/1预期返回{id:1,name:测试用户}7.3 测试创建用户接口curl -X POST http://localhost:3000/users \ -H Content-Type: application/json \ -d {name:王五}预期返回{id:1760000000000,name:王五}测试的时候还可以观察服务端控制台应该能看到日志模块输出的请求信息[GET] /users [GET] /users/1 [POST] /users这说明utils/logger.js被正确引用并执行了。7.4 启动失败的验证思路如果启动后页面打不开按以下顺序排查确认node server.js进程是否还在运行。确认终端输出有没有报错堆栈。确认端口是否被其他进程占用。用curl http://localhost:3000/users直接测试排除浏览器缓存因素。8. 资源占用与端口冲突处理模块化本身不引入额外资源开销不管拆成几个文件运行时都是同一个 Node.js 进程。资源占用主要来自 Node.js 进程本身、Express 框架、以及业务逻辑。但实际开发中端口冲突是一个非常常见的问题。8.1 查看端口是否被占用启动服务时如果看到这种报错EADDRINUSE: address already in use :::3000说明 3000 端口的进程没有正常退出。怎么查是谁占用了端口Windows 下执行netstat -ano | findstr :3000输出结果里最后一列是进程 PID。拿到 PID 后在任务管理器里查找对应进程或者用命令结束taskkill /PID 你的PID /FLinux / macOS 下执行lsof -i :3000或者netstat -anv | grep 3000找到 PID 后用kill -9 你的PID结束进程。8.2 修改端口如果 3000 端口必须留给其他程序可以从配置层修改端口。前面我们写了config/index.js支持环境变量覆盖端口set PORT8080 node server.jsLinux / macOS 下PORT8080 node server.js这样启动后服务会运行在 8080 端口。9. Node.js 模块化常见问题与排查方法下面把模块化开发中经常会遇到的问题整理成表格方便排查。问题现象可能原因排查方式解决方案require拿到的模块是空对象模块内部用了exports {}重新赋值检查模块导出代码改用module.exports {}模块导出的函数不能通过new调用使用exports.xxx导出了函数但导出的是属性不是构造函数本身检查导出方式用module.exports 构造函数修改模块内部函数的代码后运行结果不变Node.js 模块缓存导致旧模块被复用检查是否有缓存机制或进程未重启重启 Node.js 进程开发环境可用node --watch自动重启两个模块互相require导致某个变量是undefined循环引用检查 A 和 B 的依赖关系拆分公共依赖到第三个模块或延迟到函数内再requirerequire(./config)找不到模块目录中缺少index.js或package.json的main字段配置错误检查目录结构与main字段创建index.js或修正main指向启动时出现EADDRINUSE端口被其他进程占用用netstat/lsof查看端口占用结束占用进程或修改端口安装依赖报错node.js v24.20.0 is not yet released or is not availableNode.js 版本号不存在或安装源未同步检查当前 Node.js 版本切换到 LTS 版本不建议追非稳定版本require(express)报Cannot find module express当前目录缺少node_modules或安装目录不一致检查项目根目录是否有node_modules在项目根目录执行npm install模块顶层执行耗时同步操作启动变慢模块加载时同步执行大任务查看模块顶部是否有初始化逻辑将耗时操作封装为函数使用时再调用9.1 重点理解模块缓存模块缓存是排查“修改后没生效”问题时的关键知识点。Node.js 在第一次require某个模块后会把该模块的导出对象缓存起来。后续再require直接返回缓存里的对象不会重新执行模块代码。这是为了性能考虑但也意味着修改模块内容后必须重启进程才能生效。开发环境下可以使用 Node.js 提供的高级模式自动重启node --watch server.js这样每次修改文件保存后Node.js 会自动重启服务不用手动 CtrlC 再执行一次。9.2 循环引用的理解和规避循环引用指的是 A 模块requireB 模块B 模块又requireA 模块。这种情况下Node.js 不会无限循环执行而是在某个时刻返回不完整的导出对象导致拿到undefined。规避方法有三种把公共代码抽到第三个模块让 A 和 B 都依赖它。延迟require在函数内部而不是模块顶层引用。重新设计依赖方向避免双向依赖。对于 Express.js 项目前两种方法已经能解决绝大多数场景。10. 最佳实践与下一步模块化的代码组织方式直接影响项目长期维护成本。结合 Express.js 开发经验这里给出几条实用建议。10.1 第一次测试先保持小文件刚开始拆分模块时不要追求一次拆到位。先把最大的文件拆成两个跑通测试再继续拆。每次拆分后都要重新验证接口是否正常避免一次改动太大导致问题难以定位。10.2 按功能分层而不是按文件大小目录划分要有逻辑。routes放路由映射controllers放业务处理utils放通用工具config放配置。如果全部堆在一个目录里文件多了照样乱。10.3 导出方式保持统一习惯一个项目里尽量保持一致的导出习惯。函数工具模块用module.exports {...}导出对象类模块用module.exports ClassName。这样别人读代码时看到头部就知道这个模块暴露了什么。10.4 模块命名要能看出职责文件名要直接表达用途。userRoutes.js、userController.js、logger.js这种命名比a.js、b.js清晰得多。Node.js 生态里还有一种常见风格user.controller.js也可以但整个项目要保持一致。10.5 批量处理任务时的模块组织如果需要写批量处理脚本比如批量导入数据、批量生成报告建议把处理逻辑拆成三个模块入口脚本负责读取任务、处理模块负责单条数据处理、工具模块负责日志和错误记录。这样即使处理过程中出错也能快速定位是哪个环节的问题。10.6 了解现代 ES ModulesCommonJS 是 Node.js 默认方案但现代 JavaScript 生态正在向 ES Modulesimport/export迁移。Express.js 的许多新版本和工具链已经开始支持 ESM。学习模块化时至少要知道 ESM 的两种导入导出写法// 导出 export function getUsers() { // ... } export default function createApp() { // ... }// 导入 import { getUsers } from ./userController.js; import createApp from ./app.js;不过在实际项目落地时只要项目没有明确要求使用 ESMCommonJS 依然是稳妥的选择第三方包兼容性更好遇到问题时资料也更多。10.7 最容易踩的坑综合来说模块化入门阶段最容易踩的坑有三个exports与module.exports混用导致导出对象被意外覆盖。修改模块代码后没有重启进程怀疑“代码没问题但就是不生效”。目录加载规则不熟require一个目录时不知道该目录需要index.js或package.json的main字段。这三个坑对应的问题在上面的排查表里都能找到。建议把这一篇的内容和之前 Express.js 系列文章里的路由、中间件示例结合起来练一遍。动手拆一个自己的单文件app.js再把接口测试跑通模块化这部分就算真正上手了。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

ChatGPT在业财融合中的实践与优化 2026/9/7 18:08:52

ChatGPT在业财融合中的实践与优化

1. 项目概述:ChatGPT如何重塑业财融合 这份120页的PPT资料实际上是一份企业数字化转型的实战指南,重点探讨了如何利用ChatGPT这类AI技术重构传统的业财融合流程。我在去年为某跨国集团做财务系统升级时,就深刻体会到传统ERP系统与业务部门之间…

阅读更多 →
graphify 导出流水线全解:Wiki、Neo4j、FalkorDB、SVG、GraphML 与 MCP 服务的源码级实操 2026/9/7 18:08:52

graphify 导出流水线全解:Wiki、Neo4j、FalkorDB、SVG、GraphML 与 MCP 服务的源码级实操

graphify 导出流水线全解:Wiki、Neo4j、FalkorDB、SVG、GraphML 与 MCP 服务的源码级实操 【免费下载链接】graphify Turn any codebase, with its docs, SQL schemas, configs, and PDFs, into a queryable knowledge graph. A /graphify skill for Claude Code, C…

阅读更多 →
短视频自动化直播防重复内容技术方案 2026/9/7 18:08:52

短视频自动化直播防重复内容技术方案

1. 项目背景与核心挑战在短视频平台的直播生态中,自动化直播技术已经成为许多内容创作者提升运营效率的关键工具。然而近期不少使用自动化直播方案的用户反馈,系统频繁出现内容重复推送的问题,这不仅影响观众体验,更可能导致平台算…

阅读更多 →
微服务架构的实施与挑战:模式、优势与应对 2026/9/7 18:08:52

微服务架构的实施与挑战:模式、优势与应对

目录 一、微服务架构实施的前提 二、微服务实施的三大模式 (一)典型模式 (二)从无到有的实施 (三)混合式 三、实施微服务架构的优势 (一)六大技术优势 (二)业务与组织优势 四、实施微服务面临的挑战 (一)、技术架构的挑战 (二)、研发过程的挑战 五、总…

阅读更多 →
全球AI认知免疫力大普查:波普尔病毒终极审判 2026/9/7 18:08:52

全球AI认知免疫力大普查:波普尔病毒终极审判

《全球AI认知免疫力大普查:波普尔病毒终极审判》 摘要 本文档是对“本轮全球AI大模型波普尔可证伪病毒中毒程度指数试卷”的全面系统化整理与终局判定。本测试的核心目的在于,检验全球主流AI在面对“逻辑自洽性与经验证据优先级”这一元命题时&#xf…

阅读更多 →
marimo响应式数据流:从交互笔记本到可复现、可回滚的应用架构 2026/9/7 18:05:52

marimo响应式数据流:从交互笔记本到可复现、可回滚的应用架构

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞