1. 工程化认知与项目初始化

很多团队第一次把 Node.js 用于服务端时,起点往往是一个 server.js:几十行代码里塞进 HTTP 服务、路由判断、数据库连接和日志打印,靠 node server.js 启动。它能跑通,但一旦加入第二个人、第二个环境、第二个部署目标,问题就会集中爆发。所谓 Node.js 服务端工程化,本质上是把"能跑起来的脚本"变成"可协作、可复现、可交付、可演进的工程资产"。它关注的不是某个框架的 API,而是围绕代码之外的一整套约束与约定。

工程化要解决的真实痛点

先看几个高频场景。第一是环境漂移:本地 Node 18 跑得好好的,CI 上是 Node 16,构建产物在服务器上又因为 Node 20 的某个行为差异而报错。第二是依赖不可复现:package-lock.json 没提交,或者有人用 npm、有人用 pnpm,同一份 package.json 装出两套依赖树。第三是入口与脚本混乱:启动命令散落在 README、Dockerfile、CI 配置和运维文档里,改一次端口要改四处。第四是边界不清:把 Node.js 当成万能胶,既做长连接网关又做 CPU 密集的图像处理,结果事件循环被阻塞,接口 P99 抖动。

这些痛点的共同点是:它们都不在业务代码里,却直接决定项目能否稳定协作。工程化的目标就是把这些"隐性约定"显性化,写进项目骨架,让工具和新人能自动遵守。

能力边界:Node.js 适合做什么

在动手初始化之前,需要先明确边界。Node.js 基于单线程事件循环,擅长 I/O 密集型任务:HTTP 接口、BFF 聚合层、实时消息推送、SSR 渲染、CLI 工具、构建脚本。它不擅长 CPU 密集型任务:视频转码、大规模加解密、复杂图像处理、同步的大数据计算。这类任务应交给 Worker Threads、子进程,或干脆拆到独立服务。工程化的第一层认知,就是承认"不是所有后端逻辑都该由 Node.js 主线程承担",并在架构上预留拆分点。

用 package.json 搭建项目骨架

项目骨架的核心是 package.json。它不只是依赖清单,更是这个项目的"身份与契约"。执行 npm init -y 会生成一份最小文件,但真正可协作的骨架需要补齐几类字段。

{
  "name": "@acme/order-service",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "engines": {
    "node": ">=20.11.0 <21",
    "npm": ">=10.0.0"
  },
  "packageManager": "pnpm@9.1.0",
  "main": "dist/index.js",
  "scripts": {
    "dev": "node --watch src/index.js",
    "build": "tsc -p tsconfig.json",
    "start": "node dist/index.js",
    "test": "node --test"
  },
  "dependencies": {
    "fastify": "^4.26.0"
  },
  "devDependencies": {
    "typescript": "^5.4.0"
  }
}

逐项说明关键字段的作用。name 使用作用域命名(@acme/order-service),便于在私有 registry 中区分团队与项目。private: true 防止误发布到公共 npm。type: "module" 声明项目使用 ESM,避免 .mjs 与 CommonJS 混用带来的解析歧义。

engines 是环境约束的声明:它告诉使用者和 CI "这个项目期望 Node 20.11 以上、21 以下"。需要注意,engines 默认只是警告而非强制,若要让它在安装阶段直接失败,需要在项目根目录添加 .npmrc:

engine-strict=true

packageManager 字段(Corepack 规范)用于锁定包管理器及其版本。当团队成员执行 corepack enable 后,进入项目会自动使用声明的 pnpm 9.1.0,而不是各自机器上的 npm 或 yarn。这一条直接消灭了"依赖树不一致"的根源之一。

scripts 是团队的统一操作入口。约定 dev 用于本地热重载、build 产出可部署产物、start 只负责启动已构建结果、test 运行测试。这样 CI、Dockerfile、运维文档都只需引用 npm run start,不再各自拼命令。

骨架的协作闭环

把上述字段与配套文件放在一起,就形成了一条从"新人克隆仓库"到"服务可启动"的闭环。下图展示了这个骨架中各组件的职责与依赖方向。

Node.js 项目骨架的协作闭环 开发者 / CI corepack enable packageManager 锁定 pnpm@9.1.0 engines Node ≥20.11 <21 .npmrc engine-strict=true package.json name · type · scripts · deps 项目身份与操作契约 npm run dev node --watch 热重载 npm run build tsc 产出 dist/ npm run start node dist/index.js npm test node --test 统一入口:CI、Dockerfile、运维文档均引用同一组 scripts,避免命令散落

初始化后的验证动作

骨架搭好后,需要做一次可复现性验证,而不是"看起来没问题"。建议按以下顺序执行并观察结果:

  1. 在干净目录克隆项目,执行 corepack enable,再执行 pnpm install。观察终端是否自动使用 pnpm 9.1.0,而不是本机默认版本。
  2. 故意把本机 Node 切到 18,执行 pnpm install。由于 engine-strict=true,安装应直接失败并提示 engines 不满足,而不是静默通过。
  3. 执行 npm run build,确认 dist/index.js 生成;再执行 npm run start,确认服务监听端口并返回健康检查响应。
  4. 执行 npm test,确认测试框架能发现用例并输出通过/失败统计。

这四步分别验证了包管理器锁定、Node 版本约束、构建产物链路和测试入口。任何一步失败,都说明骨架还有缺口,应在提交前修复。

建议把 engines 与 packageManager 视为"团队契约"而非个人偏好。修改它们应当走代码评审,并在 PR 描述中说明影响范围,例如"升级 Node 20 → 22 需要同步更新 CI 镜像与 Dockerfile 基础镜像"。

常见误区

第一个误区是只写 engines 不加 engine-strict,以为约束生效,实际只是警告。第二个误区是把 packageManager 当成装饰字段,团队仍各自用不同包管理器,锁文件互相覆盖。第三个误区是把所有命令写进 README 而不写进 scripts,导致 CI 与本地行为分叉。第四个误区是忽略 private: true,在误操作时把内部服务发布到公共 registry。

工程化不是一次性动作,而是随着项目演进持续维护的约束体系。本章建立的骨架,是后续依赖管理、构建、测试、部署等所有环节的共同地基。把 package.json 的字段写清楚、把环境约束写死、把操作入口统一,团队才真正拥有一个可以协作的项目起点。