从装一堆工具到看懂 Node 工程化思维:我的项目复盘记录

前言就一句话:这篇记录的是我个人的学习过程,不是什么保姆级教程,也不是最佳实践清单。

事情得从去年接手的一个 Node 项目说起。

先说一下我那个翻车的项目

项目不算大,两万行上下,Express + MongoDB。接手时打开 app.js ------ 八百多行。路由注册、数据库连接、静态资源配置、中间件注册、日志初始化,全在一个文件里。配置写在代码里,切换环境靠注释。package.json 的 dependencies 和 devDependencies 分不清,TypeScript 挂在 dependencies 里。README 写了三条命令,两条已经过时。

这就是典型的野生 Node 项目。能用,能跑,但谁都不敢动。

跟企业级工程化项目比,差距在哪?企业级项目不会把全部东西压在人脑记忆上------环境有人管、依赖有人分、目录有人分、上线有流程、回滚有方案。野生项目刚好反过来,所有负担都压在"上次那个谁记得"上面。

那时候我第一反应是:这项目缺工程化。第二反应是:那补上呗,装工具谁不会?

这时候我犯了个判断错:我以为工程化不是装插件,但我自己干的头一件事就是装插件。

我一开始以为工程化 = 装工具

那段时间的操作流程:ESLint、Prettier、Husky、lint-staged、Commitlint、TypeScript 定义、Dockerfile。装完觉得自己挺厉害。

工具到位了,该检查的能检查,该格式化的能格式化。但项目本质上没变化。不知道该往哪加新功能,配置切换还是靠注释,测试还是为零------代码耦合到没法写单测。工具都上齐了,什么都改不了。

卡住之后试过别的方向:强制代码评审,大家忙起来就跳流程。补工程化文档,写完没人翻第二次。拉专项会讲标准,下周照样各写各的。这些动作的共同问题,是全部压在人的自觉上------而人的自觉在工期面前最靠不住。

结果是三个状态一直没走出来:不可维护 ------谁都不敢改,改了不知道影响哪;不可协作 ------新人进来找不到门,看两个星期才能下手;不可上线------每次上线都像押注,出问题是大概率的。

这三个痛点,后文每个方案都在解决它们。

看上去,卡在哪了

这时候才意识到:问题不是工具没装全,是我从未搞明白工程化到底在解决什么。

改变是从一个动作开始的:顶着业务方的压力,把项目的目录结构重做了。

重新做了一遍目录结构

从原来全部平铺在根目录,改成按角色分目录:

bash 复制代码
project/
  config/        # 配置文件和配置读取逻辑
  utils/         # 纯工具函数,不依赖项目上下文
  middleware/    # 中间件独立,不混在路由文件里
  modules/       # 业务模块,路由/控制器/服务在一个目录
  core/          # 核心基础设施(数据库、缓存、日志)
  constant/      # 常量集中管理
  scripts/       # 工程脚本
  test/          # 测试
  app.js

这个动作跟"装工具"没有任何关系。但做完之后效果最明显。目录一分,职责边界就出来了。config/ 一出来,配置就有人管了。middleware/ 一出来,没人再把中间件往路由里塞。目录设计本身就是工程治理------这句话是亲手做完才理解的,不是从哪本书上看来的。

平铺目录最大的问题,是职责边界和协作边界会越来越模糊。你不知道一个文件到底属于哪个模块,所有人第一反应都是"再加一个文件",而不是"这个文件应该放哪"。新目录结构下,config 管配置,utils 管工具函数,middleware 管中间件,modules 管业务模块,core 管基础设施,constant 管常量。每层边界清晰,新人过来看路径就知道去哪找代码。

配置文件治理

之前配置写在代码里,开发和生产靠注释切换。后来改成了多环境 .env 隔离:.env.development.env.production.env.test。同时用 cross-env 处理跨平台环境变量差异------Windows 和 Linux 的环境变量设置语法不同,cross-env 统一了这层差异,这也是工程化链条里很容易被漏掉的一环。

然后我才重新理解了工程化在解决什么

目录结构理顺之后再回头想工程化这件事,才发现最开始的判断是反的。

工具解决的是"怎么做"的问题,但工程化首先解决的是"要做什么"的问题。装 ESLint 之前先得确定规范是什么,装 Husky 之前先得确定哪些检查应该在提交前执行,装 Docker 之前先得确定构建流程是什么。工具是最后的落地手段,不是决策本身。

