5.2 项目开发实战 本节摘要:本章是整套教程的收尾演练——把前四章的工具和第五章的架构,组装成一个能上线的真实项目。以一个「任务管理应用」为例子,走完整条链路:需求拆解、技术选型、项目搭建、组件规划、数据与状态、编码规范、测试策略、性能优化与部署。每一步都有可执行的做法,让你看完就能照着做。 本节导读 阅读完本节,你应当能够: 把一个模糊需求拆解成功能模块与页面清单 依据项目约束完成技术选型 从零搭建项目并规划组件与目录 实现数据获取、状态管理、表单与路由 配置代码规范、测试与部署,交付可上线应用 一、问题与直觉:从「会写组件」到「交付一个项目」 前面所有章节都在教「能力」,本节把它们串成「流程」。很多人学完 React 每个 API 都懂,但真被丢进一个项目时不知道怎么开始。
本节摘要:本章是整套教程的收尾演练——把前四章的工具和第五章的架构,组装成一个能上线的真实项目。以一个「任务管理应用」为例子,走完整条链路:需求拆解、技术选型、项目搭建、组件规划、数据与状态、编码规范、测试策略、性能优化与部署。每一步都有可执行的做法,让你看完就能照着做。
阅读完本节,你应当能够:
前面所有章节都在教「能力」,本节把它们串成「流程」。很多人学完 React 每个 API 都懂,但真被丢进一个项目时不知道怎么开始。缺的不是知识,是「从需求到上线的操作顺序」。
本节用一个任务管理应用(Todo 应用的工程化版本)走完整条链路。它麻雀虽小但五脏俱全——有列表、有表单、有筛选、有路由、有状态、有接口调用,几乎覆盖了前四章的所有知识点。
拿到一个模糊需求「做一个任务管理工具」,第一件事不是写代码,是拆需求。把「模糊的一句话」拆成「明确的功能点」:
用户视角的功能清单:
先不纠结每个功能的技术实现——「筛选」可能用 state 也可能用路由参数,那是实现细节,需求层面只要确定「用户能按状态筛选」这个事实。
拆成页面与模块:
| 页面/模块 | 核心功能 |
|---|---|
| 登录页 | 登录、注册切换 |
| 任务列表页 | 列表、筛选、分页(如需要) |
| 任务编辑 | 新建、编辑共用的表单 |
| 布局 | 顶部导航、主内容区 |
需求拆解的原则:先列「用户能做什么」,再定「界面有什么」。功能点来自用户视角,页面结构从功能点推出来。别一上来就想「我要用什么组件库、什么状态库」——选型是第二步的事。
拆完需求,产出三样东西,它们直接指导后续开发:
用户故事清单。每条一句话:「作为用户,我想……以便……」这种格式把「为什么做」也记下来——需求变动时能追溯到动机。
页面路由表。每个页面对应一个 URL:/login、/tasks、/tasks/:id。路由表定了,页面结构就有了骨架。
数据模型草稿。任务对象长什么样:id、title、description、dueDate、done、createdAt。数据模型草稿让前后端联调有共同语言,也让状态设计有依据。
这三样产物加起来不到一页纸,却能把「模糊需求」变成「可执行方案」。跳过它们直接写代码,写一半才发现理解偏了的成本,比写这三样高得多。需求阶段多花十分钟,实现阶段省下的是几小时的返工。
按第 3、4 章的选型框架,为这个项目定工具链:
| 领域 | 选择 | 理由 |
|---|---|---|
| 脚手架 | Vite | 冷启动快、配置透明 |
| 语言 | JavaScript | 项目规模小,暂不需要 TS(规模大再上) |
| 路由 | React Router | 有登录页/列表页,多页面需要 |
| 状态 | Context + useState | 登录态全局低频,Context 够用 |
| 数据请求 | fetch 封装 | 接口少,不需要引 axios/请求库 |
| UI 组件 | 手写基础组件 | 组件需求简单,引库反而重 |
| 样式 | CSS Modules | 团队熟悉传统 CSS,组件少 |
| 测试 | Vitest + Testing Library | Vite 生态,API 兼容 Jest |
注意选型逻辑:每个选择都是「够用就好」。这个项目的复杂度撑不起 Redux + antd + axios 全家桶,引了是负担。选型跟着需求走,不是跟着「主流」走——这套逻辑在前四章反复出现,这里是最完整的落地。
按 5.1 节的目录思路建项目:
src/ api/ # 请求层 tasks.js auth.js components/ # 通用组件 Button/ TaskItem/ Modal/ features/ # 业务模块 auth/ LoginPage.jsx tasks/ TaskListPage.jsx TaskForm.jsx useTasks.js store/ # 全局状态(登录态) authContext.jsx utils/ # 工具 date.js styles/ global.css App.jsx main.jsx
这个目录严格对应 5.1 节的架构:api 是数据层,useTasks 是逻辑层,features/components 是 UI 层。目录是架构的物理投影——架构怎么想,目录就怎么摆,两者保持一致,团队才不会迷路。
搭建的实操顺序:Vite 建项目 → 装依赖(react-router-dom)→ 建目录 → 配路径别名与代理 → 写全局样式 → 写路由骨架。先把「骨架」立起来,再往里面填内容。
数据请求统一走封装的请求层(第 4.5 节的模式),业务代码不碰 fetch 细节:
// api/tasks.js(示意) const request = async (url, options = {}) => { const res = await fetch(`/api${url}`, { headers: { 'Content-Type': 'application/json' }, ...options, }); if (!res.ok) throw new Error(`请求失败:${res.status}`); return res.json(); }; export const fetchTasks = () => request('/tasks'); export const createTask = (data) => request('/tasks', { method: 'POST', body: JSON.stringify(data), }); export const updateTask = (id, data) => request(`/tasks/${id}`, { method: 'PUT', body: JSON.stringify(data), }); export const deleteTask = (id) => request(`/tasks/${id}`, { method: 'DELETE' });
请求层把「URL、方法、错误处理」收进一个文件。接口一变,只改这里;加统一错误处理,也改这里。第 4.5 节讲 axios 拦截器时说过同样的话——封装的意义是「业务代码不依赖请求细节」。
第 5.1 节的状态分配原则在这个项目里落地:
每个状态都有明确归属,没有「谁都能改」的全局大对象。任务列表不做成全局状态是有意的——它只在列表页用,放全局只会让其他页面无谓重渲染。这个「状态跟着使用范围走」的判断,是 5.1 节状态分配原则的日常化。
登录态用 Context 管理(第 2.5 节模板),未登录访问受保护页重定向:
function RequireAuth({ children }) { const { user } = useAuth(); if (!user) return <Navigate to="/login" replace />; return children; } // 路由配置 <Routes> <Route path="/login" element={<LoginPage />} /> <Route path="/" element={ <RequireAuth> <TaskListPage /> </RequireAuth> } /> </Routes>
登录页处理「登录成功跳转、失败回显错误」两件事——成功 navigate('/'),失败把服务端错误显示在表单下方。第 3.4 节的服务端错误回显模式在这里直接复用。
数据获取逻辑抽进自定义 Hook(第 5.1 节的三层分离):
function useTasks() { const [tasks, setTasks] = useState([]); const [status, setStatus] = useState('idle'); const [error, setError] = useState(null); const load = useCallback(async () => { setStatus('loading'); try { const data = await fetchTasks(); setTasks(data); setStatus('done'); } catch (e) { setError(e); setStatus('error'); } }, []); useEffect(() => { load(); }, [load]); return { tasks, status, error, reload: load }; }
组件层只管渲染,数据逻辑都在 Hook 里,可测可复用。useTasks 是 5.1 节「逻辑层落地为自定义 Hook」的直接体现——列表页调用它拿数据,测试也直接测它,UI 与逻辑互不纠缠。
表单用受控组件(第 3.4 节),提交走封装好的 API:
function TaskForm({ onCreated }) { const [title, setTitle] = useState(''); const [error, setError] = useState(''); const handleSubmit = async (e) => { e.preventDefault(); if (!title.trim()) { setError('标题不能为空'); return; } await createTask({ title }); onCreated(); }; // 渲染表单... }
这十几行里浓缩了第 3.4 节的所有要点:preventDefault、必填校验、提交后回调。新建和编辑复用同一个 TaskForm,编辑时用初始值填表——组件只负责「收集与提交」,数据从哪来由调用方决定。
列表用 map + key(第 2.4 节),筛选用「先算后渲染」——这个模式在第 2.4 节的过滤列表里出现过,这里把它接到真实数据上:
function TaskList({ tasks }) { const [filter, setFilter] = useState('all'); const visible = tasks.filter(t => filter === 'all' ? true : filter === 'done' ? t.done : !t.done ); return ( <div> <div className="filters"> {['all', 'done', 'active'].map(f => ( <button key={f} onClick={() => setFilter(f)}>{f}</button> ))} </div> <ul> {visible.map(t => <TaskItem key={t.id} task={t} />)} </ul> </div> ); }
筛选按钮用 map 生成,key 用筛选项本身(三个枚举值天然唯一)。visible 是派生数据,不存 state,每次渲染现算——这就是第 2.4 节「派生状态勿存」的落地。选中态按钮的高亮可以再配个 className 条件,用第 4.5 节 classnames 的写法。
删除操作配 Modal 确认(第 3.5 节的组合模式)——Modal 是通用容器,删除按钮管打开关闭与确认回调,组合出「带确认的删除」:
function DeleteButton({ onDelete }) { const [confirmOpen, setConfirmOpen] = useState(false); return ( <> <button onClick={() => setConfirmOpen(true)}>删除</button> <Modal open={confirmOpen} onClose={() => setConfirmOpen(false)} footer={<button onClick={onDelete}>确认删除</button>}> <p>确定要删除这个任务吗?此操作不可恢复。</p> </Modal> </> ); }
ESLint + Prettier 配置好,配合 husky 在提交前自动检查。规范的核心是「提交即规范」——靠工具拦截,不靠自觉。团队的规范不一致,一半是「没人定」,另一半是「定了没人执行」,husky 解决的就是后一半。
按第 3.6 节的性价比原则:核心逻辑(useTasks 的加载流程、筛选逻辑)写单元测试,关键交互(登录、新建任务)写组件测试,不写全量 E2E。这样配置下来,测试套件跑得快、维护量可控,同时把「最容易坏的核心逻辑」兜住了。
// useTasks 的加载测试(示意) test('加载任务成功后设置状态', async () => { // mock fetchTasks 返回数据 // 渲染调用 useTasks 的测试组件 // 断言 status 变为 done、tasks 有数据 });
写这类测试时坚持第 3.6 节的原则:断言「界面可见的行为」,别断言内部实现。测试的作用是让后续改代码有安全网,不是给实现细节上锁。
第 3.1 节的纪律:先测量后优化。这个规模的项目的性能优化重点是:路由级代码分割(React.lazy)、图片懒加载、列表渲染优化(长列表才需要虚拟滚动)。不预先堆优化,按需做——等 Profiler 指出问题再动手,比盲目套 memo 有效得多。
构建产物部署:Vite 构建输出 dist,部署到静态托管平台(或公司内部服务器)。配置好环境变量(API 地址),后端接口上线后联调。部署是「项目真正交付」的最后一公里,别忽略。
部署还有几件容易被忽略的事:构建前跑一遍测试,防止把坏代码带上线;检查环境变量——生产环境用生产 API 地址,别把开发地址带到线上;看构建产物体积——首屏 bundle 太大就按第 3.1 节做代码分割。上线后盯着监控和日志——前端错误不记录等于没发生,这是第 3.8 节反复强调的。一次完整的发布,到「线上数据正常」才算结束,不是「代码推上去」就完了。
真实项目里一定会遇到问题,别慌,按优先级排查:
先看浏览器控制台报错(渲染错误、网络错误都在这暴露),再看网络面板(请求是否发出、状态码多少、返回了什么),然后看组件状态(数据流到哪一步断了)。三步走完,大部分问题都能定位。定位不了再上调试工具——React DevTools 看组件树与 props,断点跟进逻辑。排查问题的顺序比技巧重要:控制台 → 网络 → 状态 → 工具,一层层逼近根因。
走完这条链路,你拥有的是一套「从需求到上线」的完整方法。第 1-4 章的能力在这里变成产出,5.1 节的架构在这里变成代码。以后遇到任何 React 项目,你都可以按同样的顺序推进:拆需求 → 选型 → 搭建 → 实现 → 规范 → 测试 → 部署。这套方法不依赖具体工具——换一套框架、换一个库,流程骨架不变,这正是它比单个 API 更有价值的地方。现在,去写你的第一个真实项目吧。
最后一句送给你:React 的一切学习,都是为了「亲手把想法变成可用的界面」。 别停在读完,打开编辑器,把一个最小的任务应用敲出来——从第一行 Vite 命令开始,你已经在路上了。这个教程到此结束,但它教你的方法会在下一个项目里继续生长。