陌上人如玉
公子世无双

12.17 生态选型问答

12.17 生态选型问答

一句话:选型不是「哪个技术更好」,而是「在给定约束下,哪个技术的『代价』你更愿意承担」——所以每个选型都该回答五个问题:约束是什么、候选有哪些、判据是什么、代价是什么、退出策略是什么。

一、选型的通用框架(⭐ 五问)

⭐⭐⭐ 任何选型都过这五问(⭐ 「退出策略」是最容易被忽略的一问):

   ① 【约束是什么】⭐
      · 团队规模与经验(⭐ 「3 个新人」与「10 个老手」完全不同)
      · 项目周期(⭐ 「3 个月的营销页」与「要维护 5 年的后台」不同)
      · 性能/可访问性/合规的硬要求
      · 「已有技术栈」与「迁移成本」
      ⭐ 不问约束就选型 = 「拿「别人的答案」解「自己的题」

   ② 【候选有哪些】⭐
      ⭐ 「至少列出 2 个真实候选」(⭐ 只想到一个说明「见得不够」)

   ③ 【判据是什么】⭐
      ⭐ 把「感觉」变成「可验证的判据」:
        · 「体积 < 20KB?」「类型安全?」「SSR 支持?」「团队熟悉度?」
        · ⭐ 「社区活跃度」(⚠️ 「一年没发版」= 未来的坑)
      ⚠️ 判据要「能排序」而不是「都很重要」

   ④ 【代价是什么】⭐ 最容易被跳过的一问
      ⭐ 每个技术都有代价:「学习成本」「运行时开销」「锁定」「生态迁移成本」
      ⭐ 「没有代价的选项」通常意味着「你还没看出它的代价」

   ⑤ 【退出策略是什么】⭐
      ⭐ 「如果用了一年发现不对,怎么换?」
      · ✅ 「有边界」的(如「数据层封装在一个 `api/` 目录」)→ 好换
      · ❌ 「渗透到每个组件」的(如「CSS-in-JS 的写法散落各处」)→ 难换
      ⭐ 判据:「**这个技术会被「隔离在一个层里」还是「渗透到业务代码里」**」

二、八个高频选型

选型 1:状态管理

候选 什么时候选 什么时候不选
useState + 提升 + Context ⭐ 「状态少、共享范围小」(多数场景) 「跨多个不相邻组件」「高频更新」(⭐ Context 会全量重渲染,见 5.9)
⭐ Zustand ⭐ 「需要全局状态 + 想要简单」(⭐ 默认推荐);「需要在 React 外读写」 「需要严格的时间旅行/事件溯源」(⭐ Redux 更合适)
⭐ Jotai ⭐ 「派生值多、依赖关系复杂」(表格合计、多条件联动,见 3.11) 「团队不熟悉原子化模型」(⭐ 学习成本真实存在)
Redux Toolkit ⭐ 「大团队 + 需要严格可追溯/中间件生态/时间旅行」 「中小项目」(⭐ 样板代码与心智成本偏高)
MobX / Valtio ⭐ 「深层嵌套 + 大量就地修改」(表单、树、编辑器,见 3.12) 「团队不接受「代理式响应式」的调试方式」
XState ⭐ 「状态多 + 有非法组合风险 + 有异步与超时」(支付、多步向导,见 3.13) 「状态少、转移简单」(⭐ 杀鸡用牛刀)
⭐⭐⭐ 「状态管理选型」的四个判据(⭐ 按顺序问自己):

   ① ⭐ 「这个状态需要「跨组件共享」吗」—— 不需要 → `useState`(⭐ 别上库)
   ② ⭐ 「它是「服务端状态」还是「客户端状态」」—— 服务端 → **数据层**(见选型 2),
      不是「状态管理库」
   ③ ⭐ 「共享范围多大」—— 「一个子树」→ Context/局部 store;「全局」→ 全局 store
   ④ ⭐ 「更新频率多高」—— 高频 → ⭐ **必须「细粒度订阅」**(Zustand 选择器 / Jotai 原子)
      ⚠️ 「Context 在高频场景会全量重渲染」

   ⭐ 一句话:「**先问「要不要」,再问「用哪个」**」——
     ⚠️ 「为了一个弹窗开关引入状态库」是常见过度设计。

选型 2:数据层(服务端状态)