Node 自由度太高,自由度越高越容易乱。工程化真正在做的是用四条线兜住这个乱:

规范化。命名、目录、依赖、提交日志。能规定的东西就规定,别靠商量,靠制度。

自动化。格式检查、测试、构建、部署。机器能做的事就别让人做。

分层解耦。配置、中间件、路由、业务、数据。每层有边界,改一层不影响另一层。

可观测可运维。日志、进程管理、异常捕获、优雅退出。上线不是终点,是运维的起点。

这四条线是全文主线,也是后面所有方案的核心------任何一个方案,服务不了其中至少一条,就不该上。

那条路走到后面,我补齐了什么

环境标准化

先用 nvm 锁定 Node 版本到 LTS,同时在 .nvmrcpackage.jsonengines 字段各约束一次。环境不统一会直接破坏协作基础------本地用 18、CI 用 16、线上用 14,三个环境三种表现,出了 bug 先排查"是不是版本问题",效率全耗在这上面。

包管理器与依赖边界

包管理器换成 pnpm。不是因为它新,而是 npm 扁平的 node_modules 在依赖冲突时排查成本太高,pnpm 的树形结构一致性更好。更重要的是划清 dependenciesdevDependencies 的边界:运行时导入的放 deps,编译、测试、类型定义放 devDeps。边界不划清,部署时经常多装不需要的包,或少装运行时需要的包。

package.json 的工程角色

很多人只把 package.json 当依赖清单。事实上它有四个字段承担了明确的工程角色:engines 声明运行环境兼容性,scripts 统一研发命令入口,main 指定 CommonJS 入口,module 指定 ES Module 入口。这些字段和运行、构建、发布直接相关,不是可填可不填的摆设。

模块规范统一

项目里 import 和 require 混着用,CJS 和 ESM 混用的代价很具体:类型推导不一致、打包需要额外转换处理、重构时容易报 ERR_REQUIRE_ESM。统一走 ESM 路线之后这些问题自然消失。先统一模块体系再扩展项目,这个顺序不能倒。

代码风格防线

ESLint 和 Prettier 装上了,但光装不够。格式问题不应该出现在代码评审里------靠人自觉维持的格式标准,工期一紧就会被人跳过。后来加了 Husky + lint-staged,在 Git hooks 里做提交前自动校验,质量防线前移到 commit 之前,而不是等人来发现。

提交日志规范

Commitlint 统一了 commit message 格式。commit 日志属于规范体系的一部分------日志不统一,回溯变更时没法靠标题快速定位那次提交改了什么,生成 Changelog 也全靠手动。日志统一直接影响协作效率和回溯能力。

TypeScript 的工程价值

之前对 TS 一直犹豫,觉得写起来更麻烦。直到线上因类型传错出过几次线上 bug,排查成本远大于写代码时把类型补上。TS 不是"写起来更麻烦的 JS",它的价值在于把运行时才会暴露的问题前置到开发阶段,这本身就是工程治理的一部分。tsconfig 配了 pathsbaseUrl,用别名代替 ../../../../utils/format 这种路径,重构时不用一个个改引用路径。

构建链路

构建工具选了 esbuild。构建速度比 Webpack 快一个数量级,产物直接就是 Node 能跑的 JS,不做无意义的拆包。构建速度影响开发体验,产物精简影响部署效率,这两个维度要同时兼顾------构建链路也是工程基座的一部分。

质量保障

之前的项目零测试,每次上线靠手工点几下接口。手工验证覆盖不了回归场景,每一次上线前的手工点验都不可重复、不可追溯。后来用 Jest 补了单元测试 + supertest 做接口自动化测试 + 覆盖率设了 80% 阈值,低于阈值 CI 不通。可测试性 = 可维护性------一个模块能不能写测试,直接反映它的耦合度。测试不是补充项,没有自动化的质量保障,工程化就不完整。

研发脚本统一

把常用操作统一成 devbuildlintteststart 五个命令。之前项目里"启动"这件事,不同人有不同记忆,有的用 npm start,有的用 node app.js,有的用 nodemon。统一之后不用再问"这项目怎么启动"。脚本工程化是上线稳定性的前提------部署脚本不会因为"那个人记得怎么配"而生效。

