React虚拟银行模拟器:从数据建模到状态管理的完整实践 📅 发布时间:2026/9/3 19:47:24 👁 浏览次数: 简介react-bank是一个仅用前端实现的虚拟银行模拟器项目面向React、Next.js和TypeScript学习者也适合正在练习Sass样式组织与页面交互设计的开发者。资源共39个文件其中15个tsx组件文件和10个scss样式文件构成主体另含json配置、css样式及少量图片素材整体压缩后仅1.88MBsrc目录下按components、contexts、styles、pages分层组织组件与样式分离便于逐模块阅读和复用。项目模拟了银行账户余额管理、Pix转账、朋友列表以及银/金/铂三级会员计划等业务规则通过上下文状态管理串联页面交互展示了如何将真实金融场景抽象为清晰的前端数据流同时TypeScript类型约束和Sass变量/混合宏的运用也能给中等以上前端开发者带来参考。已有482人学习下载适合作为React全家桶与Next.js实战项目的入门完善案例。1. 为什么虚拟银行是检验 React 基本功的最佳练手项目先从一个现象说起很多前端开发者useState、useEffect 都熟各种面试题背得滚瓜烂熟但真要独立做一个项目拿起键盘就卡住了。我也经历过这个阶段。后来我看到 react-bank 这个项目方向——用纯前端做一个虚拟银行模拟器突然觉得这就是最合适的 React 练手场景。为什么因为银行系统虽然业务是虚构的但它包含了一整套真实业务系统才有的约束账户之间转账必须同步更新双方余额交易流水只增不改余额不能凭空变负数利息结算需要定时触发所有操作都有据可查。这些约束逼着你认真设计数据流而不是像 TODO List 那样随便写写。这个项目能做什么很简单模拟用户开户、存款、取款、转账、利息结算生成交易流水配上账户总览仪表盘。所有数据只存在浏览器里刷新页面不丢失但完全不依赖后端接口。对于想学 React 的人它能帮你把组件拆分、状态管理、路由、性能优化、持久化这些知识点从头到尾串一遍对于准备面试的人它又是一个随时可以拿出来讲透的项目。我认为它最适合两类人第一类是刚学完 React 基础、想通过综合项目巩固知识的人第二类是面经背了不少、但缺少“我真正实践过”的项目经验的人。这个项目规模不大三五天能做出来但每个环节都有值得深挖的点。1.1 模拟器里的业务比真实业务更适合学 React真实的银行系统要考虑并发、分布式事务、合规这些离前端太远但模拟器把业务压缩到前端能够表达的范围一个账户、一笔交易、一次结算本质都是状态的变化。你面对的问题不是“怎么调后端接口”而是“当多个 UI 操作都在改同一份数据时如何保证这份数据始终是对的”。我之前也做过购物车、待办清单这类常见练手项目但做完之后除了熟悉语法对 React 的理解并没有质的提升。react-bank 不一样每次状态更新都涉及多个关联模块转账改了两个账户还要追加一笔流水结息遍历了所有账户还要记录结算时间。正是这种“强制你去思考数据怎么组织”的项目才能真正打通 React 的数据流。1.2 从标题就能拆出的技术关键词只看 react-bank 这个标题就能拆出一串技术点React 组件体系、状态管理与数据一致性、路由跳转与鉴权、列表渲染优化、浏览器持久化甚至还可以延伸到 Worker 多线程和跨端框架。后面我会按自己的实践过程一步步把这些点展开。如果你正好打算做一个类似的项目可以直接照着这套思路走会比从零摸索省下很多时间。2. 数据建模先行账户、交易流水与 localStorage 持久化很多新手写 React 的第一反应是“先搭组件”但我做这个项目时最后悔的就是一开始没花足够时间设计数据模型。银行的本质是一本账账本模型设计错了后面所有组件都会跟着改。2.1 三个核心实体账户、交易、结算时间第一个版本我定义了三个数据来源账户accountid、户名、账户类型活期/定期、余额 balance、开户时间 createdAt。交易transactionid、来源账户 from、目标账户 to、金额 amount、交易类型 type存款、取款、转账、利息、时间 time、备注 note。全局元信息meta上次结息时间 lastInterestAt、模拟器运行天数 simulatedDay。其中交易是纯追加的任何业务动作都往这个数组里推一条记录账户是可变状态的集合但余额只能通过 action 修改meta 用来支撑“利息结算”这种定时任务也承担了“刷新页面后还能补算利息”的职责。为什么单独把 lastInterestAt 拿出来因为浏览器刷新之后内存状态全部丢失。如果不记录上次结息时间用户下次打开页面会重新从零开始计息体验上不像一个“模拟器”。这个字段其实是在用前端的语言模拟“服务端定时任务”的断点续跑。2.2 余额到底要不要存成字段这是我在设计时纠结最久的问题。从账务角度余额应该由流水推导初始存款减去取款、加上转入得到当前余额。这样做最安全因为永远不会出现“余额和流水对不上”的情况。但纯推导有一个性能问题每次渲染账户列表都要遍历全部交易流水做聚合。在只有几百条流水的模拟器里没问题一旦把流水撑到几万条就会有一眼能察觉的卡顿。我的选择是折中账户上存一个 balance 冗余字段但每个 reducer 分支都保证它和交易流水同步更新。同时写了一个“对账”函数用全部流水重新计算每个账户余额再与 balance 比较不一致就在页面顶部报红。这个对账函数看起来简单却是整个模拟器可靠性的压舱石。每次我改完一个 action 的细节都会跑一遍对账确认没有改出数据不一致。它也让这个“虚拟银行”有了银行系统的味道。2.3 用 localStorage 当数据库但要避开 StrictMode 的坑纯前端没有后端数据自然落在 localStorage。我的方案是启动时从 localStorage 读一份初始 state没有就用默认种子数据每次 state 变化后用 useEffect 把最新 state 写入 localStorage。这里有一个很容易踩的坑——不要在 reducer 内部写 localStorage因为 reducer 必须保持纯函数而且 React 18 开发模式下的 StrictMode 会让 reducer 调用两次。如果第一次调用写了存储第二次调用又写一遍虽然结果相同但这已经让 reducer 产生了副作用属于潜在的设计错误。我最终的写法是所有副作用持久化、提示音、消息提示都放在 useEffect 里监听 state 变化reducer 只负责同步计算新状态。这套约定在项目小的时候看不出优势但面试时能讲清楚是很大的加分项。3. 状态管理选型useReducer Context 如何撑起整个模拟器确定数据模型之后接下来要决定用什么方案管理状态。这里我先排查了社区常见的选项useState 分散管理、useReducer Context、Redux Toolkit、Zustand。3.1 为什么我放弃了 Redux不是说 Redux 不好而是 react-bank 的体量配不上它的复杂度。Redux 的价值在于大型应用里跨模块共享状态、中间件处理异步流程、Redux DevTools 调试复杂更新。但我的模拟器只有一个“账本”领域所有状态变更都能汇聚到 reducer 里使用 useReducer 配合 Context 就能做到同样的效果还少装一堆依赖。做个简单的对比你就知道我的选型逻辑方案样板代码调试体验适合场景useState Props 透传少一般组件树很浅、共享状态少useReducer Context中好可以打印 action 日志单体业务、中等规模如 react-bankRedux Toolkit多最好时间旅行调试多模块大型应用、复杂异步流如果你只是做一个模拟器useReducer Context 的调试体验已经足够。Redux DevTools 的时间旅行确实诱人但我在第六部分会讲其实自己写一个 action 日志面板也能获得类似的体验。3.2 转账 action 的完整设计思路选型定了接下来的重点是 action 怎么设计。这里以最核心的转账为例function bankReducer(state, action) { switch (action.type) { case TRANSFER: { const { fromId, toId, amount } action.payload; const from state.accounts.find((a) a.id fromId); const to state.accounts.find((a) a.id toId); if (!from || !to) return state; if (from.balance amount) { return { ...state, error: 余额不足转账失败 }; } return { ...state, error: null, accounts: state.accounts.map((acc) { if (acc.id fromId) return { ...acc, balance: acc.balance - amount }; if (acc.id toId) return { ...acc, balance: acc.balance amount }; return acc; }), transactions: [ { id: crypto.randomUUID(), fromId, toId, amount, type: TRANSFER, time: Date.now(), }, ...state.transactions, ], }; } default: return state; } }这里有三个关键点。第一数据校验分成两层表单层的校验金额不能为负、不能转给自己放在 dispatch 之前由 action creator 完成余额是否充足这类依赖当前状态的校验放在 reducer 里完成。第二一次 dispatch 同时更新两个账户和交易流水天然具备“原子性”。这比在组件里连续 setState 安全得多也是 useReducer 适合这个项目的原因。第三不能在 reducer 里生成 id 和时间戳因为 StrictMode 下 reducer 可能被调用两次会产生两个不同的 id 或时间戳我把 id 生成放在 action creator 里dispatch 之前就定好。3.3 React Router 的声明式写法和 Vue Router 的差异点模拟器有三个页面登录、账户总览、转账与流水。路由我选了 React Router。React Router 用 JSX 声明路由表组件即配置Vue Router 则是传统的配置对象加 router-view。换到业务里这个差异很直观Routes Route path/login element{Login /} / Route path/ element{ RequireAuth Dashboard / /RequireAuth } / /RoutesVue Router 有全局前置守卫React Router 没有直接等价物但你可以封装一个 RequireAuth 组件完成同样的工作没有登录就跳转到 /login否则渲染子页面。React 的哲学是“一切皆组件”路由守卫本质上也是一个组件。如果你之前主要写 Vue换到 React 时接受这个理念会顺畅很多。4. 核心功能实现转账、利息模拟与仪表盘数据模型和状态管理定下来之后功能实现就变成一件很确定的事。我按“账户生命周期”的顺序来做功能开户、存款、取款、转账、结息。4.1 转账表单校验、反馈与精细订阅转账页面我分了左右两块左侧是表单右侧是最近流水。表单提交时先做一次本地校验再进入 reducer。之前我把所有校验都堆在 reducer 里结果用户输入非法金额时页面连个像样的提示都没有只能靠全局 error 字段判断。后来我把“用户输入类”校验前置reducer 只保留“业务规则类”校验体验好了很多。还有一个小细节当转账失败时我返回了{ ...state, error: 余额不足 }这会让 Context 的 value 变成新对象所有订阅方都会重新渲染。为了不把整页都刷一遍我在组件里用 useMemo 只让提示组件订阅 error 字段仪表盘这类与错误无关的组件就不会被带动渲染。这个优化很小但体现了 React 性能优化“从精细订阅开始”的思路。4.2 用 setInterval 模拟结息以及补算机制利息是最能体现“模拟器”气质的功能。我的设定是每个账户按日利率 0.01% 结算每 10 秒模拟一天。实现上useEffect 里启动一个 setInterval每次触发 dispatch{ type: INTEREST }reducer 遍历所有账户把余额乘以利率再加回去并追加一条“利息”类型流水。这里容易犯的错是在 setInterval 回调里读取旧的 state。因为闭包会捕获创建时的值如果你在回调里直接引用外部变量会拿到“过期”的余额。正确做法是什么都别在外面读dispatch 之后让 reducer 拿最新的 state 算——reducer 接收的 state 永远是当前最新值。另外为了避免刷新丢失累计天数我把lastInterestAt和simulatedDay都存进 localStorage。页面启动时先检查距上次结息过了多少模拟天数一次性补算。这个逻辑让项目更像一个“可持续运行”的模拟器而不是刷新就重来的 Demo。4.3 仪表盘派生数据与图表组件账户总览页是整个模拟器的门面。我放了一张资产趋势折线图、几个统计卡片总资产、账户数、今日交易笔数以及最近几笔交易。图表用的是 ECharts数据来源全部是交易流水通过 useMemo 按天聚合const dailyAssets useMemo( () buildDailyAssets(state.transactions, state.accounts), [state.transactions, state.accounts] );这里 useMemo 的意义在于state 里任何字段变化都会让组件函数重新执行但只有 transactions 或 accounts 变化时聚合计算才需要重跑。如果交易流水上万条这个 memo 能省下不少无用的大数组遍历。这也是我在面试里常被问的“useMemo 什么时候用”的答案不是所有计算都需要 memo只有当计算成本高、且依赖不常变化时才值得用。5. 踩坑实录React 18 批处理、长列表、Worker 和 StrictMode做一个项目踩坑的价值往往比功能实现更大。下面这几个坑是我在 react-bank 里真实遇到并解决的顺序大致按照影响从低到高排列。5.1 React 18 的自动批处理帮我提了性能也埋了一个隐蔽的坑React 18 之前setState 的批处理只发生在事件处理函数里Promise 回调、setTimeout 里多次 setState每次都会触发一次渲染。React 18 开始所有场景都自动批处理多个 setState 会被合并为一次渲染。我用这个特性优化了“批量结息”的体验——如果循环里逐个更新账户React 18 也能把它们合成一次提交页面不会频繁闪烁。但自动批处理也有代价。假设你在一个异步函数里写setBalance(balance - amount); setError(null);如果后续操作依赖第一次 setState 的结果由于批处理你读到 balance 可能还是旧值。React 18 里正确写法是函数式更新setBalance((prev) prev - amount)这样无论批处理怎么合并都能基于最新值计算。这个坑在真实项目中非常常见也是 React 面试的高频考点。5.2 交易流水上万条之后列表开始卡顿了流水是只增不改的所以它是天然的长列表。我一开始直接把 transactions 数组.map()成表格流水 500 条时没问题到 5000 条就明显卡。这里我测试过三种方案用 react-window 做列表虚拟化效果好但引入依赖和额外配置。只渲染最近 100 条提供按月份筛选实现最简单交互也自然。对所有流水做 React.memo收益有限因为数据一更新所有行都会收到新的引用。我最终选了“最近 100 条 月份筛选”方案。原因很简单模拟器的用户不会关心半年前的每一条流水他们需要的是最近记录和筛选能力。这个取舍也说明性能优化不是技术越新越好而是先想清楚用户场景再做 Trade-off。5.3 用 Web Worker 跑对账但不要在数据量小时过度设计当我故意把流水压到 5 万条做压力测试时主线程上的对账计算会导致页面卡顿。这时候我引入了 Web Worker把 account 和 transaction 的副本通过 postMessage 传给 WorkerWorker 算完所有账户余额后把结果传回主线程比对。页面全程保持流畅。但我要提醒你Worker 不是万能的。postMessage 传输大量对象需要序列化数据量大到一定程度时通信成本会超过计算本身。react-bank 在几百条流水的规模下根本不需要 Worker我是在做压力测试时才加的。这个经验的通用价值是先本文还有配套的精品资源点击获取