候选 什么时候选 什么时候不选
⭐ TanStack Query(React Query) ⭐ 「REST/GraphQL 都要」「需要缓存/失效/乐观更新/分页/轮询」(⭐ 默认推荐,见 7.4) 「只是「进页面拉一次」」(⭐ 可能不需要)
SWR ⭐ 「简单场景 + 体积敏感」(⭐ 更小、API 更少) 「需要「无限查询/复杂缓存操作」(⭐ 功能少一些)
Apollo / urql ⭐ 「用 GraphQL + 需要「规范化缓存」」(见 7.10) 「不是 GraphQL」或「不需要「跨查询自动更新」」
⭐ tRPC ⭐ 「全栈 TS 同仓」(⭐ 端到端类型安全,见 7.11) 「需要给「外部/多语言客户端」提供 API」
自己封装 fetch 层 ⭐ 「需求很简单」或「需要「极致的定制」」 ⚠️ 「你很快就会「重新实现一遍 React Query 的坑」(⭐ 缓存/竞态/失效)
框架内建(Next.js loader/RSC) ⭐ 「用 Next.js 且「数据与路由强相关」」(见 6.11/9.9) 「需要「客户端侧复杂的缓存与失效」」(⭐ 那时还是配 React Query)
⭐⭐⭐ 「数据层」最容易搞错的三件事:

   ① ⭐ **「服务端状态」不是「客户端状态」**(见 3.10)——
      ⚠️ 「把接口数据塞进 Redux 并自己管 loading/error/缓存」
        = 「重新发明一个「更差的 React Query」
   ② ⭐ **数据层「与路由层的分工」**(见 6.11/9.9)——
      Next.js 的 loader/RSC 可以做「路由级数据」,但「客户端复杂缓存」还是要数据层
   ③ ⭐ **「缓存策略」要有「失效设计」**——
      ⚠️ 「配了缓存但没想「什么时候失效」」→ 「用户看到旧数据」(见 9.9)

选型 3:样式方案

候选 什么时候选 什么时候不选
CSS Modules ⭐ 「要「零运行时 + 传统 CSS 能力」」 「需要「极致的动态样式」(主题切换靠 CSS 变量仍然可以)
⭐ Tailwind ⭐ 「团队接受「原子类」+ 想要「快速迭代 + 一致的设计尺度」」(⭐ 配合 token) 「设计稿与类名体系差异大」或「团队讨厌「HTML 里一长串类名」」
CSS-in-JS(运行时,如 emotion) 「需要「基于 props 的动态样式」」 ⭐ 「性能敏感」(⭐ 运行时开销,见 10.8);「组件库」(⭐ 优先构建期方案)
vanilla-extract / Linaria ⭐ 「想要「类型安全的 CSS」+ 零运行时」 「团队不想引入「构建期方案」的复杂度」
Sass/Less ⭐ 「已有资产 / 需要「复杂嵌套与函数」」 「新项目」(⭐ 现代 CSS 变量 + @layer 已覆盖大部分需求)
⭐⭐⭐ 「样式选型」的三个判断点:

   ① ⭐ **「运行时的成本」**——「运行时 CSS-in-JS」每渲染一次都要「生成/插入样式」
      (⭐ 见 10.8 的对比表)→ ⭐ 「组件库/性能敏感」优先「构建期方案」
   ② ⭐ **「主题切换」怎么做**——⭐ **CSS 变量是最通用的解**(见 10.8):
      「构建期方案 + CSS 变量」= 「零运行时 + 可换肤」
   ③ ⭐ **「设计一致性」**——「Tailwind 的「尺度约束」本身就是「设计系统的载体」」
      (⚠️ 「自由写 CSS」更容易「像素漂移」,见 10.13)

选型 4:表单

候选 什么时候选 什么时候不选
⭐ react-hook-form ⭐ 「字段多 / 需要性能」(⭐ 非受控 + 字段级订阅,见 1.13) 「只有 1~2 个字段」(⭐ useState 就够)
TanStack Form ⭐ 「想要「类型推导最强」+ 与 TanStack 生态一致」 「团队不熟悉 / 生态较新」
Formik ⚠️ 「老项目在维护」(⭐ 新项目一般不再选:受控 + 全量重渲染) ⭐ 「新项目」(见上)
自己写 ⭐ 「需求极简」或「要「完全控制」」 ⚠️ 「字段一多就会「重新发明一遍表单库」(⭐ 竞态/校验时机/数组字段)
⭐ 判据:⭐ **「字段数」与「是否需要「复杂校验」」**
   · 「≤ 5 个字段 + 简单校验」→ ⭐ 自己写(`useState` + 手写校验)
   · ⭐ 「字段多 / 跨字段校验 / 数组字段 / 异步校验」→ **react-hook-form**
   ⚠️ 且要记住它的核心纪律:「**不要展开整个 `formState`**」(见 1.13)

