1. 从“rea”这个标题说起一个被低估的缩写背后藏着什么第一次看到“rea”这个标题的时候我承认自己愣了几秒。三个字母没有上下文没有说明放在任何一个技术社区里都像是某种暗号。但恰恰是这种极简的命名方式在圈子里其实非常常见——很多开发者习惯用项目核心模块的首字母缩写来命名仓库时间久了连他们自己都忘了当初为什么这么叫。我后来花了不少时间去梳理“rea”在不同技术语境下可能指向的含义。结合当前网络上的讨论热度和实际项目经验这个标题最可能指向的是React 生态下的某种工程化实践或者更具体一点是围绕React 应用架构中“响应式、可扩展、可维护”三个核心诉求展开的一套方法论。也有人把它理解为“Runtime Environment Adapter”的缩写指向运行时环境适配层的设计与实现。不管是哪种解读核心都绕不开一个关键词前端工程化中的架构抽象。为什么我要从这个角度切入因为单纯讨论“rea”这三个字母本身没有意义真正有价值的是它背后代表的那类问题——当你的前端项目从几十个组件膨胀到几百个组件从单页面扩展到多页面、多端复用的时候你会发现代码的组织方式比具体用什么框架重要得多。这也是我在过去几年里反复踩坑、反复重构之后最深的体会。这篇文章适合谁看如果你正在经历以下任何一种情况那接下来的内容应该能帮到你项目里的组件越写越多但复用率极低状态管理逻辑散落在各个角落改一个地方要翻五个文件团队里每个人写代码的风格都不一样代码审查变成吵架现场或者你只是单纯对“如何让 React 项目在规模变大之后依然好维护”这个问题感兴趣。我会从架构设计的底层逻辑讲起一直讲到可以直接抄作业的目录结构和代码示例中间穿插我自己踩过的坑和总结出来的经验。2. 核心思路拆解为什么“rea”式架构值得认真对待2.1 从“能跑就行”到“能改才行”的思维转变大部分前端项目在起步阶段都是“能跑就行”。产品经理说要做个列表页你三下五除二写个组件数据一拉渲染出来收工。这个阶段没有任何问题快速交付本来就是第一优先级。但问题在于很多项目在进入第二个阶段——也就是需求开始频繁变更、页面开始互相嵌套、不同角色开始提不同要求的时候——代码结构并没有跟着升级。我见过太多项目到了中期就变成了一锅粥。一个components文件夹下面塞了上百个文件命名从Header到HeaderNew再到HeaderFinal谁也不敢删任何一个。状态管理更是重灾区有的用 Redux有的用 Context有的直接组件内部useState硬扛数据流向完全不可预测。“rea”式架构的核心思路就是在项目还没有烂到无法维护之前先建立一套约束机制。这套机制不依赖某个具体的库或框架而是一组关于“代码应该放在哪里、数据应该怎么流动、组件之间应该怎么通信”的约定。你可以把它理解成前端项目的“交通规则”——没有这些规则车也能开但迟早会堵死。2.2 三个核心原则响应式、可扩展、可维护我把“rea”拆解成三个维度来理解这样在实际操作中更容易落地。响应式Responsive在这里不仅仅指页面适配不同屏幕尺寸更指的是代码层面对变化的响应能力。当业务逻辑变了你需要改几个文件当一个接口的返回结构变了有多少地方会报错好的架构应该让这些变更的影响范围尽可能小。具体到实现上就是要把“变化的”和“不变的”隔离开把“经常一起变的”放在一起。可扩展Extensible指的是当新需求来的时候你是在现有基础上“加东西”而不是“改东西”。这两者的区别很大。加东西意味着你只需要新增文件、新增配置原有代码不动改东西意味着你要理解原有逻辑然后小心翼翼地修改生怕碰坏什么。前者是开方式的后者是封闭式的。我个人的经验是如果一个新页面的开发需要修改超过三个已有文件那说明架构的扩展性已经出问题了。可维护Maintainable是最容易被忽视但最重要的一个。可维护性体现在很多细节上命名是否清晰、依赖关系是否明确、测试是否容易写、新人是否能快速上手。我判断一个项目可维护性的标准很简单让一个没接触过这个项目的人在半小时内找到“用户登录”这个功能的全部相关代码。如果他能做到说明结构是清晰的如果他翻了半天还在问“这个逻辑到底在哪”那就需要重构了。2.3 为什么不用现成的框架或模板你可能会问市面上已经有那么多脚手架和模板了为什么还要自己折腾一套架构这个问题我认真想过。现成的方案当然有它的价值比如 Create React App 帮你省去了构建配置的麻烦Next.js 帮你解决了服务端渲染的问题。但它们解决的是“从零到一”的问题而“rea”式架构要解决的是“从一到一百”的问题。更重要的是每个项目的业务场景都不一样。电商项目的核心是商品和订单社交项目的核心是用户和关系链工具类项目的核心是功能和权限。这些差异决定了它们的代码组织方式不可能完全一样。直接套用别人的架构就像穿别人的鞋——能穿但不一定合脚。我自己的做法是先理解“rea”这三个原则然后根据项目的实际特点来设计具体的目录结构和数据流方案。下面我会详细展开我是怎么做的。3. 核心细节解析目录结构、状态管理与组件设计3.1 目录结构按“功能”还是按“类型”这是前端架构里最经典的争论之一。按类型分就是components、hooks、utils、services这样的文件夹按功能分就是user、product、order这样的模块文件夹每个模块内部再细分。我两种都用过最后的选择是混合模式顶层按功能分功能内部按类型分。具体来说目录结构大概长这样src/ modules/ user/ components/ hooks/ services/ types.ts index.ts product/ components/ hooks/ services/ types.ts index.ts shared/ components/ hooks/ utils/ app/ routes/ store/ providers/这样做的理由是功能模块内部的代码内聚性最高应该放在一起。比如用户模块的组件、钩子、接口调用它们之间的关联远比用户模块的组件和商品模块的组件之间的关联要强。把它们放在同一个文件夹下修改用户逻辑的时候就不需要跨目录跳来跳去。而shared目录放的是真正跨模块复用的东西比如通用的按钮组件、日期格式化工具、请求封装等。这里有一个判断标准如果一个东西被三个以上的模块用到才考虑放进 shared。如果只有两个模块用我倾向于先放在其中一个模块里等第三个模块也需要的时候再提取出来。过早抽象比重复代码更可怕。app目录则是整个应用的入口层负责路由注册、全局状态挂载、Provider 嵌套这些“胶水”工作。这一层应该尽量薄不包含具体的业务逻辑。3.2 状态管理什么时候用全局什么时候用局部状态管理是前端架构里最容易过度设计的地方。我见过太多项目明明只有几个页面却上来就装 Redux然后写一大堆 action、reducer、selector最后发现真正跨组件共享的状态只有用户信息那一个。我的原则是从局部开始按需升级。具体分三个层次第一层是组件内部状态用useState就够了。比如一个弹窗的显示隐藏、一个表单的输入值这些状态不需要被其他组件知道放在组件内部最合适。第二层是模块内部共享状态用Context加useReducer的组合。比如用户模块内部用户信息、权限列表、偏好设置这些状态需要在多个子组件之间共享但又不需要暴露给商品模块。这时候在模块顶层建一个 Context把状态和更新方法传下去既解决了共享问题又保持了模块的边界。第三层才是全局状态比如当前登录用户、主题配置、语言设置这些真正跨模块的东西。这一层我倾向于用轻量级的方案比如 Zustand 或者 Jotai而不是 Redux。原因很简单全局状态越少越好而 Redux 的模板代码太多容易让人产生“什么都要放进去”的冲动。这里有一个我踩过的坑不要用全局状态来传递页面之间的临时数据。比如从列表页跳到详情页需要传递一个 ID很多人会把它存到全局状态里。这样做的问题是当用户直接刷新详情页的时候全局状态是空的页面就挂了。正确的做法是通过路由参数传递这样刷新也能恢复。3.3 组件设计容器与展示的分离还有必要吗“容器组件”和“展示组件”的分离是 React 社区早期非常流行的一种模式。容器组件负责数据获取和状态管理展示组件负责渲染 UI。后来随着 Hooks 的普及这种模式被很多人抛弃了因为 Hooks 让逻辑复用变得更容易不需要再通过组件嵌套来实现。但我的观点是分离的思想依然有价值只是实现方式变了。以前是拆成两个组件现在可以拆成一个自定义 Hook 加一个纯展示组件。比如// useUserProfile.ts function useUserProfile(userId: string) { const [profile, setProfile] useState(null); const [loading, setLoading] useState(true); useEffect(() { fetchUserProfile(userId) .then(setProfile) .finally(() setLoading(false)); }, [userId]); return { profile, loading }; } // UserProfile.tsx function UserProfile({ userId }: { userId: string }) { const { profile, loading } useUserProfile(userId); if (loading) return Skeleton /; if (!profile) return Empty /; return ( div Avatar src{profile.avatar} / Name{profile.name}/Name /div ); }这样拆的好处是useUserProfile这个 Hook 可以在任何需要用户信息的地方复用而UserProfile组件只关心怎么把数据显示出来。测试的时候也方便Hook 可以单独测试组件也可以单独测试。3.4 接口层设计统一封装与错误处理接口调用是前端项目里最容易写乱的地方。我见过直接在组件里写fetch的也见过每个模块各自封装一套请求库的。这两种做法都有问题前者导致错误处理逻辑重复后者导致维护成本翻倍。我的做法是在shared目录下建一个统一的请求封装然后在各个模块的services目录里定义具体的接口函数。请求封装负责处理通用逻辑baseURL 拼接、token 注入、超时设置、错误码映射、重试策略。模块的 service 只负责定义“调哪个接口、传什么参数、返回什么类型”。// shared/api/client.ts const client { async getT(url: string, params?: object): PromiseT { const response await fetch(buildUrl(url, params), { headers: buildHeaders(), }); return handleResponseT(response); }, // post, put, delete 类似 }; // modules/user/services/profile.ts export async function fetchUserProfile(userId: string) { return client.getUserProfile(/api/user/profile, { userId }); }这样做还有一个好处当后端接口发生变化的时候你只需要改 service 文件组件和 Hook 完全不用动。这就是前面说的“把变化隔离在最小范围内”。4. 实操过程从零搭建一个“rea”式项目4.1 初始化与基础配置假设我们现在要从零开始搭建一个项目我会按下面的步骤来操作。首先用 Vite 初始化一个 React TypeScript 的项目因为 Vite 的构建速度比传统的 Webpack 快很多开发体验更好。npm create vitelatest my-app -- --template react-ts cd my-app npm install然后安装几个必要的依赖路由用 React Router状态管理用 Zustand请求库直接用原生的 fetch 封装不引入 axios减少依赖体积样式方案用 CSS Modules避免样式冲突又不需要额外的运行时。npm install react-router-dom zustand接下来配置路径别名这样在导入的时候可以用/modules/user代替../../modules/user代码更清晰。在vite.config.ts和tsconfig.json里分别加上对应的配置。// vite.config.ts import { defineConfig } from vite; import react from vitejs/plugin-react; import path from path; export default defineConfig({ plugins: [react()], resolve: { alias: { : path.resolve(__dirname, ./src), }, }, });这一步看起来简单但非常重要。我见过太多项目因为路径太深导致导入语句变成../../../../components/Button这种可读性极差。别名配置是一次性工作收益却贯穿整个项目生命周期。4.2 搭建模块化目录骨架按照前面说的混合模式我先把目录骨架建起来。这里不需要一次性建全先把核心模块和共享层建好后续按需扩展。mkdir -p src/modules/user/{components,hooks,services} mkdir -p src/modules/product/{components,hooks,services} mkdir -p src/shared/{components,hooks,utils,api} mkdir -p src/app/{routes,providers}然后在每个模块下建一个index.ts作为模块的出口文件。这个文件的作用是控制模块的对外暴露接口外部模块只能从index.ts导入不能直接深入到模块内部的某个文件。这样做的好处是当模块内部结构调整的时候只要index.ts的导出不变外部代码就不需要修改。// modules/user/index.ts export { UserProfile } from ./components/UserProfile; export { useUserProfile } from ./hooks/useUserProfile; export type { UserProfile as UserProfileType } from ./types;4.3 实现统一请求层与错误处理请求层的实现是整个项目的基础设施值得花时间做好。我的实现思路是底层用 fetch上面包一层拦截器机制支持请求前和响应后的处理。// shared/api/client.ts type RequestConfig { params?: Recordstring, string | number; body?: unknown; headers?: Recordstring, string; }; async function requestT( method: string, url: string, config: RequestConfig {} ): PromiseT { const { params, body, headers {} } config; const finalUrl params ? ${url}?${new URLSearchParams( Object.entries(params).map(([k, v]) [k, String(v)]) )} : url; const response await fetch(finalUrl, { method, headers: { Content-Type: application/json, ...headers, }, body: body ? JSON.stringify(body) : undefined, }); if (!response.ok) { const error await response.json().catch(() ({})); throw new ApiError(response.status, error.message || 请求失败); } return response.json(); } export const api { get: T(url: string, params?: Recordstring, string | number) requestT(GET, url, { params }), post: T(url: string, body?: unknown) requestT(POST, url, { body }), // put, delete 类似 };这里有一个细节值得注意错误处理我定义了一个ApiError类包含状态码和消息。这样在组件里捕获错误的时候可以根据状态码做不同的处理比如 401 跳转登录403 显示无权限提示500 显示服务器错误。4.4 编写第一个模块用户模块的完整实现有了基础设施之后写业务模块就很快了。以用户模块为例我需要实现三个东西类型定义、接口服务、展示组件。类型定义放在types.ts里// modules/user/types.ts export type UserProfile { id: string; name: string; avatar: string; email: string; role: admin | editor | viewer; };接口服务放在services/profile.ts里// modules/user/services/profile.ts import { api } from /shared/api/client; import type { UserProfile } from ../types; export function fetchUserProfile(userId: string) { return api.getUserProfile(/api/user/profile, { userId }); } export function updateUserProfile(userId: string, data: PartialUserProfile) { return api.postUserProfile(/api/user/update, { userId, ...data }); }Hook 放在hooks/useUserProfile.ts里// modules/user/hooks/useUserProfile.ts import { useState, useEffect, useCallback } from react; import { fetchUserProfile, updateUserProfile } from ../services/profile; import type { UserProfile } from ../types; export function useUserProfile(userId: string) { const [profile, setProfile] useStateUserProfile | null(null); const [loading, setLoading] useState(true); const [error, setError] useStatestring | null(null); const load useCallback(async () { setLoading(true); setError(null); try { const data await fetchUserProfile(userId); setProfile(data); } catch (e) { setError(e instanceof Error ? e.message : 加载失败); } finally { setLoading(false); } }, [userId]); useEffect(() { load(); }, [load]); const update useCallback( async (data: PartialUserProfile) { const updated await updateUserProfile(userId, data); setProfile(updated); return updated; }, [userId] ); return { profile, loading, error, reload: load, update }; }组件放在components/UserProfile.tsx里// modules/user/components/UserProfile.tsx import { useUserProfile } from ../hooks/useUserProfile; import styles from ./UserProfile.module.css; export function UserProfile({ userId }: { userId: string }) { const { profile, loading, error, reload } useUserProfile(userId); if (loading) return div className{styles.skeleton}加载中.../div; if (error) return div className{styles.error} onClick{reload}{error}点击重试/div; if (!profile) return div className{styles.empty}暂无数据/div; return ( div className{styles.container} img className{styles.avatar} src{profile.avatar} alt{profile.name} / div className{styles.info} h3{profile.name}/h3 p{profile.email}/p span className{styles.role}{profile.role}/span /div /div ); }这样一套下来用户模块的完整功能就实现了。其他模块要使用用户信息的时候只需要从/modules/user导入UserProfile组件或者useUserProfileHook 就行完全不需要关心内部实现。4.5 路由与页面组装最后在app层把各个模块组装起来。路由配置我倾向于用集中式的方式所有路由在一个文件里定义这样一眼就能看出整个应用的页面结构。// app/routes/index.tsx import { createBrowserRouter } from react-router-dom; import { UserProfilePage } from /modules/user; import { ProductListPage } from /modules/product; export const router createBrowserRouter([ { path: /, element: Layout /, children: [ { path: user/:userId, element: UserProfilePage / }, { path: products, element: ProductListPage / }, ], }, ]);这里有一个经验页面级组件应该放在模块内部而不是 app 层。app 层只负责路由映射不包含具体的页面实现。这样当某个模块被移除或者替换的时候只需要改路由配置不需要动 app 层的其他代码。5. 常见问题与排查技巧实录5.1 模块之间的循环依赖怎么破这是模块化架构里最常见的问题之一。比如用户模块需要用到商品模块的某个类型商品模块又需要用到用户模块的某个函数两边互相导入构建工具就会报循环依赖的警告严重的时候会导致运行时错误。我的解决思路分三步。第一步是检查这个依赖是否真的必要。很多时候循环依赖的出现是因为我们把不该放在一起的东西放在了一起。比如用户模块里有一个“获取用户收藏的商品”的功能这个功能到底属于用户模块还是商品模块我倾向于把它放在商品模块因为它的核心是商品数据用户 ID 只是一个查询参数。第二步是提取公共依赖到 shared 层。如果两个模块都需要用到同一个类型或工具函数那就把它提到 shared 里两边都从 shared 导入循环就打破了。第三步是用事件机制解耦。如果两个模块确实需要互相通信但又不想互相导入可以引入一个简单的事件总线。用户模块触发事件商品模块监听事件两者之间没有直接的代码依赖。这种方式要慎用因为它会让数据流变得不那么直观但在某些场景下确实是必要的。5.2 状态更新后组件不重新渲染这个问题我遇到过很多次原因通常有几个。最常见的是直接修改了状态对象的属性而不是创建一个新对象。React 判断是否需要重新渲染的依据是状态的引用是否变化如果你直接state.name new引用没变React 就认为状态没变。正确的做法是用展开运算符或者immer这样的库来创建新对象// 错误 setProfile((prev) { prev.name new name; return prev; }); // 正确 setProfile((prev) ({ ...prev, name: new name, }));另一个常见原因是Context 的 value 每次渲染都创建了新对象。比如MyContext.Provider value{{ user, setUser }}每次父组件渲染的时候{ user, setUser }都是一个新对象导致所有消费这个 Context 的组件都重新渲染。解决办法是用useMemo把 value 缓存起来。5.3 接口请求的竞态问题竞态问题是指多个请求同时发出但返回的顺序不确定导致后发的请求先返回先发的请求后返回最终显示的数据是错的。比如用户在搜索框里快速输入每次输入都触发一次请求如果第一次请求比第三次请求晚返回页面就会显示第一次的结果。解决办法有两种。一种是取消之前的请求用AbortController来实现useEffect(() { const controller new AbortController(); fetchData({ signal: controller.signal }) .then(setData) .catch((e) { if (e.name ! AbortError) setError(e); }); return () controller.abort(); }, [query]);另一种是给请求打标记只处理最新一次请求的结果const requestIdRef useRef(0); async function load() { const id requestIdRef.current; const data await fetchData(); if (id requestIdRef.current) { setData(data); } }两种方式都可以我通常用第一种因为取消请求还能节省网络资源。5.4 常见问题速查表问题现象可能原因排查方向解决方案组件不重新渲染状态引用未变化检查 setState 是否创建了新对象用展开运算符或 immer循环依赖警告模块互相导入查看构建工具的警告信息提取公共依赖或使用事件机制接口数据错乱请求竞态检查是否有多个请求同时发出使用 AbortController 或请求标记样式冲突全局样式污染检查是否有同名 class使用 CSS Modules 或命名空间打包体积过大未做代码分割分析构建产物路由级懒加载 动态导入首屏加载慢资源未优化检查网络面板图片压缩、字体子集化、预加载关键资源5.5 几个我踩过的坑和对应的经验第一个坑是过度抽象。刚开始做架构的时候我总想着要“设计得足够灵活”于是定义了很多抽象层和接口。结果就是写一个简单的功能要改五六个文件新人看了直摇头。后来我学乖了抽象要等到重复出现三次以上再做在此之前重复代码是可以接受的。第二个坑是忽略构建工具的配置。很多人只关注业务代码不关注构建配置结果项目大了之后构建速度慢得离谱。我的经验是尽早配置好路径别名、代码分割、依赖预构建这些后期能省很多时间。第三个坑是没有统一的代码规范。团队里每个人都有自己的风格有人喜欢用分号有人不喜欢有人用双引号有人用单引号。这些看起来是小事但代码审查的时候会浪费大量时间在格式争论上。解决办法是配置 ESLint 和 Prettier并且在提交代码前自动格式化。第四个坑是测试覆盖率追求数字。我曾经为了达到 80% 的覆盖率写了很多没有实际意义的测试比如测试一个纯展示组件是否渲染了某个 div。后来我意识到测试应该关注行为而不是实现应该测试“用户点击按钮后是否发起了请求”而不是“按钮的 class 名是否正确”。6. 性能优化与工程化配套6.1 代码分割与懒加载的实操细节当项目变大之后首屏加载时间会成为一个明显的问题。用户打开页面要等好几秒才能看到内容体验很差。代码分割是解决这个问题的核心手段。我的做法是按路由做分割每个页面打包成一个独立的 chunk用户访问哪个页面就加载哪个 chunk。在 React Router 里实现起来很简单import { lazy, Suspense } from react; const UserProfilePage lazy(() import(/modules/user/pages/UserProfilePage)); const ProductListPage lazy(() import(/modules/product/pages/ProductListPage)); // 在路由配置里用 Suspense 包裹 Suspense fallback{PageLoading /} Routes Route path/user/:userId element{UserProfilePage /} / Route path/products element{ProductListPage /} / /Routes /Suspense这里有一个细节懒加载的组件最好用默认导出因为lazy函数期望的返回值是一个包含default属性的模块。如果组件用的是命名导出需要额外处理一下。另一个细节是预加载。当用户鼠标悬停在某个链接上的时候可以提前加载对应的 chunk这样点击的时候就能瞬间打开。实现方式是在链接的onMouseEnter事件里调用import()function PrefetchLink({ to, children }: { to: string; children: React.ReactNode }) { const prefetch () { const path to.split(/)[1]; if (path user) import(/modules/user/pages/UserProfilePage); if (path products) import(/modules/product/pages/ProductListPage); }; return Link to{to} onMouseEnter{prefetch}{children}/Link; }6.2 构建产物的分析与优化代码分割做完之后还需要分析一下产物的构成看看有没有可以优化的地方。Vite 自带了一个分析工具在构建的时候加上--mode analyze参数就能生成可视化报告。我通常会关注几个指标每个 chunk 的大小、第三方依赖的占比、是否有重复打包的模块。如果某个 chunk 特别大就要看看里面是不是包含了不该包含的东西。比如 lodash 如果全量引入会有几百 KB但实际上可能只用了几个函数这时候就应该改成按需引入。// 不推荐 import _ from lodash; _.debounce(fn, 300); // 推荐 import debounce from lodash/debounce; debounce(fn, 300);还有一个常见的优化点是图片资源。很多项目直接把设计稿导出的 PNG 放进项目里一张图就好几 MB。我的做法是能用 SVG 的用 SVG不能用 SVG 的压缩成 WebP 格式并且根据显示尺寸生成多套分辨率用srcset让浏览器自己选择。6.3 环境变量与多环境部署项目开发的时候通常有多个环境本地开发、测试环境、预发布环境、生产环境。每个环境的接口地址、CDN 地址、功能开关可能都不一样。管理这些差异的常见做法是用环境变量。Vite 里以VITE_开头的环境变量会被注入到客户端代码中。我通常建几个文件.env.development、.env.staging、.env.production分别对应不同的环境。# .env.development VITE_API_BASE_URLhttp://localhost:3000 VITE_ENABLE_MOCKtrue # .env.production VITE_API_BASE_URLhttps://api.example.com VITE_ENABLE_MOCKfalse然后在代码里通过import.meta.env.VITE_API_BASE_URL来读取。这里有一个安全提醒不要在前端环境变量里放敏感信息比如数据库密码、私钥等因为这些变量最终会被打包到客户端代码里任何人都能看到。6.4 错误监控与日志上报项目上线之后你不可能时刻盯着用户的屏幕看有没有报错。所以需要一套错误监控机制自动捕获异常并上报到服务端。我通常会在app层做三件事。第一是全局错误边界捕获 React 组件树里的渲染错误显示一个友好的错误页面而不是白屏。第二是全局事件监听捕获window.onerror和unhandledrejection把未处理的异常上报。第三是接口错误统一处理在请求层拦截错误根据错误类型决定是重试、提示还是跳转。// app/providers/ErrorBoundary.tsx class ErrorBoundary extends React.Component { state { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } componentDidCatch(error: Error, info: React.ErrorInfo) { reportError({ error, info, type: react }); } render() { if (this.state.hasError) { return ErrorPage onRetry{() this.setState({ hasError: false })} /; } return this.props.children; } }上报的时候要注意脱敏不要把用户的个人信息、token 等敏感数据传到服务端。同时要做频率限制避免某个错误疯狂上报把服务打挂。7. 团队协作与代码规范落地7.1 代码规范的自动化执行前面提到过团队协作最大的摩擦往往来自代码风格的不统一。解决这个问题不能靠口头约定必须靠工具自动化执行。我的配置组合是ESLint Prettier Husky lint-staged。ESLint 负责代码质量检查比如未使用的变量、错误的依赖数组Prettier 负责格式化比如缩进、引号、分号Husky 负责在 git 提交时触发检查lint-staged 负责只检查本次修改的文件提高速度。// package.json { lint-staged: { *.{ts,tsx}: [eslint --fix, prettier --write], *.{css,md}: [prettier --write] } }这样配置之后每次提交代码都会自动格式化和检查不符合规范的代码根本提交不上去。刚开始可能会有人抱怨麻烦但一周之后就习惯了而且代码审查的时候再也不用讨论格式问题可以把精力放在逻辑上。7.2 组件文档与示例当 shared 目录下的通用组件越来越多的时候新人往往不知道有哪些组件可以用、怎么用。这时候就需要一份组件文档。我试过几种方案最后选择了Storybook。它的好处是可以在浏览器里直接看到组件的各种状态还可以交互式地调整 props非常直观。每个组件写一个.stories.tsx文件定义几种典型用法// shared/components/Button.stories.tsx export default { title: Shared/Button, component: Button, }; export const Primary () Button variantprimary主要按钮/Button; export const Secondary () Button variantsecondary次要按钮/Button; export const Loading () Button loading加载中/Button; export const Disabled () Button disabled禁用/Button;写 Storybook 确实需要额外的时间但收益是长期的。新人可以通过 Storybook 快速了解有哪些组件可用设计师可以确认组件的视觉效果是否符合预期测试也可以基于 Storybook 做视觉回归测试。7.3 代码审查的关注点代码审查是保证代码质量的重要环节但很多团队的代码审查流于形式要么只看格式要么直接点通过。我总结了几个审查时的关注点按优先级排列第一是逻辑正确性。代码是否实现了需求描述的功能边界情况是否处理了错误情况是否有兜底第二是架构一致性。新代码是否遵循了项目的目录结构和分层约定有没有把业务逻辑写在展示组件里有没有跨模块直接导入内部文件第三是可测试性。核心逻辑是否被提取成了纯函数或自定义 Hook是否容易写单元测试第四是性能影响。有没有不必要的重复渲染有没有在循环里做昂贵的计算有没有引入体积很大的依赖第五才是代码风格。这一项其实应该由工具自动处理人工审查时不需要花太多精力。7.4 新人上手指南的编写一个好的项目应该让新人能在一天之内跑起来并提交第一个 PR。为了达到这个目标我通常会在项目根目录写一个CONTRIBUTING.md包含以下内容项目的基本架构说明用一张简单的目录树加注释本地开发环境的搭建步骤包括 Node 版本、依赖安装、环境变量配置常用的开发命令比如启动开发服务器、运行测试、构建生产包代码提交的规范比如分支命名、commit message 格式常见问题的解决方法比如依赖安装失败、端口被占用等这份文档不需要写得很长但一定要准确、可执行。我见过很多项目的 README 写得很漂亮但实际按照步骤操作却跑不起来这种文档还不如不写。8. 一些个人体会和后续扩展方向这套“rea”式架构在我自己的项目里跑了挺长时间中间也经历过几次调整。最大的感受是架构不是设计出来的而是演化出来的。一开始不要追求完美先把基本的分层和模块边界定下来然后在实际开发中不断调整。哪些地方别扭了就说明那里需要优化哪些地方顺畅了就说明那里的设计是对的。另一个体会是工具和规范是为了解决问题而存在的不要为了用而用。我见过一些项目引入了各种先进的工具和框架但团队成员根本用不起来最后反而增加了维护成本。选择工具的时候要考虑团队的实际情况适合的才是最好的。后续如果继续扩展这套架构我会考虑几个方向。一是微前端的接入当项目大到需要多个团队并行开发的时候可以考虑把不同模块拆成独立的子应用通过模块联邦的方式组合。二是服务端渲染的集成如果对首屏性能和 SEO 有要求可以引入 Next.js 或者 Remix 这样的框架把现有的模块化结构迁移过去。三是自动化测试的完善目前主要是单元测试后续可以补充集成测试和端到端测试覆盖更完整的用户流程。不过这些都是后话先把眼前的项目结构理清楚、把团队的协作流程跑顺比什么都重要。架构这件事急不得也省不得。