Web 前端怎么选型和组织:框架与构建、何时用 SSR/SSG、路由、数据请求与缓存、状态、表单、组件库、样式、图表与编辑器等重型功能、前端监控、包体积与性能,以及私有化部署下的资源本地化。
每个前端项目在 ADR 里写明项目类型(SPA、后台、内容站、SSR 站点、嵌入式组件、组件库)以及选用和放弃的框架
怎么检查:CI 检查仓库里有前端选型 ADR;评审时看新项目是否说明了放弃的选项
为什么、来源
为什么:惯性选型常在后期才暴露问题,比如为不需要 SEO 的后台引入 SSR,多出服务端运维和缓存治理。
登录后才能用的应用默认构建为静态 SPA;公开可索引的内容默认静态生成;只有需要个性化服务端渲染时才用 SSR 框架
怎么检查:评审选型 ADR 时核对:选 SSR 的理由是否具体(SEO 的个性化页面、首屏数据必须服务端取),是否评估了部署、缓存一致性和安全更新
为什么、来源
为什么:SSR 带来 Node 服务、多实例缓存一致性和服务端组件安全更新等运维负担;登录后应用通常不需要 SEO。
- 静态 SPA 部署时,静态服务器要配置 history 回退(未知路径返回 index.html)。
- 官网和应用同域不等于必须同一框架:
www 用 Astro、app 用 Vite SPA,共享设计令牌和身份服务即可。
- 公开页面多且需要个性化时,用真实样板工程对比 Astro、Next.js、React Router 框架模式和 TanStack Start,而不是只比构建速度。
一个应用只用一套路由库,不混用两套路由
怎么检查:CI 检查依赖清单里同时出现多个路由库(如 react-router 和 @tanstack/react-router)时报错
为什么、来源
为什么:两套路由的导航、数据加载和 URL 语义不一致,链接和返回行为会出错。
筛选、排序、分页、当前标签页等能从 URL 重建的视图状态放进路由的查询参数
怎么检查:评审时检查:刷新页面或分享链接后视图是否还原
为什么、来源
为什么:放在内存里的视图状态刷新就丢,也无法分享和回退。
服务器数据交给请求缓存库或框架的 loader 管理,不复制进全局 store
怎么检查:评审时检查全局 store 里是否存放了接口返回的实体列表;确需复制的写明理由
为什么、来源
为什么:复制出来的数据会过期,失效、重试、乐观更新都要自己再实现一遍。
状态归属:
- URL 可重建的视图 → 路由查询参数
- 服务器实体(列表、详情、权限快照)→ 请求缓存库或 loader
- 纯本地临时状态(弹层开关、表单中间值)→ 组件内 state
- 跨视图共享的轻量客户端状态(布局偏好、命令面板开关)→ context 或轻量 store
- 会话与权限 → 服务端为准,前端只持有展示用的副本
所有接口调用经过统一的 HTTP 客户端层,鉴权、错误转换、超时和重试只在这一层实现
怎么检查:用 lint 的 import 限制规则禁止在客户端层以外直接调用 fetch 或引入 HTTP 库
为什么、来源
为什么:每个页面各写一套 401 处理和重试,会导致认证行为分叉和重试风暴。
后端提供 OpenAPI 契约时,前端的接口类型从契约生成,生成文件不手改
怎么检查:CI 重新运行生成命令后执行 git diff --exit-code,有差异即失败
为什么、来源
为什么:手写的接口类型会和后端悄悄漂移,错误要到运行时才暴露。
新增依赖前登记用途、备选、许可证、维护状态、包体积影响和需要的外部资源
怎么检查:PR 模板要求填写;CI 发现依赖清单新增直接依赖时检查 PR 描述是否有登记
为什么、来源
为什么:前端依赖增长快,事后很难说清每个包为什么在、能不能删。
商业许可、强 copyleft 或已归档的包未经许可审查不得引入;同一项目的免费版和付费版功能要分清
怎么检查:CI 对锁文件跑许可证扫描,命中禁用或待审许可证即失败;评审时核对精确包名
为什么、来源
为什么:名字相近的包可能许可和维护状态完全不同;付费功能误当免费用有法律风险。
常见坑:
react-pdf(wojtekmaj,MIT)和 react-pdf-viewer(已归档、商业许可)是两个项目。
- AG Grid 社区版是 MIT,企业版功能需要商业许可;Tiptap 核心是 MIT,Pro 扩展和云服务另有条款。
- tldraw、在线 Office 预览服务的许可要逐项审查。
依赖的主版本升级单独提 PR,写明影响范围、迁移步骤和回滚方式,并跑完整 E2E;不批量升级多个主版本
怎么检查:CI 发现依赖主版本变化时,要求 PR 描述包含迁移和回滚小节
为什么、来源
为什么:前端库的主版本常有不兼容的 API 变化(如 TanStack Table 新主版本与旧版不兼容),混在一起升级出问题时无法定位。
对话框、菜单、弹出层、下拉选择、组合框等交互组件,基于成熟的可访问基础组件库实现,不自己写焦点管理和键盘交互
怎么检查:评审时检查新组件里是否手写了焦点陷阱、role="dialog"、方向键导航等逻辑;有则要求改用基础组件库
为什么、来源
为什么:焦点、键盘、遮罩滚动、层级这些行为细节多,自己写几乎一定漏掉读屏和键盘用户。
同一应用的公共组件层只用一套基础组件库,不混用多套
怎么检查:CI 检查依赖清单里同时出现多套基础组件库(如 Base UI、Radix、React Aria、Ark UI)时报错,例外要有 ADR
为什么、来源
为什么:不同库的事件模型、样式接口和可访问实现不一致,组合起来行为难以预测。
以源码方式复制进仓库的组件(如 shadcn/ui)登记上游来源、引入时的版本和本地修改;业务页面复用公共组件,不另复制变体
怎么检查:维护一份组件清单文件;CI 检查公共组件目录里的文件都在清单中
为什么、来源
为什么:复制源码换来了定制自由,代价是上游修复不会自动到达;不登记就没法升级。
虚拟列表、复杂表格、拖拽排序、日期时区计算、富文本、协同编辑、代码编辑器、PDF 渲染、终端模拟用成熟库,不自己实现
怎么检查:评审时识别自研的此类实现;确需自研的,先写一份「自研还是采用」的决策记录,列出候选库、缺口、测试和负责人
为什么、来源
为什么:这些领域边界情况极多(动态行高、输入法、时区和夏令时、选区和撤销),自研的成本和缺陷远超预期。
可以自己写的:具体业务流程、页面布局、简单展示组件、接口适配和数据转换、与业务语义绑定的交互。
一个应用只用一套主样式方案;全局样式只在应用入口定义
怎么检查:评审时检查是否同时引入多套原子类框架或 CSS-in-JS 库;lint 限制全局样式只能在入口文件引入
为什么、来源
为什么:多套样式方案叠加会造成优先级冲突和体积膨胀。
界面文案不硬编码在组件里,统一走 i18n 库;语言包随应用打包,不在运行时请求公网翻译服务
怎么检查:lint 规则查找 JSX 里的裸文本字面量;构建产物 URL 扫描
为什么、来源
为什么:硬编码文案后续无法翻译;在线翻译服务在内网和断网环境不可用。
每个数据请求处理加载中、空结果、错误三种状态,并在离开页面时取消未完成的请求;每个路由有错误边界
怎么检查:组件测试用接口模拟返回空和错误;评审时检查请求是否接入取消信号
为什么、来源
为什么:只处理成功路径的页面,在网络慢或接口出错时会白屏或显示过期数据。
关键用户流程的 E2E 覆盖六种状态:成功、空结果、接口错误、超时、无权限、断网
怎么检查:用 E2E 框架的网络拦截模拟各状态;CI 检查关键流程的测试用例齐全
参考值:六种状态是本规范统一的口径,取代原调研中「正常/超时/无权限」等多种说法。
为什么、来源
为什么:只测成功路径,最常见的故障场景就没人验证过。
SSE 和 WebSocket 连接集中在通信层管理:断线指数退避加随机抖动重连、认证失效时重新认证、按事件编号去重,服务端支持时用游标续传
怎么检查:测试里模拟断线、重复和乱序事件;评审时检查组件内是否直接创建连接
为什么、来源
为什么:组件里各自开连接容易重复订阅、内存泄漏;不退避的重连会在服务恢复时形成风暴。
日期时间用日期库或 Intl 处理;接口传输带时区偏移的 ISO 8601 字符串,不用 new Date('YYYY-MM-DD') 做业务计算
怎么检查:lint 规则查找对日期字符串直接 new Date() 的用法;测试在固定时区和夏令时切换日运行
为什么、来源
为什么:不带时区的日期字符串在不同浏览器和时区下解析结果不同,常导致差一天的错误。
不可信的 HTML 和 Markdown 不得未经净化注入 DOM;Markdown 渲染默认禁用原生 HTML,并校验链接协议
怎么检查:lint 禁用 dangerouslySetInnerHTML、innerHTML、v-html,例外只允许经过净化函数的调用点;安全测试放入 XSS 载荷
为什么、来源
为什么:用户内容、评论、工具输出里的 HTML 是 XSS 的主要入口。净化器本身也会有漏洞,所以先避免渲染 HTML,必须渲染时再净化。
- 链接只允许 http、https、mailto 等白名单协议,拒绝
javascript:。
- 需要渲染 HTML 时用 DOMPurify 这类维护活跃的净化库,配置严格白名单。
- 图示代码(如 Mermaid)来自用户时,开启其安全模式。
访问令牌和刷新令牌不得写入 localStorage 或 sessionStorage
怎么检查:代码扫描查找对 localStorage/sessionStorage 写入 token 类键名;评审登录实现
为什么、来源
为什么:页面一旦有 XSS,脚本可读的存储里的令牌会被直接窃取。
推荐服务端会话加 HttpOnly、Secure、SameSite Cookie,或由 BFF(为前端服务的后端)持有令牌。用 Cookie 鉴权时写操作要有 CSRF 防护,CORS 不能代替 CSRF 防护,方案见 19。确需在前端持有令牌时,只放内存并缩短有效期。
前端构建产物不依赖内联脚本和 eval,能在不含 unsafe-inline、unsafe-eval 的 CSP 下正常运行
怎么检查:E2E 在启用严格 CSP 的环境运行,检查控制台和 CSP 报告里没有违规
为什么、来源
为什么:产物离不开 unsafe-inline 或 unsafe-eval,CSP 就形同虚设。静态站点可以用哈希,SSR 可以用 nonce。
权限以服务端校验为准;前端隐藏按钮和菜单只是体验,不作为访问控制
怎么检查:每个受保护接口有服务端权限负例测试;评审时检查是否有只靠前端判断的权限
为什么、来源
为什么:前端代码和请求都能被用户随意修改。
上传文件的类型、大小、配额和权限以服务端校验为准,前端的 accept 和大小提示只改善体验
怎么检查:服务端有绕过前端直接上传的负例测试
为什么、来源
为什么:前端校验可以被绕过。
使用服务端组件(RSC)或 SSR 框架的项目,订阅框架安全公告,严重漏洞按安全修复时限升级
怎么检查:依赖漏洞扫描覆盖框架包,告警进入安全修复流程
为什么、来源
为什么:服务端渲染让前端框架的漏洞变成服务端漏洞;React 官方曾公告服务端组件的严重漏洞。
页面加载的外部脚本、字体、CDN、iframe、统计和地图服务列成清单,写明域名、用途、传出的数据和离线替代
怎么检查:CI 扫描构建产物里的外部 URL,与清单比对,不在清单里的报错
为什么、来源
为什么:外部资源会带出用户数据,也会在内网和断网环境里失效。
私有化或断网部署的前端,字体、图标、样式、Worker(PDF.js、Monaco 等)全部随应用提供,不请求任何未批准的外部地址,并在断网环境验收
怎么检查:E2E 在屏蔽外网的环境运行核心流程,拦截并记录所有外部请求,有则失败
参考值:参考值:未批准的外部请求为 0 个。
为什么、来源
为什么:联网环境下能用不代表断网能用;字体、Worker 和图标偷连外网,是断网部署最常见的故障。
前端错误和性能上报先去敏:不上报用户输入、正文、令牌和完整 URL 查询参数;含客户数据的上报不得发往未批准的公网服务
怎么检查:上报 SDK 配置统一的过滤钩子并有单元测试;评审出口清单
为什么、来源
为什么:错误上下文、录屏和面包屑里常带着用户数据,发到第三方就是数据外泄。
- 生产 sourcemap 只上传给监控服务,不随静态资源公开部署。
- 录屏类功能默认关闭,开启需隐私评审。
每次构建带唯一的发布编号,错误上报和性能数据都附带它
怎么检查:CI 检查构建产物里注入了发布编号;监控里能按发布编号筛选
为什么、来源
为什么:没有发布编号,线上错误无法对应到具体版本和 sourcemap。前端错误边界也不能代替服务端观测。
为首屏和主要路由设包体积预算并在 CI 检查,超出要在 PR 里说明
怎么检查:用 size-limit 或构建分析工具在 CI 比对预算
参考值:起步参考(自拟草案,无公认出处,按目标设备和网络校准):登录后 SPA 首屏 JS 约 170 KB(gzip);内容页首屏 JS 约 80 KB(gzip),纯内容页力争 0 JS;按需加载的单个 chunk 超过约 250 KB(gzip)时说明。
为什么、来源
为什么:不设预算,「顺手引入一个大库」会逐步拖慢首屏。
代码编辑器、图表、PDF 预览、富文本、白板、Office 预览按路由或交互懒加载,不进首屏包
怎么检查:构建分析检查首屏 chunk 里不含这些库
为什么、来源
为什么:这些库体积大,只有少数页面用到,却让所有用户付出首屏成本。
在生产环境用真实用户数据按 P75 衡量 LCP、INP、CLS,以官方「良好」阈值为目标;实验室跑分不代替真实用户数据
怎么检查:用 web-vitals 采集上报到自有收集端;CI 另跑 Lighthouse 防退步
参考值:官方「良好」阈值:LCP ≤ 2.5 秒、INP ≤ 200 毫秒、CLS ≤ 0.1,按 75 百分位计;移动和桌面分开看。
为什么、来源
为什么:开发机上的满分不能说明中低端设备和慢网络上的体验。
首屏最大内容的图片不懒加载并写明宽高;字体自托管并设置 font-display
怎么检查:Lighthouse CI 检查 LCP 图片懒加载和布局偏移;构建扫描字体来源
为什么、来源
为什么:懒加载 LCP 图片会推迟最大内容绘制;不写尺寸会引起布局偏移。
嵌入第三方页面的组件做样式和全局变量隔离;宿主不可信或需强隔离时用 iframe
怎么检查:在一个带冲突样式的测试宿主页面里加载组件,检查双方样式互不影响
为什么、来源
为什么:嵌入组件污染宿主的全局 CSS 和 window,会破坏客户页面。
可复用组件库把 react、react-dom 声明为 peerDependencies,构建时不打进产物,并提供 exports 和类型声明
怎么检查:CI 检查 package.json 的 peerDependencies,并扫描产物中不含 React 源码
为什么、来源
为什么:组件库自带一份 React 会让使用方出现两份 React 实例,hooks 失效。
没有符合条件的规则。
登录后应用的框架与构建
React + TypeScript + Vite,构建为静态 SPA · React、Vite 为 MIT
备选:Vue + Vite(团队熟悉 Vue 或客户已有 Vue 资产;路由用 Vue Router,状态用 Pinia);Svelte + SvelteKit(团队熟悉且有明确收益时试点)
不推荐:不分场景一律用 SSR 框架
需要服务端渲染时的框架
已有 React 团队且需要渐进 SSR:React Router 框架模式;已深度用 TanStack:TanStack Start · React Router、TanStack Start、Next.js 为 MIT
备选:Next.js(内容与 React SSR 深度融合、团队已熟悉;自托管多实例时按官方要求处理缓存一致性);Nuxt(Vue 团队需要 SSR)
不推荐:没做部署与缓存评估就引入服务端组件;一个项目里混用多套 SSR 框架
官网、博客、内容站
Astro(静态优先,交互部分用 islands 按需加载) · Astro 为 MIT
备选:Starlight(基于 Astro)或 VitePress(技术文档站)
不推荐:把纯内容页做成客户端 SPA
路由
TanStack Router(类型安全) · MIT
备选:React Router(已有 React Router 资产或使用其框架模式)
不推荐:自写路由或混用两套
数据请求与缓存
TanStack Query + 统一的类型化 HTTP 客户端 · MIT
备选:框架自带的 loader(使用 SSR 框架时与框架数据机制对齐);SWR(需求简单、只要缓存与重新验证)
不推荐:自建全局请求缓存
界面状态
组件内 state 和 context;跨视图共享的轻量状态用 Zustand · MIT
备选:Jotai(原子化的跨区域状态);Redux Toolkit(已有 Redux 资产或需要严格的动作审计)
不推荐:把服务器实体复制进全局 store
表单与校验
React Hook Form + Zod · MIT
备选:TanStack Form(类型要求高的动态表单);原生 form + schema 校验(简单表单)
不推荐:手写通用校验引擎
基础组件(定制产品界面)
shadcn/ui + Base UI · shadcn/ui、Base UI、Radix、React Aria、Mantine 为 MIT
备选:shadcn/ui + Radix(已有 Radix 项目,不必迁移);React Aria Components(可访问性要求严、组件复杂);Mantine(快速搭建内部工具,不需深度定制)
不推荐:自写焦点陷阱和可访问弹层;同一组件层混用多套基础组件库
管理后台组件库
Ant Design · MIT
备选:Element Plus(Vue 项目)
样式方案
Tailwind CSS + CSS 变量(定制产品);Ant Design 项目用其主题机制 · MIT
备选:CSS Modules 或普通 CSS(团队不用原子类或组件库自带样式体系)
不推荐:同时混用多套原子类或 CSS-in-JS;在线字体服务
图标
lucide-react(按需导入) · ISC
备选:@ant-design/icons(Ant Design 项目)
不推荐:运行时从在线图标 CDN 加载
表格
TanStack Table(高度定制);后台用 Ant Design Table · TanStack Table 为 MIT;AG Grid 社区版 MIT,企业版商业
备选:AG Grid 社区版(大型数据网格;企业版功能需商业许可)
不推荐:照着旧主版本教程写新版代码;自研排序、列拖动、树形行
虚拟列表
TanStack Virtual · MIT
备选:react-window(固定行高的简单列表)
不推荐:自研动态高度虚拟滚动
拖拽
@dnd-kit/core · MIT
备选:@dnd-kit/react(新项目可评估;仍处 0.x,关键路径先做迁移和键盘回归);原生 HTML 拖放(文件拖入等简单交互)
不推荐:react-beautiful-dnd
日期时间
date-fns + Intl(目标浏览器支持时可用 Temporal) · MIT
备选:Day.js(Ant Design 项目);Luxon(时区逻辑重)
图表
Apache ECharts(复杂看板);Recharts(React 简单报表) · ECharts 为 Apache-2.0;Recharts 为 MIT
备选:visx 或 uPlot(特殊定制或大规模时序数据)
不推荐:自写坐标轴、提示框和布局
代码编辑与 diff
Monaco Editor(类 IDE 和 diff);CodeMirror(轻量编辑) · MIT
备选:react-diff-view(只读 diff 展示)
不推荐:在普通评论框加载 Monaco;用 textarea 造编辑器
Markdown 渲染
react-markdown + remark-gfm,禁用原生 HTML · react-markdown 为 MIT;DOMPurify 为 Apache-2.0 或 MPL-2.0 双许可
备选:markdown-it + DOMPurify(有特定语法插件需求)
不推荐:渲染结果直接 dangerouslySetInnerHTML
富文本
Tiptap 核心(扩展模型成熟) · Tiptap 核心、Lexical、ProseMirror 为 MIT;Tiptap Pro 和云服务为商业
备选:Lexical(需要深度定制编辑器);ProseMirror(高度特殊的编辑模型)
不推荐:自造选区、撤销、粘贴处理
协同编辑
Yjs + 编辑器绑定 + provider + 自有的权限与持久化 · MIT
备选:Automerge(需要不同的同步模型)
不推荐:自写 CRDT 或 OT;把 Yjs 文档当业务审计记录
白板与流程图
Excalidraw(自由绘图);React Flow(节点编辑);Mermaid(只读图示) · Excalidraw、React Flow、Mermaid 为 MIT
备选:ELK.js 或 Dagre(需要自动布局;ELK.js 为 EPL-2.0);tldraw(先审许可再用)
不推荐:把自由白板当流程引擎
文件上传
原生 File / FormData + react-dropzone · MIT
备选:Uppy(大文件分片、断点续传、多来源)
PDF 预览
react-pdf(wojtekmaj)+ 本地提供的 PDF.js Worker · react-pdf 为 MIT;PDF.js 为 Apache-2.0
备选:直接封装 pdfjs-dist(需要细粒度控制)
不推荐:react-pdf-viewer;外部 PDF 查看服务
Office 预览
服务端转换成 PDF 或图片后预览
备选:自托管 Collabora Online 或 ONLYOFFICE(需要原格式交互编辑;许可证逐项审查)
不推荐:公网在线 Office 预览
实时通信
服务端单向推送用 SSE;双向用 WebSocket;流式响应用 fetch 流 · Socket.IO 为 MIT
备选:Socket.IO(需要其协议生态和降级能力)
不推荐:每条消息轮询;组件里各自开连接和重连
前端国际化库
i18next + react-i18next · MIT
备选:FormatJS(react-intl)(需要完整 ICU 消息格式);vue-i18n(Vue 项目)
不推荐:运行时请求公网翻译
动画
CSS 过渡 + View Transitions · MIT
备选:Motion(需要编排复杂动画)
不推荐:为简单交互引入重型动画库
组件文档
Storybook(组件库、设计系统) · MIT
备选:Ladle(需要更轻量)
不推荐:每个业务单页应用都装 Storybook
错误监控
Sentry 自托管或自有错误收集服务,配合发布编号和 sourcemap 管理
备选:GlitchTip(需要更轻量的自托管方案)
不推荐:默认把异常和 sourcemap 上传公网 SaaS
性能监控
web-vitals 采集 + 自有收集端 + Lighthouse CI · web-vitals 为 Apache-2.0;OpenTelemetry 为 Apache-2.0
备选:OpenTelemetry 浏览器 SDK(需要与后端链路打通;官方仍标为实验性)
不推荐:只看本地 Lighthouse
OpenAPI 客户端生成
见 10(TypeScript 生成器选择);契约语义见 16
TypeScript 配置、lint 与格式化、包管理器、单元测试库
见 10
✕ 错误做法:把所有接口数据放进全局 store
✓ 应该:服务器数据交给请求缓存库,store 只放客户端界面状态
✕ 错误做法:每个页面自己写 fetch、401 处理和重试
✓ 应该:统一 HTTP 客户端层加接口生成类型
✕ 错误做法:随手新建 Modal、Popover、Select
✓ 应该:基于可访问基础组件库实现
✕ 错误做法:为一千行数据自写虚拟列表
✓ 应该:用 TanStack Virtual
✕ 错误做法:几百 KB 的编辑器加载到所有页面
✓ 应该:按路由或交互懒加载
✕ 错误做法:把工具输出或评论里的 Markdown 当 HTML 注入
✓ 应该:react-markdown 禁用原生 HTML 并校验链接
✕ 错误做法:在 useEffect 里随手开 WebSocket 且不清理
✓ 应该:集中的通信层管理连接、重连和去重
✕ 错误做法:看包名相似就引入
✓ 应该:核对精确包名、许可证和归档状态
✕ 错误做法:把 npm update 当修复
✓ 应该:锁定版本、逐个升级、完整回归、准备回滚
✕ 错误做法:断网后字体和图标不见了,却说系统支持离线
✓ 应该:屏蔽外网后跑完整的冒烟测试
✕ 错误做法:用 UI 截图代替功能断言
✓ 应该:E2E 断言六种状态,截图只做视觉验收
✕ 错误做法:引入一批库作为「平台能力储备」
✓ 应该:有需求时按功能引入并登记