Skip to content

前端工程化地图 ​

“工程化”这个词听起来很大,但它要解决的问题都很具体:

  • 同一个项目,在同事电脑上能跑,在你电脑上报错;
  • 每次提交都夹带着几百行格式变化,真正的改动被淹没了;
  • 本地检查都过了,上线后才发现类型错误;
  • 线上出了问题,没人说得清现在跑的是哪个版本的代码。

这篇先画出一张完整的地图,再逐个解决这些问题。

读完这篇你会

  • 说清从写代码到上线观察的每一层工具在解决什么问题
  • 让依赖、格式、检查在每个人的电脑和 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
线上问题只能靠用户描述猜接入错误监控和性能指标

“别的项目也在用”不是引入一个工具的充分理由。

排查思路 ​

  1. 依赖问题:先确认 Node.js 和包管理器版本,再用 npm ls 包名 查看依赖树,看是否装了多个版本或版本不对。
  2. 本地和 CI 结果不一致:对比两边的 Node 版本、安装命令、环境变量;在本地用 npm ci 重新安装后再跑一次。
  3. 构建变慢:查看 CI 每个步骤的耗时,确认依赖缓存是否生效、是否有重复执行的检查。
  4. 线上问题:先确认线上版本号,再在对应版本的代码上复现。

检查清单 ​

  • [ ] 锁文件已提交,CI 使用 npm ci 或等价命令
  • [ ] 声明了包管理器和 Node.js 版本
  • [ ] 格式化交给 Prettier,评审不讨论格式
  • [ ] 本地和 CI 使用同一组脚本,CI 是合并前的最终把关
  • [ ] 构建产物带有版本号,错误上报带上版本
  • [ ] 每个工具都有明确的用途和维护人

延伸阅读 ​

为复用而记录,为理解而整理。