容器化与自动化交付

Docker 用了多阶段构建:构建阶段装依赖编译代码,运行阶段只拷贝产物和运行时依赖,镜像体积能缩 70% 以上。配上 CI/CD 自动化流程,从代码推送到构建、测试、部署走一条链,不需要人肉执行步骤。工程化必须覆盖从构建到交付的全链路。

环境配置

配置从注释切换改成了多环境 .env 隔离:.env.development.env.production.env.test。配上 cross-env 处理跨平台环境变量差异,Windows 和 Linux 之间不需要改脚本。多环境配置隔离和跨平台变量处理,看起来是小事,但都是工程化链条里容易被漏掉的环节。

进程守护与异常处理

Node 是单线程模型,一个未捕获异常能让整个进程退出。用 PM2 做了进程守护,写好了优雅退出的回调------收到 SIGTERM 时先停新请求,等正在处理的请求完成再退出。全局异常捕获也加上了,uncaughtExceptionunhandledRejection 都有日志记录。上线不是项目工程化的终点,而是运维期工程能力的起点。

后面这条路,我还在继续

上面这一套在一个项目里搭起来之后,新问题来了:下个项目要不要重新搭一遍?

方向有两个。一是自制脚手架,把目录结构、ESLint 配置、tsconfig 模板、测试框架这些基座做成工具,起新项目时一键生成。脚手架能保证所有项目有一致工程结构,差异只留到业务代码层面。

二是 Monorepo。当项目从一个变成多个------前端、后端、公共库------工程规范怎么共享、依赖怎么统一治理就成了新课题。还在试 pnpm workspace,没完全走通。

这条路的演进路径大概分三层:

  • 项目级:单项目工程基座搭稳
  • 团队级:脚手架保一致性,新人不用重新学一套
  • 架构级:Monorepo 统一多项目治理

你的项目,现在最痛的是哪个?

整件事走下来,最大的变化是终于分清楚了两层东西:工具只是在落地,思维才是决策。

翻车的根本原因,是以为装齐工具等于工程化完成。但工程化真正在做的,是把项目里靠人记住、靠人自觉、靠人商量的部分,一个一个变成靠制度、靠工具、靠自动化来解决。

对我自己来说,判断一个工程化动作值不值得做,就看三件事------它能不能少让一个人来问你"这个怎么配"、能不能让半年后的自己少骂几句当时的代码、能不能让上线的时候少出几次冷汗。说白了就是协作成本、维护成本、出错成本。三样里一样都不占的,就先不动它。

这条路我自己也还在走,远没到"搭完了"的状态。上面列的这些方向------环境标准化、目录分层、代码规范、测试覆盖、CI/CD------也不是一次性全做完的,就是一个痛点一个痛点补过来的。

如果你也在补 Node 工程化这条线,我比较好奇的是:你项目现在最让你头疼的是哪个方向?目录乱了、依赖分不清、还是测试为零?欢迎在评论区聊聊。

相关推荐
65岁退休Coder1 小时前
LangChain v1.3.4 笔记 - 05 Agent 上下文记忆
后端
颜酱1 小时前
04 | 召回前置准备:搭好召回所需的四个数据库
前端·人工智能·后端
JaneConan2 小时前
鸿蒙 韶非 UI 系列:能力调用 startAbilityForResult,跳能力拿回参,鸿蒙能力路由入门
后端·harmonyos
晴空了无痕2 小时前
从 Go 基础到 K8s:一条可落地的 Go 服务端成长路线
开发语言·后端·golang·kubernetes
用户8356290780512 小时前
如何使用 Python 在 Excel 中添加、编辑和删除超链接
后端·python
花椒技术2 小时前
原本要 2 天的服务端冒烟前置审查,为什么 3 分半就能出报告?|QA 质量交付实践(三)
后端·ai编程·测试
达达尼昂2 小时前
AI 编程的工程化实践:Flutter AI Harness 的设计与落地
人工智能·后端·全栈
IT_陈寒3 小时前
React的useEffect依赖项把我坑惨了
前端·人工智能·后端
凌虚3 小时前
基于 PostgreSQL WAL 构建 CDC 系统:原理与工程实现
数据库·后端·postgresql