云计算百科
云计算领域专业知识百科平台

分离 Express 应用与服务器:Node.js 项目结构中的 app/server 拆分实践

  • 文档
  • 教程
  • 后端

【免费下载链接】nodebestpractices

✅ The Node.js best practices list (July 2026)

项目地址:
https://gitcode.com/GitHub_Trending/no/nodebestpractices

点击查看 免费下载

本篇文章源自 nodebestpractices(The Node.js best practices list,Node.js 最佳实践清单)的"项目结构(Project Structure)"章节。该章节的核心主张是:把 Express 的 应用声明(app) 与 服务器网络配置(server) 拆分为两个文件。本文将完整展开这一实践的原理、双语言代码范式、进程内测试方法,并结合仓库源码与示例,说明它如何提升可测试性、可部署性与代码整洁度。

为什么要把 app 与 server 分离

最新版本的 Express 应用生成器自带一项值得保留的实践:API 的声明与网络相关配置(端口、协议等)彼此分离。换言之,负责"应用长什么样"(路由、中间件、业务逻辑)的代码,与负责"应用如何监听网络"(端口号、HTTP 协议、进程启动)的代码,应当放在不同的文件中。

这一拆分带来的核心收益有四点:

  • 进程内测试(in-process testing):测试可以直接引用 app 模块而无需发起真实网络调用。由于省去了端口绑定、socket 通信、序列化/反序列化开销,测试执行速度更快,也能稳定获取代码覆盖率指标;
  • 灵活的网络部署条件:同一份 API 代码可以在不同的网络环境中复用——本地开发用 3000 端口、生产环境由编排平台注入端口、测试环境甚至完全不监听端口,只需在启动层决定;
  • 更好的关注点分离(separation of concerns):中间件、路由与网络配置不再纠缠在同一个文件里;
  • 更清晰的代码结构:每个文件的职责单一,review 和定位问题时只需按职责找文件。
  • 从仓库的文档组织看,这一实践位于 sections/projectstructre 目录下,与"将 Express 保持在自身边界内"的分层实践(createlayers.md)同属一个主题序列:后者从纵向上把组件划分为 entry-points(入口层)、domain(领域层)、data-access(数据访问层),而本实践从横向上先把"应用本体"与"网络入口"切割开——可以看作是分层思想在 Express 项目里最轻量、最易落地的一步。

    app.js / app.ts:只声明 API,不监听端口

    按照该实践,API 声明应当位于 app.js(或 TypeScript 项目的 app.ts)中。这里只做三件事:创建 Express 实例、挂载全局中间件、挂载路由模块:

    const app = express();
    app.use(bodyParser.json());
    app.use('/api/events', events.API);
    app.use('/api/forms', forms);

    关键点是:这段代码没有任何 app.listen() 调用。文件在模块加载完成后即返回一个可被其他模块引用的 app 实例。它既可以被 /bin/www 这样的启动器拿去创建服务器,也可以被测试框架直接拿去发起进程内请求——这正是后面"进程内测试"能够成立的前提。

    从仓库示例看,sections/examples/dockerfile/src/app.ts 中的最小 Express 应用采用了同一形态:先 import * as express from 'express' 并创建 const app = express(),再定义路由处理器。虽然该示例为了演示的简洁性直接调用了 app.listen(3000)(将其作为 Docker 演示入口),但在正式项目中,这正是应当被拆分出去的那一行——把"创建应用"与"启动监听"从同一个文件里剥离开来。

    /bin/www:服务器网络声明的唯一归属

    网络层的声明应当位于 /bin/www(Express 生成器约定的启动器文件位置)。它的职责是:从环境变量读取端口、把端口写入 Express、创建 HTTP 服务器:

    const app = require('../app');
    const http = require('http');

    // Get port from environment and store in Express.
    const port = normalizePort(process.env.PORT || '3000');
    app.set('port', port);

    // Create HTTP server.
    const server = http.createServer(app);

    对应 TypeScript 版本:

    import app from '../app';
    import http from 'http';

    // Get port from environment and store in Express.
    const port = normalizePort(process.env.PORT || '3000');
    app.set('port', port);

    // Create HTTP server.
    const server = http.createServer(app);

    这里有三个值得注意的实现细节:

    • normalizePort:这是 Express 生成器自带的工具函数,负责把端口值规范化为合法数字、具名管道或 false(表示端口无效),确保后续 server.listen() 拿到的是安全可用的端口值;
    • process.env.PORT || '3000':端口优先从环境变量注入,缺省回退到 3000。这让同一个 API 在本地、CI、容器和集群环境中都可以复用——容器平台只需注入 PORT 环境变量即可改变监听端口,无需改动任何代码;
    • http.createServer(app):把 Express 应用实例作为请求处理回调交给 Node 原生 HTTP 服务器。这意味着协议层(HTTP/HTTPS/HTTP2)的切换发生在 /bin/www 这一层,app 模块本身完全不感知网络协议细节。这也印证了分层文档 createlayers.md 中的论断:当领域/应用层不感知任何边缘协议时,它就能服务于任意客户端,而不只是 HTTP 调用。

    进程内测试:用 supertest 在不启动网络的情况下验证 API

    分离 app 与 server 最直接的收益体现在测试环节。supertest(原文档称之为"流行的测试包")允许直接把 Express 应用实例传入并发起 HTTP 风格的请求断言,全程不绑定端口、不做真实网络调用:

    const app = express();

    app.get('/user', (req, res) => {
    res.status(200).json({ name: 'tobi' });
    });

    request(app)
    .get('/user')
    .expect('Content-Type', /json/)
    .expect('Content-Length', '15')
    .expect(200)
    .end((err, res) => {
    if (err) throw err;
    });

    TypeScript 版本(带类型标注):

    const app = express();

    app.get('/user', (req: Request, res: Response) => {
    res.status(200).json({ name: 'tobi' });
    });

    request(app)
    .get('/user')
    .expect('Content-Type', /json/)
    .expect('Content-Length', '15')
    .expect(200)
    .end((err: Error) => {
    if (err) throw err;
    });

    这段代码演示了 supertest 的核心用法:request(app) 直接接收 app 实例;.get('/user') 指定方法与路径;链式 .expect(…) 可以同时断言响应头(Content-Type、Content-Length)与状态码(200);最后 .end(callback) 收尾,回调中抛出任何断言错误。

    这种测试方式的优势与文件拆分直接相关:测试只需 require('../app')(或 import app from '../app')就能拿到完整的应用实例,而 /bin/www 里的端口逻辑被完全旁路——不需要关心端口是否被占用、不需要等服务器就绪、不产生额外的网络开销。配合 sections/testingandquality 目录下的测试最佳实践(如 AAA 模式 aaa.md、测试命名三段式 3-parts-in-name.md、中间件测试 test-middlewares.md),可以构建出又快又稳的 API 测试套件。

    与容器化部署的配合

    app/server 拆分在容器化场景中同样适用。仓库的 Docker 示例 展示了这一点:Dockerfile 的构建阶段先执行 npm ci(以 lockfile 为准)再 npm run build,运行阶段则通过

    CMD [ "node", "dist/app.js" ]

    启动构建产物(见 Dockerfile,对应 package.json 中的 build: tsc –outDir dist … 脚本)。容器启动命令指向的正是应用入口文件——这与"应用模块(app)与进程启动(entry)分离"的思路一脉相承:镜像内部通过 EXPOSE 3000 声明端口,实际端口在运行时由 PORT 环境变量注入,应用代码本身不硬编码监听端口。这也呼应了仓库 Docker 章节中关于生产镜像、多阶段构建的最佳实践。

    落地建议:从拆分到分层

    综合本节文档(separateexpress.chinese.md 与英文原版 separateexpress.md 同属该主题)及 createlayers.md 的论述,落地时可以遵循以下递进式步骤:

  • 第一步(本实践):把 app.listen(…) 从 app 模块中移除,将端口与服务器创建逻辑迁入 /bin/www 之类的启动器文件,app 模块仅保留实例创建、中间件与路由挂载;
  • 第二步:把启动器与业务模块进一步解耦,让 app.js 成为一个纯导出模块,便于测试与复用;
  • 第三步(进阶):参考分层实践,在组件内部按 entry-points(入口/控制器)→ domain(领域/业务)→ data-access(数据访问)三个层次组织代码,让 Express 彻底保持在边界之内——入口层负责适配请求与响应,领域层只接收协议无关的普通 JavaScript 对象,数据访问层负责与数据库交互。
  • 完成这三步后,项目将获得:毫秒级的进程内测试、可移植的网络部署、清晰的职责边界——这正是 Node.js 项目结构最佳实践希望达成的最终状态。

    赞

    分享

    • 文档
    • 教程
    • 后端

    【免费下载链接】nodebestpractices

    ✅ The Node.js best practices list (July 2026)

    项目地址:
    https://gitcode.com/GitHub_Trending/no/nodebestpractices

    点击查看 免费下载

    上一篇:
    IPTVnator 官网博客封面设计:一张“官方信号”插画如何贯通 Astro 内容集合与 Open Graph 元数据

    下一篇:
    BetterNCM完整安装指南:3分钟搞定网易云插件管理终极方案

    创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 分离 Express 应用与服务器:Node.js 项目结构中的 app/server 拆分实践
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!