选型 5:路由

候选 什么时候选 什么时候不选
⭐ React Router ⭐ 「生态最主流 + 团队熟悉」(⭐ 默认推荐) 「重度依赖 URL 状态的搜索页/后台」(⭐ 见下)
⭐ TanStack Router ⭐ 「URL 状态复杂(筛选/分页/排序都在 URL)+ 想要类型安全」(见 6.11) 「路由简单」或「团队不想学新东西」
Next.js App Router(文件路由) ⭐ 「用 Next.js」(⭐ 路由即文件结构 + 与 RSC 配合) 「纯 SPA 且不需要 SSR」
自己写(history 封装) ⭐ 「学习/极简场景」 ⚠️ 「生产项目」(⭐ 你会「重新实现一遍「嵌套路由/懒加载/滚动恢复」」)
⭐ 判据:⭐ **「URL 是不是「一等公民的状态容器」」**
   · 「是」(电商筛选、数据看板、后台报表)→ ⭐ **TanStack Router 的收益最大**(见 6.11)
   · 「否」(页面少、参数简单)→ ⭐ React Router 更省心(⭐ 生态熟悉度也是成本)

选型 6:构建工具

候选 什么时候选 什么时候不选
⭐ Vite ⭐ 「SPA/库开发」——开发体验最好(⭐ 见 10.14 的两段式机制) 「需要「SSR/RSC/文件路由」的一体化」(⭐ 那是框架的活)
⭐ Next.js(next build) ⭐ 「需要 SSR/SSG/RSC/API」的全栈应用 「纯静态 SPA」(⭐ 可能过度)
Rspack ⭐ 「已有 webpack 资产 + 想要大幅提速」(⭐ API 兼容,见 10.14) 「新项目」(⭐ Vite 通常更省心)
webpack ⚠️ 「老项目在维护 / 深度定制的构建」 「新项目」(⭐ 慢、配置多)
Turbopack ⭐ 「Next.js 项目想更快」(⭐ 逐步成熟) ⚠️ 「求稳的生产环境」(⭐ 视版本成熟度)
⭐ 判据:⭐ **「你要不要「框架能力」(SSR/RSC/文件路由)」**
   · 「要」→ ⭐ **用框架**(Next.js 等)——构建工具是它的内部实现
   · 「不要」(纯 SPA)→ ⭐ **Vite**
   · 「已有 webpack 且很痛」→ ⭐ **Rspack 是「迁移成本最低的提速」**

选型 7:测试

候选 什么时候选 什么时候不选
⭐ Vitest ⭐ 「新项目 / Vite 项目」(⭐ 快、配置少、API 兼容 Jest) 「已有大量 Jest 配置与生态依赖」
Jest ⭐ 「老项目 / 依赖 Jest 生态(如某些 React Native 配置)」 ⭐ 「新项目」(⭐ Vitest 更快更省心)
⭐ Testing Library ⭐ 「组件测试」的标准(⭐ 且「用 role 查询顺便校验无障碍」,见 10.12) —
⭐ Playwright ⭐ 「E2E + 跨浏览器 + 分片」(⭐ 默认推荐,见 10.14) 「只测「单页小组件」」(⭐ 单测更合适)
Cypress ⭐ 「团队已熟悉 / 喜欢它的 DX」 ⚠️ 「跨浏览器 + 并行分片的成本」(⭐ Playwright 更省心)
Storybook + 视觉回归 ⭐ 「组件库/设计系统」必备(⭐ 见 10.8) 「简单业务项目」(⭐ 成本可能不划算)
⭐⭐⭐ 「测试选型」的核心是「**分层**」(⭐ 不是「选一个」):
   ① ⭐ **单测(Vitest)**:纯逻辑、工具、reducer、service(⭐ 最多、最快)
   ② ⭐ **组件测试(Testing Library)**:组件行为(⭐ 用 `role` 查询)
   ③ ⭐ **E2E(Playwright)**:核心用户流程(⭐ 少而精,覆盖「下单/支付/登录」)
   ⭐ 比例建议:「**大量单测 + 适量组件测试 + 少量 E2E**」(⭐ 反过来的「冰淇淋筒」是反模式)

