陌上人如玉
公子世无双

12.15 如何评估一个 React 项目的健康度

12.15 如何评估一个 React 项目的健康度

一句话:评估一个项目不是「看它用了什么技术」,而是看「它有没有让『改需求』这件事变便宜」——所以六个维度分别是:结构边界、数据流、性能可观测、工程质量、依赖与债、线上可观测,而每个维度都能用「一个具体的信号」在几分钟内判断。

一、先说「为什么评估」:三种场景

⭐⭐⭐ 三种「需要评估」的场景(⭐ 目标不同,信号也不同):

① 【接手一个陌生项目】(⭐ 最实用)
   目标:「**我要多久能安全地改第一个需求**」
   ⭐ 关注:「能不能跑起来」「改一处会不会炸别处」「有没有测试兜底」「线上出问题看得见吗」

② 【面试时「反问」或「判断团队水平」】
   目标:「**这个团队是「工程化」还是「能跑就行」**」
   ⭐ 关注:「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 少」就说性能有问题、看到「没测试」就建议全面补测试、把「代码风格」当健康度。

赞(0) 打赏
未经允许不得转载:陌上寒 » 12.15 如何评估一个 React 项目的健康度

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