
一句话:评估一个项目不是「看它用了什么技术」,而是看「它有没有让『改需求』这件事变便宜」——所以六个维度分别是:结构边界、数据流、性能可观测、工程质量、依赖与债、线上可观测,而每个维度都能用「一个具体的信号」在几分钟内判断。
一、先说「为什么评估」:三种场景
⭐⭐⭐ 三种「需要评估」的场景(⭐ 目标不同,信号也不同):
① 【接手一个陌生项目】(⭐ 最实用)
目标:「**我要多久能安全地改第一个需求**」
⭐ 关注:「能不能跑起来」「改一处会不会炸别处」「有没有测试兜底」「线上出问题看得见吗」
② 【面试时「反问」或「判断团队水平」】
目标:「**这个团队是「工程化」还是「能跑就行」**」
⭐ 关注:「CI/测试/lint 的严格度」「监控与告警」「依赖是否失控」「技术决策有没有记录」
③ 【技术决策前的「体检」】(⭐ 决定「要不要重写/升级」)
目标:「**哪些是可修的、哪些是要重写的**」
⭐ 关注:「耦合的边界在哪」「有没有「不敢动的模块」「测试覆盖率与真实质量」
⭐ 一个「15 分钟速查」的顺序(⭐ 从「最快得到信号」到「最慢」):
① `package.json` + 目录结构(⭐ 2 分钟:「技术栈、依赖数量、有没有边界」)
② 测试与 CI 配置(⭐ 2 分钟:「有没有兜底」)
③ 状态与数据流的入口文件(⭐ 5 分钟:「数据怎么流、有没有「散落的状态」」)
④ 一个「最复杂的页面」的组件(⭐ 3 分钟:「它是怎么写的」)
⑤ 监控/埋点/错误上报的初始化代码(⭐ 3 分钟:「线上看不看得见」)
二、六个维度与它们的「信号」
维度 1:结构与边界
| 好的信号 | 坏的信号 |
|---|---|
⭐ 目录按「业务域」划分(features/order/...) |
按「技术类型」堆(components/ 里有 300 个文件) |
⭐ 有「明确的边界」(features/* 不能互相 import,或通过 index.ts 暴露) |
「任何文件 import 任何文件」 |
⭐ 有「共享层」(shared/、lib/)与「业务层」的分界 |
「工具函数散落在各处」(utils.ts 有 2000 行) |
⭐ 有「布局/页面的分层」(app/ 只管路由与布局,业务在 features/) |
路由文件里「既写路由又写业务又写样式」 |
⭐ 「一个文件一个主要职责」(ProductCard.tsx 短) |
⭐ 「800 行的组件文件」(⚠️ 「不敢动」的信号) |
⭐⭐⭐ 一个「快速判断结构好坏」的问题(⭐ 面试/接手都能用):
⭐ 「如果我要改『订单退款』的逻辑,需要打开几个文件?」
· 「1~3 个」(且在同一个目录下)→ ✅ 边界清晰
· 「8 个以上,散落在 utils/components/store/api 里」→ ⚠️ 「没有边界」
⭐ 第二个问题:「如果我要删掉一个功能,会不会「不知道「还有哪些地方在用它」」」
· ✅ 「不会(有类型/测试/引用清晰)」→ 健康
· ⚠️ 「会(靠搜索 + 猜)」→ 债
维度 2:数据流与状态管理
⭐⭐⭐ 这是「React 项目健康度」最关键的维度(⭐ 大部分「难改」的根源在这里):
✅ 好的信号:
· ⭐ **「服务端状态」与「客户端状态」分清**(见 3.10):
接口数据用「数据层」(React Query/tRPC/GraphQL),
UI 状态用「本地 state / 轻量 store」
⚠️ **反面**:「把接口数据也塞进 Redux,然后自己管 loading/error/缓存」
· ⭐ **「数据的来源唯一」**:同一个数据「只有一个地方在管」
⚠️ **反面**:「既在 store 里,又在组件 useState 里」→ 必然不同步
· ⭐ **「状态尽量靠近使用处」**(见 5.4):能用 `useState` 就不上全局
· ⭐ **「服务端状态的缓存/失效有明确策略」**(见 7.4)
❌ 坏的信号:
· ⭐ **「状态散落」**:`useState` + Context + Redux + 自己写的 EventEmitter 混用
· ⭐ **「派生值被存进 state」**(要「手动同步」,见 3.15 的「派生值不持久化」同理)
· ⭐ **「Effect 里做数据获取 + 手动 loading/error/竞态」**(见 7.2)
· ⭐ **「全局 store 巨大」**(一个 store 有 100 个字段,谁都在里面)
· ⭐ **「用 Context 传高频变化的量」**(→ 全量重渲染,见 5.9)
/** ⭐⭐ 一个「30 秒判断数据流健康度」的探针(⭐ 直接在项目里搜) */
// 在代码库里搜这几个模式(⭐ 命中越多,债越多):
const BAD_SMELLS = [
// ① 「Effect 里手动拉数据」(应该用数据层)
/useEffect\(\s*\(\s*\)\s*=>\s*\{[\s\S]{0,300}fetch\(/,
// ② 「手动管理 loading/error」(应该用数据层)
/setLoading\(true\)/,
// ③ 「把 props 拷进 state」(⭐ 经典反模式)
/useState\(props\./,
// ④ 「派生值塞进 state + 手动同步」
/useEffect\([\s\S]{0,200}setSubtotal\(/,
// ⑤ 「在渲染里读 ref」
/\bref\.current\b(?=[\s\S]{0,80}return)/,
// ⑥ 「用 index 做 key」(⭐ 列表有状态时会错位)
/key=\{index\}/,
// ⑦ 「Effect 依赖失配」(会被 lint 抓,但常被 disable)
/eslint-disable-next-line react-hooks\/exhaustive-deps/,
];
// ⭐ 这些「不是绝对的错」,但「出现频率」是很强的信号:
// 一个「小而健康的项目」里,它们「几乎不出现」。
维度 3:性能「有没有被量化」
⭐⭐⭐ 核心问题不是「快不快」,而是「**团队知不知道它快不快**」:
✅ 好的信号:
· ⭐ **有性能预算**(「初始 gzip < 200KB」「LCP P75 < 2.5s」,见 5.14/5.11)
· ⭐ **有 RUM**(真实用户指标 + 归因,见 5.11)
· ⭐ **有体积门禁**(CI 里查体积,见 5.14)
· ⭐ **优化前先测量**(⭐ 「Profiler 录制 + 对比」,见 5.12)
❌ 坏的信号:
· ⭐ **「性能优化」全靠「感觉」与「经验」**(没有数据)
· ⭐ **「到处 memo」但「没有一次 Profiler 录制」**(⭐ 见 5.3:可能「负优化」)
· ⭐ **「首屏靠「加大 CDN」而不是「减包」」**
· 「性能问题只在「用户投诉后」才知道」
· ⭐ **没有「性能回归」的防线**(⭐ 一次「加个大依赖」就回到解放前)
⭐⭐⭐ 一个「5 分钟跑出来的硬指标」(⭐ 比「读代码」客观得多):
① ⭐ **产物大小**:跑一次构建 → 看「初始 chunk 的 gzip 总量」(见 5.14)
· < 250KB → 健康;> 500KB → 需要处理
② ⭐ **首屏请求瀑布**:开 DevTools Network → 看「关键路径上有几个串行请求」
· 「3 个以内」健康;「8 个以上串行」→ 有问题(见 5.16)
③ ⭐ **依赖数量**:`node_modules` 的「直接依赖数」
· 「30 个以内」健康;「100+ 直接依赖」→ 需要审计(见下文维度 5)
④ ⭐ **Lighthouse/RUM 的 LCP/INP/CLS**(⭐ 有线上数据优先看线上)
维度 4:工程质量(测试 / lint / 类型 / CI)
| 好的信号 | 坏的信号 |
|---|---|
⭐ strict: true(含 noUncheckedIndexedAccess) |
strict: false 或「满屏 as any」 |
| ⭐ 有 CI 跑「lint + typecheck + test + build」 | 「靠本地自觉」 |
| ⭐ 测试覆盖「关键路径」(下单、支付、鉴权) | ⭐ 「测试覆盖率 0%」或「只测了 utils」 |
| ⭐ E2E 覆盖「核心用户流程」 | 「所有依赖人工点」 |
| ⭐ 有 commit 规范 / PR 模板 / review 门禁 | 「直接 push main」 |
⭐ 依赖有 lock 文件且 CI 用 --frozen-lockfile |
⭐ 「lock 文件在 gitignore 里」 |
⭐⭐⭐ 「测试健康度」的判断要点(⭐ 不是「看覆盖率数字」):
① ⭐ **测试「测什么」**:是「行为」(用户能做什么)还是「实现细节」(某个函数被调用)
· 测行为的 → ✅ 「重构时不炸」
· 测实现的 → ⚠️ 「改实现就红」→ 团队就会「删测试」
⭐ 信号:「测试里有没有大量 `getByTestId`(实现)vs `getByRole`(行为)」(见 10.12)
② ⭐ **有没有「难测的部分」**:如果「核心逻辑」因为「耦合 DOM/请求」而「测不了」→ ⚠️ 结构问题
(⭐ 解法:「把业务逻辑抽成纯函数/service」,见 9.10)
③ ⭐ **测试「跑得快吗」**:单测「几秒」vs「几分钟」→ 影响「团队愿不愿意跑」
④ ⭐ **有没有「flaky 测试」**:⚠️ 「随机失败的测试」会「让团队忽视整个测试套件」(⭐ 这比「没测试」更糟)
维度 5:依赖与「技术债的可见度」
⭐⭐⭐ 「依赖健康度」的四个检查点:
① ⭐ **数量**:直接依赖「30 个以内」合理;「100+」通常「有大量可去重/可替换的」
② ⭐ **体积**:`npx vite-bundle-visualizer`(见 5.14)→ 「谁占了地方」
③ ⭐ **维护状态**:⚠️ 「一年没发版 + 大量 issue」的依赖 = 「未来升级的坑」
(⭐ 尤其「深度耦合」的:UI 库、表单库、构建工具)
④ ⭐ **重复依赖**:`npm ls <pkg>` → ⚠️ 「同一个包两个版本」
(⭐ 尤其「react 装了两份」→ 直接崩,见 5.14/10.9)
⭐⭐⭐ 「技术债的可见度」比「技术债的数量」更重要:
✅ 有债但「可见」:
· 有 `TODO`/`FIXME` 且「带 issue 链接」
· 有「已知问题清单」
· 有「废弃标记」与「迁移计划」(⭐ `@deprecated` + codemod,见 10.8)
❌ 有债但「不可见」(⭐ 更危险):
· 「没人知道」的隐式约定(⭐ 「这个字段必须手动同步」只存在于某人的记忆里)
· 「不敢动」的模块(⭐ 没有测试 + 没有文档 + 原作者已离职)
· 「文档与实现不一致」(⭐ 文档骗人比没文档更糟)
维度 6:线上可观测性
⭐⭐⭐ 「线上能不能看见」决定「你敢不敢改」:
✅ 好的信号:
· ⭐ **有错误监控**(Sentry 等)+ **source map 上传**(见 8.7)
· ⭐ **有 RUM**(LCP/INP/CLS + 接口耗时,见 5.11/7.13)
· ⭐ **有告警**(⭐ 且告警能定位到「版本/接口/页面」)
· ⭐ **有「发布与回滚」的机制**(⭐ 特性开关 / 秒级回滚,见 8.9/9.11)
· ⭐ **release 版本号贯穿前后端**(见 7.13)
❌ 坏的信号:
· ⭐ **「没有错误监控」**→ 「用户投诉才知道挂了」
· ⭐ **「上报没带 source map」**→ 「报错是一堆压缩后的字符」(见 8.7)
· **「告警太吵」**→ 团队「关掉告警」
· ⭐ **「回滚靠「重新部署上一个 commit + 等 10 分钟」」**
三、一个「评分卡」(⭐ 可直接用)
⭐⭐⭐ React 项目健康度评分卡(⭐ 每项 0~2 分,总分 24)
【结构与边界】(6 分)
□ 目录按业务域划分(0/1/2)
□ 有明确的「模块边界」且有强制手段(lint/tsconfig)(0/1/2)
□ 「改一个功能」需要打开的文件 ≤ 3(0/1/2)
【数据流】(6 分)
□ 服务端状态与客户端状态分清(0/1/2)
□ 同一个数据「只有一个来源」(0/1/2)
□ 没有「Effect 手动拉数据 + 手动 loading」(0/1/2)
【性能】(4 分)
□ 有「体积/性能预算」且 CI 检查(0/1/2)
□ 有 RUM(真实用户指标)(0/1/2)
【工程质量】(4 分)
□ `strict: true` + CI 跑 lint/typecheck/test/build(0/1/2)
□ 关键路径有测试(⭐ 且测的是「行为」)(0/1/2)
【依赖与债】(2 分)
□ 无「重复依赖」且「关键依赖在维护中」(0/1/2)
【可观测性】(2 分)
□ 有错误监控(带 source map)+ 有回滚手段(0/1/2)
⭐ 读分:
· 18~24:「健康」(⭐ 可以快速迭代)
· 10~17:「有债但可控」(⭐ 边改边还)
· < 10:「风险高」(⭐ 「先补兜底(测试/监控)再改」)
/** ⭐⭐ 一个「自动化体检」脚本的骨架(⭐ 把主观判断变成可重复的检查) */
// scripts/project-health.mjs
import { readFileSync, existsSync } from 'node:fs';
import { execSync } from 'node:child_process';
const results = [];
const add = (name: string, ok: boolean, detail: string) => results.push({ name, ok, detail });
// ============ ① 结构与边界 ============
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
add('有 lint 脚本', Boolean(pkg.scripts?.lint), pkg.scripts?.lint ?? '(缺失)');
add('有 typecheck 脚本', Boolean(pkg.scripts?.typecheck), pkg.scripts?.typecheck ?? '(缺失)');
add('有 test 脚本', Boolean(pkg.scripts?.test), pkg.scripts?.test ?? '(缺失)');
// ⭐ 依赖数量(⭐ 直接依赖)
const depCount = Object.keys(pkg.dependencies ?? {}).length;
add('直接依赖 ≤ 40', depCount <= 40, `当前 ${depCount} 个`);
// ⭐ 是否有 lock 文件(且「被提交」)
add('有 lock 文件', existsSync('pnpm-lock.yaml') || existsSync('package-lock.json') || existsSync('yarn.lock'), '');
// ⭐ tsconfig 严格度
const tsconfig = existsSync('tsconfig.json') ? JSON.parse(readFileSync('tsconfig.json', 'utf8')) : {};
const strict = tsconfig.compilerOptions?.strict === true;
add('TypeScript strict 开启', strict, `strict=${tsconfig.compilerOptions?.strict}`);
// ⭐ CI 配置
add('有 CI 配置', existsSync('.github/workflows') || existsSync('.gitlab-ci.yml'), '');
// ============ ② 数据流「坏味道」扫描(⭐ 粗扫,命中数作参考) ============
let smells = 0;
try {
// ⚠️ 真实实现要「遍历源码 + 正则」(这里示意)
const out = execSync(
`grep -rIl "eslint-disable-next-line react-hooks/exhaustive-deps" src 2>/dev/null | wc -l`,
{ encoding: 'utf8' }
);
smells = Number(out.trim());
} catch { /* 忽略 */ }
add('「依赖检查被禁用」的文件数 ≤ 3', smells <= 3, `当前 ${smells} 个文件`);
// ============ ③ 可观测性(⭐ 找「错误监控的初始化」) ============
let hasMonitor = false;
try {
execSync('grep -rIl "Sentry\\.init\\|initErrorMonitor\\|installGlobalHandlers" src 2>/dev/null', { stdio: 'ignore' });
hasMonitor = true;
} catch { hasMonitor = false; }
add('有错误监控初始化', hasMonitor, '');
// ============ 输出 ============
const passed = results.filter((r) => r.ok).length;
console.log(`\n📋 项目体检:${passed}/${results.length} 项通过\n`);
for (const r of results) {
console.log(`${r.ok ? '✅' : '⚠️ '} ${r.name}${r.detail ? ` —— ${r.detail}` : ''}`);
}
// ⭐ 「不通过的项」= 「可以放进「技术改进 backlog」的东西」
四、怎么把评估「说出去」(⭐ 面试 / 汇报都用得上)
⭐⭐⭐ 一个「有说服力」的评估结论的结构(⭐ 四段):
① 【说清楚「评估的目标」】
「我不是要「评判技术选型」,而是想知道「**改需求的成本**」与「**线上风险**」。」
② 【给「客观信号」而不是「主观印象」】⭐
❌ 「这个项目代码很乱」(⭐ 无法行动)
✅ 「**改动『退款』需要打开 9 个文件,且没有测试覆盖;初始 gzip 是 780KB
(预算是 250KB);错误监控没上传 source map**」——
⭐ 「每个判断都指向一个具体的东西」
③ 【分清「可修的」与「要重写的」】⭐
· 可修:加监控、加测试、拆分巨型组件、去掉重复依赖
· 要重写:「耦合到无法测的核心模块」——⚠️ **但也要先「补测试」再重写**
④ 【给「优先级」而不是「一堆问题」】⭐
⭐ 「**先补可观测性(监控 + 回滚)→ 再补关键路径的测试 → 再清理结构**」——
⭐ 因为「**没有兜底就重构 = 赌博**」。
⭐⭐⭐ 一个「反直觉但重要」的原则:
⭐⭐ 「**评估的目的是「决定下一步做什么」,不是「给项目打分」**」
· 打分是「结果」,**行动**才是「目的」
· ⭐ 且「**先建兜底(监控/测试/回滚),再改结构**」——
否则「你改坏的每一处都「没人知道」,直到用户投诉」
⭐ 这也是「接手项目」的标准顺序:
① 跑起来 + 看清「线上有什么」→ ② 补关键路径的测试与监控
→ ③ 小步改动 → ④ (有必要时)才动结构
五、面试延伸:怎么答这类问题
⭐⭐⭐ 「你怎么评估一个项目的质量?」(⭐ 四段答法)
① 【先立框架】
「我会从「**改需求的成本**」这个角度切入,看六个维度:
**结构边界、数据流、性能可观测、工程质量、依赖与债、线上可观测**。」
② 【给「可操作的信号」(⭐ 这是加分点)】
⭐ 「比如:
· 结构——『改一个功能要打开几个文件』『能不能删得干净』
· 数据流——『服务端状态与客户端状态分清了吗』『同一个数据是不是只有一个来源』
· 性能——『有没有体积预算与 RUM』(⭐ 而不是「快不快」这种主观判断)
· 工程——『CI 跑不跑 lint/typecheck/test』『测试测的是行为还是实现』
· 依赖——『直接依赖多少、有没有重复依赖、关键依赖还在维护吗』
· 可观测——『有没有错误监控 + source map + 回滚手段』」
③ 【说「优先级」(⭐ 展示判断力)】
⭐ 「我会**先看可观测性与测试**——因为「**没有兜底就重构是赌博**」;
这两项不行,其它「优化」都是「冒险」。」
④ 【给「下一步」(⭐ 落到行动)】
⭐ 「所以接手项目我的顺序是:
**跑起来 → 看清线上有什么 → 补关键路径测试与监控 → 小步改 → (必要时)动结构**。」
六、本篇特有的坑(评估本身容易犯的错)
① 「用「技术栈新不新」判断项目好坏」
⭐ 「用 antd + Redux 的老项目」可能「比「用新潮技术但没测试没监控」的项目更健康」
② 「看到「memo 少」就说「性能有问题」」
⚠️ 「memo 的多少与性能没有直接关系」(见 5.3);
⭐ 应该看「**Profiler 数据 / RUM**」
③ 「看到「没有测试」就建议「全面补测试」」
⚠️ 「全面补测试」通常「补不完」且「做成 KPI 后写成垃圾测试」
⭐ 应该「**先补关键路径 + 高危模块**」(⭐ 见上文)
④ ⭐ 「把「代码风格」当「健康度」」
⚠️ 「分号/引号/命名」是「lint 一次就能统一的」;
⭐ 「结构、边界、数据流」才是「真正难还的债」
⑤ 「只看「静态代码」不看「线上」」
⭐ 「线上监控/RUM/告警」的缺失「比「代码丑」严重得多」
⑥ ⭐ 「忽略「团队因素」」
⚠️ 「同一个项目」「不同团队维护」的健康度会变(⭐ 因为有「约定与记忆」)
⑦ 「把「技术债」等同于「要重写」」
⭐ 大部分债「可以直接还」(加测试/拆文件/去重复依赖)
⑧ 「评估完「只给问题清单」不给优先级」
⚠️ 「100 个问题」= 「没有行动」
⭐ 应该「**先兜底(监控/测试/回滚)→ 再结构**」
⑨ ⭐ 「忽略「可回滚」这件事本身」
⭐ 「能不能快速回滚」直接决定「敢不敢发布」——
⭐ 这是「健康度」里「最被低估的一项」
⑩ 「把「大厂做法」直接套到「小团队」」
⚠️ 「微前端、Monorepo、完备 CI」在「3 人团队」里可能「成本 > 收益」(见 10.9/10.10)
⭐ 应该「按「约束」选择」(⭐ 团队规模、发布频率、业务复杂度)
七、一句话速记
评估项目不看「用了什么技术」,而看「改需求的成本」——六个维度:① 结构边界(⭐ 判据:「改一个功能要打开几个文件」「能不能删得干净」);② 数据流(⭐ 最关键:服务端状态与客户端状态分清、同一个数据只有一个来源、没有「Effect 手动拉数据 + 手动 loading」);③ 性能可观测(⭐ 不是「快不快」而是「团队知不知道它快不快」——体积预算 + RUM + 门禁);④ 工程质量(
strict: true+ CI 跑 lint/typecheck/test/build + ⭐ **测试测的是「行为」而不是「实现」 + 没有 flaky 测试);⑤ 依赖与债(数量 / 体积 / 重复依赖 / ⭐ 「债的可见度」比「债的数量」更重要);⑥ 线上可观测(错误监控 + source map + RUM + 告警 + ⭐ 能秒级回滚/有特性开关);⭐ **一个 15 分钟速查顺序:package.json+ 目录 → 测试与 CI → 数据流入口 → 一个最复杂页面 → 监控初始化;⭐ 评分卡(24 分制,< 10 分「风险高」);⚠️ 评估的目的不是「打分」而是「决定下一步」——⭐ 且顺序永远是「先建兜底(监控/测试/回滚),再改结构」(没有兜底就重构 = 赌博);⭐ 接手项目四步:跑起来 → 看清线上有什么 → 补关键路径测试与监控 → 小步改 → (必要时)才动结构;⚠️ 四个常见误判:用「技术栈新不新」判断好坏、看到「memo 少」就说性能有问题、看到「没测试」就建议全面补测试、把「代码风格」当健康度。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!