前端工程化地图
“工程化”这个词听起来很大,但它要解决的问题都很具体:
- 同一个项目,在同事电脑上能跑,在你电脑上报错;
- 每次提交都夹带着几百行格式变化,真正的改动被淹没了;
- 本地检查都过了,上线后才发现类型错误;
- 线上出了问题,没人说得清现在跑的是哪个版本的代码。
这篇先画出一张完整的地图,再逐个解决这些问题。
读完这篇你会
- 说清从写代码到上线观察的每一层工具在解决什么问题
- 让依赖、格式、检查在每个人的电脑和 CI 上保持一致
- 判断什么时候该引入一个新工具,什么时候不该
1. 原理:在流水线上设置质检站
把软件交付想成一条工厂流水线,工程化就是在流水线上设置质检站:
text
写代码 → 格式化/Lint → 类型检查 → 自动化测试 → 构建 → 预览/发布 → 线上监控
│ │ │ │ │ │ │
└── 越往左发现问题,修复成本越低;越往右,代价越高(甚至影响用户)──┘一个拼写错误,在编辑器里被标红只需要几秒钟就能改;如果到了线上才被用户发现,就要经历排查、修复、重新发布,还可能造成损失。工程化的核心价值,就是把问题尽量拦在左边。
| 层 | 解决什么问题 | 常见工具 |
|---|---|---|
| 依赖管理 | 每个人装出来的依赖一样吗 | npm / pnpm、锁文件 |
| 开发服务 | 修改后能不能马上看到效果 | Vite 开发服务器、热更新 |
| 代码质量 | 低级错误能不能在提交前发现 | Prettier、ESLint、TypeScript |
| 测试 | 功能改坏了能不能自动发现 | Vitest、Playwright |
| 构建发布 | 产物是否可复现、可追溯 | Vite / Webpack、CI 流水线 |
| 线上观察 | 出了问题能不能发现、定位 | 错误监控、性能指标、日志 |
2. 统一入口:每个项目都有同一组命令
无论用什么框架和工具,项目都应该提供一组统一的脚本:
json
{
"scripts": {
"dev": "vite",
"lint": "eslint .",
"typecheck": "vue-tsc --noEmit",
"test": "vitest run",
"build": "vite build"
}
}新同事拿到项目,不需要知道底层用了哪些工具,只要会 npm run dev、npm run build 就能开始工作。CI 也调用同一组脚本,而不是在服务器上另写一套构建逻辑,这样本地和 CI 的行为才一致。
3. 坑一:同一个项目,每个人装出来的依赖不一样
现象:项目在同事电脑上正常,你刚克隆下来安装依赖后就报错;或者 CI 上某天突然构建失败,代码一行没改。
原因:
- 没有提交锁文件(
package-lock.json、pnpm-lock.yaml),每次安装都可能拿到依赖的新版本; - CI 用的是
npm install,它会根据package.json里的版本范围更新锁文件; - 团队里有人用 npm、有人用 pnpm,各自生成不同的锁文件;
- Node.js 版本不一致。
修复:
- 锁文件必须提交到仓库;
- CI 使用
npm ci(或pnpm install --frozen-lockfile):严格按照锁文件安装,锁文件和package.json不一致时直接报错; - 在
package.json里声明packageManager字段,统一包管理器和版本; - 用
.nvmrc或engines字段声明 Node.js 版本。
json
{
"packageManager": "pnpm@9.12.0",
"engines": { "node": ">=20" }
}4. 坑二:格式之争和巨大的 diff
现象:评审时一半的评论在讨论“这里应该用单引号还是双引号”;某人保存文件时编辑器自动格式化,一次提交改了几百行,真正的业务改动只有三行。
修复:
- 用 Prettier 统一格式,用
.editorconfig统一缩进和换行,格式问题从此交给工具,评审不再讨论; - 第一次引入格式化时,单独用一次提交格式化整个仓库,不要和业务改动混在一起;
- 把这次格式化提交的哈希写进
.git-blame-ignore-revs文件,git blame时跳过它,不影响追溯代码的历史。
5. 坑三:本地检查都过了,CI 或线上却出了问题
现象:开发者说“我本地跑过 lint 了”,但合并后 CI 报类型错误;或者有人跳过了提交钩子,把有问题的代码推了上去。
原因:只依赖“每个人自觉在本地跑检查”,或者本地和 CI 跑的检查不一样。
修复:
- 提交前钩子(如 husky + lint-staged)只检查本次修改的文件,速度快,用来尽早给出反馈;
- CI 才是最终的把关者:它在干净的环境里跑完整的 lint、类型检查、测试和构建,不通过就不能合并。提交钩子可以被跳过,CI 不能;
- 本地脚本和 CI 调用同一组命令。
6. 坑四:说不清线上跑的是哪个版本
现象:用户报告了一个 bug,开发者在本地最新代码里怎么都复现不了。后来发现线上跑的还是三天前的版本,那个 bug 早就修了,只是还没发布。
修复:构建时把版本信息注入到产物中:
js
// vite.config.js
import { execSync } from 'node:child_process'
const commit = execSync('git rev-parse --short HEAD').toString().trim()
export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify(commit),
__BUILD_TIME__: JSON.stringify(new Date().toISOString())
}
})js
// 应用启动时打印出来,错误上报也带上它
console.info(`version ${__APP_VERSION__}, built at ${__BUILD_TIME__}`)在页面底部、“关于”页面或控制台里展示版本号;错误监控上报时带上版本号,就能知道问题出在哪个版本。
7. 坑五:工具越加越多,没人知道为什么要它
现象:项目里有三个 lint 工具、两套格式化配置,构建要跑五分钟。问起某个插件为什么存在,没人说得清,也没人敢删。
修复:每引入一个工具,都记录三件事:解决什么问题、谁负责维护、什么情况下可以移除。引入的理由应该是一个反复出现、可以观察到的痛点:
| 信号 | 合理的改进 |
|---|---|
| 同类格式问题反复出现在评审里 | 引入格式化工具,接入 CI |
| 修好的 bug 经常又回来 | 为关键流程补自动化测试 |
| 发布要靠某个人的电脑 | 把构建和部署写进 CI |
| 线上问题只能靠用户描述猜 | 接入错误监控和性能指标 |
“别的项目也在用”不是引入一个工具的充分理由。
排查思路
- 依赖问题:先确认 Node.js 和包管理器版本,再用
npm ls 包名查看依赖树,看是否装了多个版本或版本不对。 - 本地和 CI 结果不一致:对比两边的 Node 版本、安装命令、环境变量;在本地用
npm ci重新安装后再跑一次。 - 构建变慢:查看 CI 每个步骤的耗时,确认依赖缓存是否生效、是否有重复执行的检查。
- 线上问题:先确认线上版本号,再在对应版本的代码上复现。
检查清单
- [ ] 锁文件已提交,CI 使用
npm ci或等价命令 - [ ] 声明了包管理器和 Node.js 版本
- [ ] 格式化交给 Prettier,评审不讨论格式
- [ ] 本地和 CI 使用同一组脚本,CI 是合并前的最终把关
- [ ] 构建产物带有版本号,错误上报带上版本
- [ ] 每个工具都有明确的用途和维护人
延伸阅读
- Git 协作与提交边界:提交和分支的习惯
- 持续集成与发布:流水线怎么搭
- 构建工具选型:构建工具本身的选择和坑