刚开始接到一个只有三个字母的项目名“rea”,我其实愣了一下。说它是单词吧,不像,说它是缩写吧,又太短。但你真去翻翻技术圈的命名习惯,就会发现这种三个字母的项目名反而是常态。前有 Vue 的全家桶,后有各种工具链的简写命名,短名字往往承载着密集的技术栈信息。放在前端这个语境下,“rea”最合理的展开方式就是一套以 React 为核心的前端组合体系,配套 Redux 做状态管理,再叠加 Redis 做缓存层——三个东西的首字母连起来刚好是“rea”。
这篇文章我就把这条线完整梳理一遍。不管你是刚入门想搞懂这三者的关系,还是已经写了一阵子业务代码、想把手里的项目整理得更清爽,都可以参考我这里面的拆解思路。我会讲清楚每个环节解决什么问题、为什么这么配、以及实际落地时容易踩哪些坑。整个体系其实不复杂,但组合在一起,覆盖面会很广。
1. 内容整体设计与思路拆解
1.1 从命名反推核心技术栈
看到“rea”的时候,我脑子里其实并行闪过了几个联想方向。第一个是 React,毕竟这是前端绕不开的一个库;第二个是 Redux,因为在 React 生态里做状态管理,十个人里有八个第一个接触的都是它;第三个是 Redis,这个可能稍微偏后端一点,但你只要做过性能优化、缓存层设计,就一定绕不过它。
三个技术放在一起,恰好形成了一条从页面渲染到数据处理、再到性能加速的完整链路。用“rea”作为项目代号,本质上就是想把这套组合方案做成一个可复用的模板——不管是做中后台系统、移动端 H5、还是轻量级的电商页面,都可以用这套骨架快速起项目。
从设计角度说,这个组合解决的核心问题其实是在一个项目里把“界面怎么组织”“数据怎么流动”“性能怎么兜底”这三件事同时理顺。单拎出任何一个都容易,但组合在一起还能保持清晰,就需要在架构层面提前定好规矩。这也是我把“rea”当作一套体系来解读的原因。
1.2 为什么是这三件套,而不是别的方案
可能有人会说,状态管理不是还有 MobX、Zustand 吗?缓存不是还有 Memcached 吗?性能优化不是可以用 Web Worker 吗?为什么偏偏选 React + Redux + Redis?
我的理解是,这套组合的优势在于生态成熟度和思维模型的一致性。React 的组件化思维、Redux 的单向数据流、Redis 的键值模型,三者都有一个共同特征:用相对简单的数据结构去描述复杂问题。组件树是树形结构,状态树是普通对象,缓存是键值对——你不需要在脑子里维护大量的隐式状态关联,一切都是显式的、可追踪的。
另外,这套组合的学习资料和社区案例都非常丰富。遇到问题搜一下基本都有答案,不需要在一个坑里蹲太久。虽然现在 Zustand 这类轻量方案很流行,Redux 也经常被吐槽样板代码多,但如果你做一个需要长期维护、多人协作的项目,Redux 的规范性反而变成一种优势——大家都按同样的模式写代码,交接成本会低很多。
1.3 这套方案的通用应用场景
“rea”这套组合最典型的应用场景是中后台管理系统,比如用户管理、订单处理、数据看板这类页面多、交互密集、状态关联复杂的项目。
为什么?因为中后台页面的特点是:表格多、表单多、弹窗多、状态多。一个订单列表页,可能要同时维护分页参数、筛选条件、选中项、弹窗开关、加载状态、接口返回的数据快照……如果全部塞在组件内部用 useState 管,代码很快就乱成一团。而用 Redux 统一管理之后,每个页面模块从哪拿数据、往哪提交修改,都是可追踪的。前端页面性能的稳定性,则交给 Redis 来处理接口缓存。
除此之外,移动端 H5 也经常用这套组合。H5 的特点是首屏加载要求快、交互不能太卡顿,而 Redis 缓存可以在接口层面先挡住一部分重复请求,配合 Redux 的全局状态管理,能省掉不少重复渲染的问题。
2. 核心细节解析与实操要点
2.1 React 组件层:状态与渲染的最小闭环
先看最基础的 React 层。很多新手写组件,第一反应是“我需要什么数据,就放一个 state 管着”。逻辑本身没错,但问题容易出在“多个组件共享同一份数据”的时候。
举个例子:一个用户列表页,父组件里有个弹窗需要展示当前选中的用户信息,同时列表行要高亮选中项,搜索框要显示当前查找的关键词。如果用局部 state 管,这些状态分别散落在父组件、子组件、弹窗组件里面,改一个地方还得想着其他几个同步改,非常容易出现状态不一致。
我常用的做法是先在组件层明确两条规则:只把“只属于当前组件自身 UI”的状态留在本地,比如弹窗自身的开关、按钮的 loading、下拉框的展开收起;其余涉及跨组件共享的数据,一律交给 Redux。这样本地状态负责控制的只是“看得见摸得着”的 UI 细节,全局状态负责的是业务数据。
在 React 里还有一个容易忽略的点:组件拆分的粒度。拆得太细,props 层层传递会变得很啰嗦;拆得太粗,一个组件几百行,阅读和测试都很困难。我个人的尺度是:一个组件如果超过三百行,或者同时承担了渲染和业务逻辑处理两大类职责,就考虑拆开。
2.2 Redux 状态管理:单向数据流如何落地
Redux 的核心思想是单向数据流:组件通过 action 发出意图,reducer 根据 action 计算新状态,store 更新数据,然后通过订阅机制通知所有使用该数据的组件重新渲染。
这套流程初看有点绕,但好处是每一步都是纯函数式的,可预测、可调试。实际项目中我建议用 Redux Toolkit(简称 RTK)来写,官方推荐的工具链,样板代码比手写传统 Redux 少很多。RTK 里内置了 createSlice,可以把 action 和 reducer 写在一个对象里,再通过 configureStore 统一创建 store,整体代码量至少减少三分之一。
状态结构的设计上,我踩过最大的坑是“把所有东西都往 Redux 里塞”。开头说了,本地 UI 状态不要全局化。否则你会看到 Redux 里挂着一堆 isModalVisible、isDropdownOpen 这种纯 UI 标志位,每次点击都要 dispatch 一下,完全没必要。
关于状态管理的规范,我总结了三条实操原则:
- 原则一:异步逻辑放 thunk 或副作用函数里,reducer 只处理同步的 state 变更。这样把接口请求、数据处理这些“脏活”和纯函数计算拆开,出问题时定位更快。
- 原则二:按领域拆 reducer,而不是按页面拆。比如 userReducer 管所有和用户相关的状态,orderReducer 管所有和订单相关的状态,即使订单状态只在一个页面用到,也不要拆成 orderListPageReducer,否则后续扩展会非常痛苦。
- 原则三:把“请求状态”和数据本身分开存。比如一个列表数据,建议同时维护 data、loading、error 三个字段,而不是只用一个数组变量。这样处理加载、报错、重试的逻辑会清晰很多。
2.3 Redis 缓存层:性能兜底的关键一环
说完前端两层,再来说说 Redis。它本质上是一个基于内存的键值数据库,读写速度极快(微秒级)。在前端项目里引入 Redis,通常不是直接让浏览器连 Redis,而是在后端接口层加缓存,或者在前端通过 BFF(Backend For Frontend)层做数据聚合时缓存。
Redis 在“rea”架构里的主要职责是:把热点数据的查询压力从数据库层挡掉。比如某个产品详情页的接口,一天被调用几十万次,每次都要查 MySQL,肯定扛不住。这时候可以把查询结果序列化之后丢进 Redis,key 可以用产品 id 拼上当前筛选条件和分页参数,value 就是接口返回的数据,同时设置一个合理的过期时间,比如五分钟。五分钟内同样参数的请求直接返回缓存结果,数据库压力瞬间降下来。
Redis 里最关键的两个概念是过期策略和缓存穿透。过期策略好理解,就是给 key 设置 TTL(Time To Live),到期自动删除。缓存穿透则是“查一个不存在的数据”:每次请求都先查 Redis,发现没有,再查数据库,数据库也没有,于是每次都穿透到数据库层,然后缓存里始终存不下空结果。
解决穿透的经典做法有两个:一是对空结果也做短期缓存,比如一分钟;二是用布隆过滤器,在缓存层之前先判断这个 key 是否存在。前者简单,直接可用;后者更严谨,但实现成本偏高。
3. 实操过程与核心环节实现
3.1 从零搭建一个最小的 rea 项目
我用一个具体的例子来说明这套体系怎么落地。目标:搭建一个带“用户列表 + 用户详情弹窗”的最小业务场景,前端用 React,全局状态用 Redux Toolkit,后端接口层用 Redis 做缓存。
第一步,初始化前端项目。现在最省事的方式是用构建工具直接初始化:
npx create-vite my-rea-demo --template react-ts cd my-rea-demo npm install之所以选 TypeScript 模板,是因为后面写 reducer、状态切片的时候,类型定义能帮我们提前发现结构错误,尤其是大项目里字段特别多的情况下。
第二步,安装 Redux Toolkit 和连接 React 的依赖:
npm install @reduxjs/toolkit react-redux第三步,创建 store。在 src 目录下建一个 store 文件夹,里面放 index.js 和用户状态切片文件。
import { createSlice, configureStore } from '@reduxjs/toolkit'; const userSlice = createSlice({ name: 'user', initialState: { data: [], loading: false, error: null, }, reducers: { fetchStart(state) { state.loading = true; state.error = null; }, fetchSuccess(state, action) { state.data = action.payload; state.loading = false; }, fetchError(state, action) { state.error = action.payload; state.loading = false; }, }, }); export const { fetchStart, fetchSuccess, fetchError } = userSlice.actions; export const store = configureStore({ reducer: { user: userSlice.reducer, }, });这段代码里最关键的设计是 initialState 的三个字段。data 存实际返回的列表,loading 控制加载状态,error 存放错误提示。这样组件里只需要通过 useSelector 读取这几个字段,就能完整表达“加载中”“加载成功”“加载失败”三种情况。
第四步,在 React 组件里连接 store。用 react-redux 提供的 Provider 把 store 挂到根组件上,然后子页面里通过 useDispatch 触发 action,通过 useSelector 读取数据。
import { useDispatch, useSelector } from 'react-redux'; import { fetchStart, fetchSuccess, fetchError } from '../store/userSlice'; function UserList() { const dispatch = useDispatch(); const { data, loading, error } = useSelector((state) => state.user); const loadUsers = async () => { dispatch(fetchStart()); try { const res = await fetch('/api/users'); const json = await res.json(); dispatch(fetchSuccess(json.data)); } catch (e) { dispatch(fetchError(e.message)); } }; useEffect(() => { loadUsers(); }, []); if (loading) return <div>加载中...</div>; if (error) return <div>加载失败:{error}</div>; return ( <ul> {data.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }这个流程跑通之后,你会发现 Redux 在里面的作用就是统一管理“用户数据”这份全局状态。不管后续有多少个组件都要读取同一份 user 列表,它们都从 state.user.data 取值,不用再一层层传 props。
3.2 接入 Redis 缓存接口数据
前端部分跑通之后,核心接口层要引入 Redis。这里的逻辑是:当前端请求/api/users接口时,后端先查 Redis,如果有缓存数据就直接返回;没有缓存,才去数据库查询,然后把查询结果写入缓存并返回。
假设后端用的是 Node.js,配合 Redis 的官方客户端库:
import Redis from 'ioredis'; const redis = new Redis({ port: 6379, host: '127.0.0.1' }); async function getUsersFromCache() { const cacheKey = 'api:users'; const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } // 模拟从数据库查询 const users = await queryDatabase('SELECT * FROM users'); // 写入缓存,5分钟过期 await redis.set(cacheKey, JSON.stringify(users), 'EX', 300); return users; }这里有几个关键参数要说明。cacheKey 的命名规则我用的是api:资源名,比如api:users、api:users:123,冒号分隔可以让键值在 Redis 里按前缀做信息分类。过期时间 300 秒并不是拍脑袋定的,而是根据业务数据更新频率折中的结果——太短的话缓存效果差,太长的话数据可能出现延迟,具体场景需要测试后自己调整。
Redis 提供的命令我经常用的是 set、get、expire、del 四个。del 用于在数据发生变更时主动清理缓存。比如新增了一个用户,前端拿到成功响应后,后端顺手执行一下redis.del('api:users'),下次请求就会重新查库并刷新缓存。这就是最简单的缓存一致性处理方式。
3.3 联调中的关键参数与配置细节
联调是最容易出问题的地方,我挑几个最常踩的细节展开。
第一个是跨域配置。前端本地开发跑在 5173 端口,后端接口跑在 3000 端口,浏览器会直接拦掉跨域请求。我在项目里处理的方式是在 Vite 的配置里加代理,而不是在后端开 CORS:
export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, }, }, }, });这个做法能避免生产环境和开发环境跨域策略不一致的问题。前端代码里请求统一写相对路径/api/xxx,本地开发环境通过代理转发到后端,生产环境再由网关做路径转发。
第二个是 Redis 连接池的超时参数。默认配置下,如果 Redis 服务偶发超时,后端的请求会一直阻塞到连接超时,前端等久了就会出现白屏。我的建议是给 Redis 客户端加上 maxRetriesPerRequest 和 connectTimeout 配置:
const redis = new Redis({ port: 6379, host: '127.0.0.1', connectTimeout: 5000, maxRetriesPerRequest: 1, });这样 Redis 一旦不可用,请求会快速失败,而不是无限等待。
第三个是接口返回数据的结构。前端用 Redux 管理状态之后,最怕的就是后端返回的数据结构经常变。我建议后端返回统一的结构:
{ "code": 0, "message": "success", "data": { "list": [], "total": 10 } }前端在派发 action 时,固定从 data 字段中取值。如果后端返回格式不统一,前端 reducer 里就要做大量的容错判断,代码会变得非常难看。
4. 常见问题与排查技巧实录
4.1 Redux 状态更新了,组件却没刷新
这是新手最容易卡住的问题。状态看起来变了,但页面没反应。我遇到过的场景都是同一个原因:在 reducer 里直接修改了原始 state 对象。
Redux 的更新机制依赖引用变化。如果你在 reducer 里直接state.data.push(user),state 的引用并没有变,Redux 会认为没有任何更新,组件自然不重新渲染。
排查手法很简单:检查 reducer 里有没有直接的赋值、push、splice 这类操作。正确做法是返回一个新对象,或者用 RTK 的 immer 能力(它内部会自动处理不可变更新)。RTK 里写state.data = action.payload这种写法是被允许的,因为 RTK 内部通过 immer 帮你生成了 new state;但如果你是手写原生 reducer,千万别这么做。
4.2 缓存穿透导致数据库压力反而更大
加入 Redis 之后,数据库压力不降反升的情况,十有八九是缓存穿透。常规数据都能查到,缓存能命中,但用户搜索一些不存在的数据,每次都穿透。
我在实际排查时,会先看 Redis 的 key 命中率监控。如果发现某个接口的 key 命中率骤降,而数据库请求量飙升,基本就是缓存穿透的问题。
解决手法按优先级排序:
- 对空结果做缓存,TTL 设短一些,比如 60 秒,防止恶意高频请求直接打穿数据库。
- 对请求参数做基础校验,比如一个 id 明显不符合格式,直接返回错误,不进入查询链路。
- 布隆过滤器方案适合稳定性要求比较高的系统,实现起来需要额外维护一个过滤层。
最快速见效的还是第一种,成本低、逻辑简单。我一般会把“空数据缓存”直接集成到查询函数里:查完数据库,如果结果为空,就缓存一个约定好的空标记(比如空数组或空字符串),过期时间控制在 30 到 60 秒。
4.3 接口请求竞态导致旧数据覆盖新数据
这个情况在 React 项目里特别隐蔽。比如用户在一个列表页快速切换筛选条件,第一次请求比较慢,第二次请求比较快,结果第二次先返回,reducer 更新了数据,紧接着第一次的旧数据也返回了,又把新数据覆盖掉。页面显示的结果和当前筛选条件不匹配。
排查的时候,用 React DevTools 看 state 变化能发现数据被多次改写。解决办法是在触发请求之前带上一个请求序列号,只有最新的请求才允许更新状态:
let requestSeq = 0; async function loadUsers(params) { const currentSeq = ++requestSeq; const res = await fetch(`/api/users?search=${params}`); const json = await res.json(); if (currentSeq === requestSeq) { dispatch(fetchSuccess(json.data)); } }这里用一个模块级的自增变量做标记,每次请求前先递增,等响应返回时对比一下,就能判断当前响应是不是最新发出去的。
4.4 开发环境一切正常,生产环境样式混乱
这类问题通常不在“rea”三件套的核心逻辑里,但确实会影响整体体验。最常见的原因是 CSS 类名冲突。React 生态里推荐的做法是用 CSS Modules,组件内部的样式文件引入时会自动添加哈希后缀。如果你用了全局 CSS 文件,生产打包时类名重名就会互相覆盖。
排查技巧:打开浏览器控制台,查看实际渲染出来的类名里有没有哈希后缀。没有的话,说明样式没有被模块化处理。把普通import './style.css'改成import styles from './style.module.css',并在组件里用styles.xxx引用类名即可。
4.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| Redux 更新但组件未刷新 | reducer 直接修改了原始 state | 查看 reducer 代码里是否有直接赋值 | 返回新对象或用 RTK + immer |
| 数据库压力增大 | 缓存穿透 | 观察缓存命中率和 DB 请求量 | 空结果缓存或布隆过滤器 |
| 快速切换条件时数据显示错乱 | 请求竞态 | 观察 reducer 里数据的更新顺序 | 用自增序列号丢弃过期响应 |
| 接口一直 pending | Redis 连接阻塞 | 检查 Redis 客户端超时配置 | 设置 connectTimeout 和 maxRetriesPerRequest |
| 缓存数据陈旧 | TTL 太长或未主动清理 | 检查缓存过期时间与写操作后的处理 | 缩短 TTL 或写操作后 del 缓存键 |
| 生产环境样式乱 | CSS 类名冲突 | 检查浏览器渲染的类名 | 使用 CSS Modules |
5. 从 rea 扩展到更多场景
5.1 加入路由层和组件库
前面这套最小体系里还没有引入路由。实际做项目肯定要有页面切换,所以“rea”模板通常会再加入 React Router,把页面划分成若干个路由模块,每个模块对应一个页面容器,页面内再拆分业务组件。
组件库方面,中后台项目建议直接引入成熟的 UI 组件库,比如基于 React 的 antd。它能帮我们解决表格、表单、日期选择、弹窗等高频组件的开发效率问题。但要注意按需加载,否则打包体积会大得离谱——现在常用的方式是用 Vite 插件配合组件库的按需导入配置,让构建工具自动处理样式和代码的按需引入,只把用到的组件打包进产物。
引入组件库之后,Redux 里存储的数据结构建议跟 UI 组件的 props 结构尽量对齐。比如 antd 的 Table 组件需要 dataSource、columns、pagination 这几个字段,那 state 里数据的组织方式就按这个结构存,避免每次渲染前还要做大量的数据转换。
5.2 性能优化与监控
“rea”架构的性能优化主要分三层:加载性能、运行性能、数据性能。
加载性能优化关注的是首屏 bundle 大小。通过代码分割(React.lazy 和 Suspense)把不同路由对应的组件拆成独立的 chunk,首屏只加载必需的核心代码。
运行性能优化关注的是组件渲染效率。React 的 useMemo 和 useCallback 是常用工具,但也不能滥用,每个 memo 都会带来额外的缓存开销。我的习惯是:只有遇到真实的渲染卡顿才做针对性优化,不提前做过渡优化。
数据性能优化关注的就是 Redis 这一层。除了缓存接口数据,可以进一步把一些不容易变化的数据,比如字典数据、城市列表、配置项,常驻 Redis,TTL 设置成一个更长的时间,比如一小时或一天。
监控层面,可以给 Redis 加 info 命令的定时巡检,观察内存使用和 key 数量变化;给 Redux 加 middleware,记录每次 action 的派发日志和 state 变化时间,方便定位性能瓶颈到底是在数据变更环节还是渲染环节。
5.3 服务端渲染和移动端扩展
“rea”组合天然支持扩展成服务端渲染(SSR)架构。React 生态里的 Next.js 和 Redux 配合得相当好,服务端先获取数据填充 store,客户端拿到初始化状态直接渲染,能解决首屏白屏和 SEO 问题。移动端方面,React Native 和 Redux 也是经典搭配,唯一需要注意的是 Redis 缓存层需要走移动端接入的 BFF 服务,而不是让 App 直连数据库。
实际项目里我用这个模式做过一次跨端改造:同一套 Redux store 逻辑,同时服务于 Web 端和 App 端,两端的接口请求都打到一个 BFF 层,BFF 层统一对接 Redis 和数据库。这样改动业务时只需要改一份状态逻辑,两端共用,开发成本降低了不少。
6. 写在最后的实操心得
按“rea”这套结构做项目也有一年多时间,踩过不少坑,也沉淀了一些自己的习惯。这里分享几个个人感受。
第一,Redux 每多存一份数据,都要问自己一句:这份数据真的需要跨组件共享吗?如果只是某个组件内部的临时状态,放 useState 就好。把状态管理的边界划清楚,后续维护会省很多心力。
第二,Redis 的过期时间不是配置完就不管了。要定期看命中率监控,根据不同接口的实时调用情况微调 TTL。一个接口如果半小时内数据都没变化,TTL 设成 5 分钟就是白白浪费。
第三,整套体系最怕的不是技术不熟,而是没有一个固定的代码规范。我给团队里订了几条死规矩:所有接口请求都走统一的请求模块,所有异步更新状态都必须走 thunk 或者副作用函数,所有 reducer 里禁止写复杂逻辑。一旦这些规范从项目一开始就立住,后面即使加人加功能,代码也都像是同一个人写的。
最后再分享一个小技巧:做 Redux 状态设计的时候,可以把页面原型打印出来,用笔在上面画一下哪些数据在哪些组件之间互相传递。你能画出来“这条线要跨两个组件”,这条线就应该走 Redux;如果只是在同一个组件内部绕了一圈,就留在本地。这张纸画完,项目的状态结构基本就定好了,比打开代码直接开写要稳得多。