选型 8:组件库 / 设计系统

候选 什么时候选 什么时候不选
Ant Design ⭐ 「中后台」(⭐ 组件全、表单/表格强) 「C 端「品牌感强」的页面」(⭐ 定制成本高)
MUI ⭐ 「Material 风格 + 国际化团队」 「设计语言与 Material 差异大」
shadcn/ui ⭐ 「想要「代码在你仓库里」+ 完全可控」(⭐ 复制式而非依赖式) 「团队不想「自己维护组件」」
Radix / Ark UI(Headless) ⭐ 「要自建设计系统」(⭐ 无样式 + 无障碍 + 可组合,见 1.11) 「需要「开箱即用的样式」」
⭐ 自研设计系统 ⭐ 「有设计团队 + 长期维护 + 品牌要求」 ⚠️ 「没有「设计系统维护者」」(⭐ 会烂掉,见 10.8)
⭐⭐⭐ 判据(⭐ 四问):
   ① ⭐ 「C 端还是中后台」—— ⭐ 中后台「用成熟库」(省时间);C 端「自研或 Headless」
   ② ⭐ 「要不要「自己拥有代码」」—— ⭐ shadcn/ui 的思路(「复制进仓库」)适合「想改造」
   ③ ⭐ 「有没有「设计系统的人」」—— ⚠️ 「没有「专职设计系统」的自研 = 「迟早烂掉」
   ④ ⭐ 「无障碍要求」—— ⭐ Radix/Ark 这类 Headless 库「无障碍做得好」(见 10.12)

三、选型的反模式

⭐⭐⭐ 八个「选型反模式」(⭐ 面试里能说出这些,比「背对比表」加分):

   ① ⭐ **「简历驱动选型」**——「用它是因为「想学/想写在简历上」
      ✅ 「用它的理由应该是「约束」」
   ② ⭐ **「最流行驱动」**——「star 最多就选它」
      ⚠️ 「流行度是「判据之一」但「不等于适合你的约束」」
   ③ ⭐ **「不为「退出」设计」**——把某个库的 API 渗透到业务代码里
      ✅ 「隔离在一层」(⭐ 见五问的「退出策略」)
   ④ ⭐ **「一次引入太多」**——「状态库 + 数据层 + 表单库 + 样式库 + UI 库」同时上
      ✅ 「一次引入一个,且「每引入一个都要「说明它解决了什么痛点」」
   ⑤ ⭐ **「重复职责」**——「Redux 管缓存 + React Query 也管缓存」
      ⚠️ 「两层缓存」→ 「不知道数据从哪来」(⭐ 见 7.10 的「Apollo 与 React Query 别叠」)
   ⑥ ⭐ **「为了「统一」而统一」**——「全公司必须用同一个 UI 库」
      ⚠️ 「不同产品的「约束不同」(⭐ C 端与中后台的诉求相反)
   ⑦ ⭐ **「忽略「团队熟悉度」」**——选一个「只有一个人懂」的技术
      ⭐ 「**降低认知成本往往比「技术更优」更重要**」(⭐ 招聘/交接/搜索答案)
   ⑧ ⭐ **「不写「决策记录」」**——一年后「没人知道「为什么选它」」
      ✅ ⭐ 「写 ADR(Architecture Decision Record):**背景、候选、决策、代价、何时重评**」
<!-- ⭐⭐ 一个「决策记录」模板(⭐ 团队工程化的标志) -->
# ADR-012:数据层选用 TanStack Query

## 背景
- 我们有 40+ 个接口,全部是 REST;当前「每个页面自己 useEffect + useState」。
- 痛点:缓存重复实现、竞态、失效靠手动、loading/error 状态散落。

## 候选
1. TanStack Query(缓存/失效/乐观更新/分页齐全)
2. SWR(更小,但「无限查询/复杂失效」能力弱)
3. 自己封装 fetch 层(可控但「要自己实现竞态/缓存/失效」)

## 决策
选 **TanStack Query**。
理由:① 我们「需要分页与乐观更新」(SWR 能力不足);② 社区活跃 + 团队有人用过。

