news 2026/9/16 16:33:36

cu/curriculum 首个 React 实战:构建可编辑 CV/简历生成器的状态与组件设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
cu/curriculum 首个 React 实战:构建可编辑 CV/简历生成器的状态与组件设计

cu/curriculum 首个 React 实战:构建可编辑 CV/简历生成器的状态与组件设计

【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum

导读

本篇文章围绕 cu/curriculum(The Odin Project 开放前端课程)中 React 学习路径的第一个综合项目展开,目标是从零搭建一个用户可输入个人信息并生成 CV/简历的小型应用。通过完成这个项目,你将把此前学过的组件、props、state 与表单处理知识串联成一条完整实战链路,并掌握将 React 应用部署到 Netlify、Vercel、Cloudflare Pages 等 PaaS 平台的标准化流程。

项目定位:为什么用 CV 应用作为第一个 React 项目

在正式进入本项目的任务说明前,先明确它的教学定位。该文档位于 react/states_and_effects 目录下,紧跟在 introduction_to_state.md、more_on_state.md 与 how_to_deal_with_side_effects.md 之后,是"状态与副作用"这一章节的收口练习。

原文档在 Introduction 中明确指出:这是你的第一个 React 项目,虽然体积不大,却涵盖了此前学过的众多基础概念(组件拆分、状态管理、props 传递、表单交互),同时不会带来过高的复杂度负担。这类"小而全"的练习项目恰好适合验证你是否真正理解了 React 的核心心智模型——尤其是"状态驱动 UI"和"单向数据流"这两条主线。

任务总览:三大信息区块 + 编辑/提交闭环

项目要求构建的应用包含以下三块信息输入区域:

  1. 基本信息(General information):姓名、邮箱、电话号码;
  2. 教育经历(Educational experience):学校名称、所学专业/学位名称、学习日期;
  3. 工作经历(Practical experience):公司名称、职位名称、主要职责、入职与离职日期。

每个区块(或整个 CV)都必须配备Submit(提交)Edit(编辑)两个按钮:

  • 点击Submit后,表单提交并把输入字段的值渲染为 HTML 元素(即从"编辑态"切换到"展示态");
  • 点击Edit后,输入字段重新出现,且以之前提交的内容作为各字段的默认值,用户可直接修改并再次提交。

这一"编辑态 ⇄ 展示态"的切换,正是对 more_on_state.md 中受控组件(Controlled Components)与状态结构设计的一次完整实操——你需要在 state 中记录"当前处于哪种模式"以及"已提交的数据快照",再根据模式条件渲染表单或展示视图。

动手第一步:用 Vite 创建 React 项目

课程要求首先创建一个新的 React 项目。当前课程体系已全面转向 Vite 工具链(setting_up_a_react_environment.md 中明确指出 Create React App 已于 2023 年初弃用,不再推荐用于新项目),因此使用 Vite 的 React 模板脚手架即可:

npm create vite@latest cv-project -- --template react

命令执行期间若提示安装create-vite包,输入y确认;随后进入项目目录安装依赖并启动开发服务器:

cd cv-project npm install npm run dev

启动成功后终端会输出类似➜ Local: http://localhost:5173/的信息,浏览器访问localhost:5173即可看到模板首页。开发服务器可通过Ctrl+C退出,之后在项目目录内随时用npm run dev重新启动。

如果已经在 GitHub 创建了空仓库并克隆到本地,也可以直接在克隆目录中执行:

npm create vite@latest . -- --template react

让 Vite 使用当前目录作为项目根目录,免去二次关联 remote 的步骤。

提前认识 StrictMode 的"双重渲染"现象

项目任务文档特别以警示框提醒了一个新手很容易被吓到、但属于预期行为的现象:项目运行后,你会发现部分代码执行了两次(例如console.log输出成双份)。

这是由main.jsx中包裹应用的React.StrictMode引起的。脚手架生成的入口代码(可参考 setting_up_a_react_environment.md 中的main.jsx示例)结构如下:

import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import App from "./App.jsx"; import "./index.css"; createRoot(document.getElementById("root")).render( <StrictMode> <App /> </StrictMode>, );

StrictMode仅在开发环境下生效,它会刻意让组件经历"挂载 → 卸载 → 再挂载"的流程,从而提前暴露副作用(side effect)相关的隐患,例如 how_to_deal_with_side_effects.md 中Clock组件未清理setInterval导致的计数器加倍问题。在本项目中你不需要处理它,只需理解:这不是 bug,而是 React 帮你做的一次开发期体检

组件结构设计:把 UI 拆成可复用的块

任务明确要求先思考如何把应用拆分为组件,并给出了两个目录规范:

  1. src下创建components目录,把所有组件放进去;
  2. src下创建styles目录存放 CSS 文件,并在组件文件中通过import引入对应样式。

结合 react_components.md 中的组件思想,一个合理的拆分方案如下:

src/ ├── main.jsx # 入口,渲染 App ├── App.jsx # 顶层组件,持有整个 CV 的状态 ├── components/ │ ├── GeneralInfo.jsx # 基本信息区块 │ ├── Education.jsx # 教育经历区块 │ ├── PracticalExperience.jsx # 工作经历区块 │ └── ... └── styles/ ├── App.css ├── GeneralInfo.css └── ...

关于组件命名有两个必须记住的规则(出自 react_components.md):

  • 组件函数名必须大写,如GeneralInfo。JSX 解析时靠大写区分"HTML 标签"与"React 组件实例"——写成<generalInfo />会被当作普通 HTML 元素而失效;
  • 每个组件通过export default导出,父组件用import引入后即可作为子组件渲染。

状态设计:谁来持有数据?如何切换编辑/展示模式

这是本项目最核心的练习点。任务文档强调"你要大量使用 state 和 props",因此需要想清楚两个问题:

1. 状态放在哪里:提升状态(Lifting State Up)

按照 how_to_deal_with_side_effects.md 中"提升状态"的原则,React 的数据流是单向的(父 → 子,见 passing_data_between_components.md)。当多个子组件共享同一份 CV 数据、或父组件需要汇总所有区块数据时,正确的做法是把状态提升到共同的父组件(如App)中,再通过 props 下发给子组件。

2. 每个区块的"编辑态/展示态"如何建模

一个直观的做法是:每个区块组件内部维护两个核心状态——表单字段的当前值,以及"是否处于编辑模式"的布尔值。以基本信息为例:

import { useState } from "react"; function GeneralInfo() { const [isEditing, setIsEditing] = useState(true); const [formData, setFormData] = useState({ name: "", email: "", phone: "", }); const handleChange = (event) => { const { name, value } = event.target; setFormData({ ...formData, [name]: value }); }; const handleSubmit = (event) => { event.preventDefault(); setIsEditing(false); }; const handleEdit = () => { setIsEditing(true); }; if (isEditing) { return ( <form onSubmit={handleSubmit}> <label> 姓名 <input name="name" value={formData.name} onChange={handleChange} /> </label> <label> 邮箱 <input name="email" value={formData.email} onChange={handleChange} /> </label> <label> 电话 <input name="phone" value={formData.phone} onChange={handleChange} /> </label> <button type="submit">Submit</button> </form> ); } return ( <div> <h2>{formData.name}</h2> <p>邮箱:{formData.email}</p> <p>电话:{formData.phone}</p> <button onClick={handleEdit}>Edit</button> </div> ); } export default GeneralInfo;

这段代码同时体现了 more_on_state.md 中的三个关键要点:

  • 受控组件inputvalue直接绑定 state,onChange时通过setFormData更新——输入框不再自己"记住"内容,React 始终掌握最新值,这正是"Edit 后能回显旧数据"的基础;
  • State 不可变更新:更新对象时用展开运算符创建新对象{ ...formData, [name]: value },而不是直接修改formData的属性。因为setStateObject.is()判断前后状态,只有传入新对象才能可靠触发重渲染;
  • 状态驱动条件渲染:通过isEditing布尔值在两个 JSX 视图间切换,这是 React 声明式 UI 的典型写法。

3. 教育/工作经历的"多条记录"问题

教育经历与工作经历通常不止一条。此时可以结合 rendering_techniques.md 的列表渲染方式,把经历存成对象数组,用map渲染,并为每条记录提供唯一的key

const [experiences, setExperiences] = useState([ { id: 1, company: "", position: "", tasks: "", from: "", to: "" }, ]);

新增一条记录时调用setExperiences([...experiences, newExperience]),删除时用filter生成新数组——始终以新数组调用 setter,绝不原地push

实战进阶:Edit 回显与多次 setState 的陷阱

任务要求"Edit 按钮要把之前展示的信息作为输入字段的值",这正是上面受控组件方案自动满足的:因为value始终来自 state,切回编辑态时输入框天然显示已提交的数据。

但要特别注意 more_on_state.md 中演示的陷阱:state 更新是异步的,且"每次渲染都是一次快照"。在同一个事件处理器里连续调用两次 setter 并不会叠加:

// 不会把年龄 +2! setPerson({ ...person, age: person.age + 1 }); setPerson({ ...person, age: person.age + 1 });

因为两次调用都基于本次渲染的旧快照。如果确实要基于最新状态连续更新,应传入函数式更新(updater):

setPerson((prev) => ({ ...prev, age: prev.age + 1 })); setPerson((prev) => ({ ...prev, age: prev.age + 1 }));

本项目表单字段每次提交都是独立事件,通常用直接传值即可;但当你实现"新增工作经历"这类依赖上一次数组内容的操作时,函数式更新是更稳妥的选择。React 还会在可能的情况下批量合并同一次事件中的多次 setState 调用,只触发一次重渲染。

此外还有一个容易踩的坑:不要在组件函数体内直接调用 setter(如下面的写法会导致无限重渲染,React 最终会抛出 "too many re-renders" 错误):