## 代价
- ⭐ 新增约 12KB(gzip);⭐ 团队要理解「服务端状态与客户端状态的区别」。
- ⚠️ 要注意「不要与「Next.js 的缓存」叠加两层」(见 9.9)。

## 何时重评
- 如果「tRPC 成为全栈标准」→ 我们会「基于 tRPC 的 React Query 集成」(见 7.11)。
- 如果「接口数量降到 10 个以内」→ 考虑「去掉它」。

四、面试延伸:怎么答「选型题」

⭐⭐⭐ 「你怎么做技术选型?」(⭐ 五步答法,与开头呼应)

   ① ⭐ 【先问约束】
      「我会先确认「团队规模与经验、项目周期、性能要求、已有栈、迁移成本」——
        因为「约束不同,答案完全不同」。」

   ② ⭐ 【列候选 + 判据】
      「然后列 2~3 个真实候选,并给「可排序的判据」:
        体积、类型安全、SSR 支持、社区活跃度、团队熟悉度。」

   ③ ⭐ 【主动讲代价】⭐
      「每个选项我都会说「它的代价」——
        比如「选 Tailwind 换来「迭代速度与设计尺度一致」,代价是「HTML 里类名很长 + 团队要接受原子类」」。」

   ④ ⭐ 【讲退出策略】⭐ 加分项
      「我会问「这个东西会「被隔离在一层」还是「渗透到业务代码」」——
        能被隔离的(数据层、表单层)好换;渗透的(样式、组件库)要更慎重。」

   ⑤ ⭐ 【给「重评条件」】
      「最后我会写下「什么情况下我们会重新评估」——
        比如「如果接口数量下降、或者生态标准变了」。」
⭐ 三个「加分的话术」:

   ① ⭐ 「**降低认知成本往往比「技术更优」更重要**」(⭐ 团队熟悉度是真实成本)
   ② ⭐ 「**我会先问「现在有什么痛点」,再决定要不要引入**」
      (⭐ 而不是「因为这个库很流行」)
   ③ ⭐ 「**我会写一个 ADR,把「代价」和「重评条件」记下来**」
      (⭐ 展示「工程化思维」)

五、一句话速记

选型 = 「在给定约束下,选一个「代价你更愿意承担」的方案」——⭐ 五问框架:约束是什么(团队/周期/硬要求/已有栈/迁移成本)→ 候选有哪些(至少 2 个真实候选)→ 判据是什么(可排序:体积/类型/SSR/活跃度/熟悉度)→ ⭐ **代价是什么(最容易被跳过:学习成本/运行时开销/锁定/生态迁移)→ ⭐ **退出策略(「它会被隔离在一层还是渗透到业务」);八个高频选型的核心判据:① 状态管理(先问「要不要」再问「用哪个」:跨组件?服务端还是客户端状态?共享范围?更新频率?——高频必须细粒度订阅)、② 数据层(服务端状态≠客户端状态,别用 Redux 管接口数据;简单可以用框架 loader/RSC,复杂缓存才上 React Query)、③ 样式(⭐ **运行时 CSS-in-JS 有每渲染成本 → 组件库/性能敏感优先「构建期 + CSS 变量」)、④ 表单(≤5 字段自己写,多字段/跨字段校验/异步校验 → react-hook-form)、⑤ 路由(⭐ 判据是「URL 是不是一等公民的状态容器」→ 是就用 TanStack Router,否就用 React Router)、⑥ 构建(要框架能力用 Next.js,纯 SPA 用 Vite,webpack 资产迁移用 Rspack)、⑦ 测试(⭐ 分层:大量单测 + 适量组件测试 + 少量 E2E;E2E 优先 Playwright)、⑧ 组件库(⭐ C 端自研/Headless、中后台用成熟库;没有专职设计系统就别自研);⭐ **八个反模式:简历驱动 / 最流行驱动 / 不为退出设计 / 一次引入太多 / 重复职责(两层缓存)/ 为统一而统一 / 忽略团队熟悉度 / ⭐ 不写决策记录(ADR:背景·候选·决策·代价·何时重评);⭐ 面试加分话术:「降低认知成本往往比技术更优更重要」「先问现在有什么痛点」「写 ADR 记下代价与重评条件」。

赞(0) 打赏
未经允许不得转载:陌上寒 » 12.17 生态选型问答

评论 抢沙发

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

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

微信扫一扫

支付宝扫一扫