function BadComponent() { const [count, setCount] = useState(0); setCount(count + 1); // 渲染中修改状态 → 无限循环 return <h1>{count}</h1>; }

所有的状态修改都应发生在事件处理器、useEffect回调或用户交互触发的函数里。

部署上线:三大 PaaS 平台实操

完成应用开发后,任务要求把代码推送到 GitHub 并部署到公网。原文档特别解释了为什么这一步从 GitHub Pages 切换到 PaaS:此前部署的都是静态网页,继续用 GitHub Pages 虽可行但较为繁琐;而 PaaS 平台通常在 push 时自动触发部署,无需手动处理构建步骤,也为后续课程中的路由、构建配置等问题提前扫清障碍。三个推荐平台的部署步骤对比如下:

Netlify

  1. 把 React 应用推送到 GitHub 仓库;
  2. 登录 Netlify,通过"导入项目"入口选择你的 GitHub 仓库;
  3. 选择要部署的分支(默认的main即可),点击Deploy site
  4. 如有需要,可在项目设置中继续调整构建命令、输出目录等高级配置。

Vercel

  1. 同样先把 React 应用推送到 GitHub;
  2. 进入 Vercel 的"新建项目"页面,导入你的 GitHub 仓库;
  3. Vercel 会自动检测你使用的是 Vite 并生成相应配置,设置项目名称后点击Deploy即可完成部署。

Cloudflare Pages

  1. 先把应用推送到 GitHub;
  2. 在 Cloudflare 中创建新应用时务必选择Pages(而不是 Worker),并导入 GitHub 仓库,按照框架部署指南设置正确的构建命令输出目录(Vite 项目默认构建命令为npm run build,输出目录为dist);
  3. 点击Save and Deploy,等待构建完成即可看到线上站点。

原文档也提到,以上每个平台都还提供其他部署方式(例如 Netlify 可直接上传dist目录或使用netlify-cli),但对本阶段的学习而言,导入 GitHub 仓库是最省心、最能享受 deploy-on-push 便利的路径。课程对平台选择不做硬性要求——只要应用能成功上线到互联网,就达到了练习目标。

收尾自查清单

在推送并部署前,对照以下问题检查自己的实现:

  • 三个信息区块(基本信息、教育经历、工作经历)是否都能输入并提交?
  • 提交后是否以 HTML 元素展示数据,而非继续停留在表单状态?
  • 点击 Edit 后,输入框是否回显了此前提交的内容,且可修改并重新提交?
  • 所有组件是否都在src/components目录下,CSS 是否放在src/styles目录并被正确 import?
  • 是否理解了代码中每处useStateprops的职责,而不是照抄示例?
  • 代码是否已推送到 GitHub 并通过上述任一平台成功部署?

本项目的完整要求原文可随时回到 project_cv_application.md 复核;如果对状态、props 或副作用仍有疑问,对应的三篇前置课程 introduction_to_state.md、more_on_state.md、how_to_deal_with_side_effects.md 以及组件基础 react_components.md 都值得反复研读。完成并部署这个项目,意味着你已经具备了用 React 独立构建交互式应用的基本能力。

【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 16:33:03

FiftyOne 插件生态系统完全指南:从发现、安装到开发与共享

FiftyOne 插件生态系统完全指南&#xff1a;从发现、安装到开发与共享 【免费下载链接】fiftyone Refine high-quality datasets and visual AI models 项目地址: https://gitcode.com/GitHub_Trending/fi/fiftyone FiftyOne 插件&#xff08;Plugins&#xff09;是扩展…

作者头像 李华
网站建设 2026/9/16 16:32:34

WPF DataGrid仿Excel筛选:ICollectionView.Filter实战指南

简介&#xff1a;本资源是一个基于WPF与.NET 6.0实现DataGrid仿Excel筛选功能的完整可运行实例&#xff0c;面向WPF桌面应用开发者及中高级C#学习者&#xff0c;解决数据表格交互中缺乏原生多条件筛选能力的痛点&#xff0c;适用于需高效处理结构化数据的管理类、报表类桌面软件…

作者头像 李华
网站建设 2026/9/16 16:30:39

使用wechatapi进行微信二次开发,微信api

默认模块 wechatapi.net Base URLs: Authentication 开发API/登录模块 POST (步骤1)获取登录二维码 POST /login/getLoginQrCode appId参数为设备ID&#xff0c;首次登录传空&#xff0c;会自动触发创建设备&#xff0c;掉线后重新登录则必须传接口返回的appId&#xff0…

作者头像 李华
网站建设 2026/9/16 16:30:33

JavaWeb金融借贷系统源码解析:Servlet+JDBC全流程实战

简介&#xff1a;这套基于JavaWeb实现的金融借贷系统&#xff08;即P2P金融管理系统/小额贷款平台&#xff09;主要面向计算机相关专业毕业设计学生与Java项目实战开发者&#xff0c;前台提供融资产品浏览、贷款申请、每日新闻查询&#xff0c;后台涵盖贷款申请审核、融资产品与…

作者头像 李华