4.6 为什么需要前端框架:React 在替你做什么
一、本节你会做出什么
你会看懂 page.tsx 里那些“长得像 HTML 又不是 HTML”的东西,并亲手把一段重复三遍的代码变成一个组件。
📷 效果位:改造前(重复三段)vs 改造后(一个组件 + 三行调用)
这一节解释一个你可能一直在疑惑的事:4.2 学的 HTML 明明挺好,为什么真实项目里不这么写?
二、开始前你需要
⚠️ 4.4 的 map 如果没亲手跑过,建议回去跑一遍。 本节第 6 步直接建立在它上面。
三、跟着做
第 1 步:先体会没有框架的痛
假设「遇见成都」要显示 6 个景点卡片。用 4.2 的纯 HTML 写,是这样:
<div class="card"> <img src="/panda.jpg" alt="大熊猫基地"> <h3>大熊猫繁育研究基地</h3> <p>¥55 · 3 小时</p></div>
<div class="card"> <img src="/kuanzhai.jpg" alt="宽窄巷子"> <h3>宽窄巷子</h3> <p>免费 · 2 小时</p></div>
<!-- ... 再复制粘贴 4 遍 ... -->现在来三个真实需求,感受一下疼在哪:
| 需求 | 纯 HTML 的代价 |
|---|---|
| 「卡片加个圆角」 | 改 6 个地方(漏一个就不一致) |
| 「每张卡片加一个“收藏”按钮」 | 改 6 个地方 |
| 「景点改成 20 个」 | 复制粘贴 20 遍,而且数据和样子混在一起 |
还有更难的。如果数据是从服务器来的呢?
页面加载时你并不知道有几个景点。你得用 4.4 的 JS 手动做这些事:
// 没有框架时,大致要这么干const container = document.querySelector(".cards");container.innerHTML = ""; // 先清空attractions.forEach(item => { const div = document.createElement("div"); // 一个个造元素 div.className = "card"; div.innerHTML = `<h3>${item.name}</h3>...`; // 手动拼字符串 container.appendChild(div); // 一个个塞进去});数据一变,这一整套重新来一遍。页面上有十个这样的区域,你就要维护十套这种代码,而且**很容易出现“数据改了但页面没更新”**的 bug。
🔑 这就是框架要解决的问题。 不是为了炫技,是为了让上面这些事不用你操心。
第 2 步:React 替你做的三件事
| React 做什么 | 换成大白话 |
|---|---|
| ① 数据驱动界面 | 你只管改数据,页面自己跟着变。 不用再手动找元素、改元素 |
| ② 组件化 | 一段界面可以打包成一个“自定义标签”,到处复用,改一处全都变 |
| ③ 高效更新 | 数据变了,它自己算出“页面上哪一小块需要变”,只改那一块 |
第 ① 条是最核心的思想转变:
没有框架: 数据变了 → 你去找到元素 → 你手动改元素React: 数据变了 → 页面自动变(你什么都不用做)💡 一个类比: 没有框架像手写账本——改一个数字,你得把受影响的所有行都手动重算一遍。 React 像 Excel——你改一个单元格,所有依赖它的公式自动重算。
你只负责改数据,刷新界面是 React 的活儿。
第 3 步:JSX —— 为什么 HTML 长在 JS 里
打开 app/page.tsx,你看到的大概是这样:
export default function Home() { return ( <main className="p-8"> <h1 className="text-3xl font-bold">遇见成都</h1> </main> );}第一反应会是:这是 HTML 还是 JS?
答案:这是 JSX —— 在 JS 里直接写标签。 它会被编译成真正的 HTML。
| 你要知道的 | 说明 |
|---|---|
| 它就是 4.2 学的那些标签 | <div> <h1> <img> 全都一样 |
class 要写成 className |
因为 class 在 JS 里是保留字。这是 JSX 最常见的一个差异 |
{} 里面是 JS |
<h1>{city}</h1> —— 大括号里的东西会被当代码执行,结果显示出来 |
| 标签必须闭合 | <img> 要写成 <img />,4.2 里 HTML 容忍的写法这里不行 |
| 只能返回一个最外层元素 | 要并列多个就用 <>...</> 包起来(叫 Fragment) |
{} 这个能力是关键,它让“数据”和“界面”连起来了:
const city = "成都";const count = 6;
return ( <div> <h1>遇见{city}</h1> {/* 显示:遇见成都 */} <p>共 {count} 个景点</p> {/* 显示:共 6 个景点 */} <p>{count > 5 ? "很多" : "不多"}</p> {/* 还能写判断 */} </div>);🔑 看到
{}就知道:这里的内容是算出来的,不是写死的。 这是你读 JSX 时最该注意的信号——页面上哪些部分是动态的,一眼就看出来了。
第 4 步:组件 —— 自己造标签
组件就是一个返回 JSX 的函数。 就这么简单。
function AttractionCard() { return ( <div className="rounded-xl shadow p-4"> <h3>大熊猫繁育研究基地</h3> <p>¥55 · 3 小时</p> </div> );}定义好之后,它就能像标签一样用:
<AttractionCard />| 规则 | 说明 |
|---|---|
| 首字母必须大写 | <AttractionCard /> ✅ / <attractionCard /> ❌ —— 小写会被当成普通 HTML 标签 |
| 它是个函数 | 所以你会看到 function Xxx() 或 const Xxx = () => |
| 必须 return JSX | 返回的东西就是它渲染出来的界面 |
💡 怎么在代码里认出组件:找首字母大写的标签。
<div>是 HTML,<AttractionCard>是别人写的组件——想改它的样子,就去找同名的那个函数。
第 5 步:props —— 给组件传参数
上面那个卡片有个问题:内容写死了,六个景点就得写六个组件。
props 解决这个——像给标签加属性一样给组件传数据:
// 定义时:接收 propsfunction AttractionCard(props) { return ( <div className="rounded-xl shadow p-4"> <h3>{props.name}</h3> <p>¥{props.price} · {props.duration}</p> </div> );}
// 使用时:像 HTML 属性一样传<AttractionCard name="大熊猫基地" price={55} duration="3 小时" /><AttractionCard name="宽窄巷子" price={0} duration="2 小时" />对照 4.2 学的 HTML 属性,你会发现是同一个思路:
HTML: <img src="/panda.jpg" alt="熊猫" /> ← 给标签传信息React: <AttractionCard name="大熊猫基地" /> ← 给组件传信息你还会常看到这种解构写法,意思完全一样:
function AttractionCard({ name, price, duration }) { return <div><h3>{name}</h3>...</div>;}看到
({ name, price })这个形状,就知道“这个组件需要别人传 name 和 price 给它”。这是你读组件代码时第一个要看的地方——它需要什么数据。
第 6 步:组件 + 数组 + map = 第 5 章的核心套路
现在把 4.4 的 map 接上来。这是本章最重要的一个组合。
const attractions = [ { id: 1, name: "大熊猫基地", price: 55, duration: "3 小时" }, { id: 2, name: "宽窄巷子", price: 0, duration: "2 小时" }, { id: 3, name: "武侯祠", price: 50, duration: "1.5 小时" }];
export default function Home() { return ( <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> {attractions.map(item => ( <AttractionCard key={item.id} name={item.name} price={item.price} duration={item.duration} /> ))} </div> );}读一遍这段代码,你会发现本章学的东西全在里面:
| 这一部分 | 来自哪一节 |
|---|---|
[{...}, {...}] 数据形状 |
4.4(“一张表格”) |
.map(item => ...) |
4.4(把一个数组变成另一个数组) |
grid grid-cols-1 md:grid-cols-3 gap-6 |
4.3(Grid 布局 + 响应式断点) |
<AttractionCard /> 自定义标签 |
4.6(组件) |
name={item.name} |
4.6(props) |
现在景点改成 20 个,你只要改数据数组。 卡片样式要改,只改 AttractionCard 一处。这就是第 1 步那三个痛点的答案。
🔑
key={item.id}是干什么的:React 需要一个身份证来区分列表里的每一项,否则更新时会错乱。你会遇到这个警告:控制台黄字
Each child in a list should have a unique "key" prop。看到它就知道:某个 map 渲染的列表少了 key。跟 AI 说一句就能修。
第 7 步:state —— 会变的数据
props 是“别人传进来的数据”,state 是“组件自己的、会变的数据”。
import { useState } from "react";
function Counter() { const [count, setCount] = useState(0); // ↑读 ↑改 ↑初始值
return ( <button onClick={() => setCount(count + 1)}> 已有 {count} 人想去 </button> );}useState 返回两个东西:
| 作用 | |
|---|---|
count |
当前的值(读它) |
setCount |
改这个值的函数(必须用它改) |
点一下按钮 → setCount 改了数据 → React 自动重新渲染,按钮上的数字变了。
⚠️ 一条铁律:改 state 必须用
setXxx。直接写count = count + 1不会有任何反应——React 不知道数据变了,界面不会更新。这是新手最常见的 React bug,而且它不报错,就是“点了没反应”(4.1 定位表里那一条)。
什么东西该做成 state:
| 场景 | 要 state |
|---|---|
| 输入框里用户打的字 | ✅ |
| 弹窗是开着还是关着 | ✅ |
| 当前选中的是哪个标签页 | ✅ |
| 从服务器拿回来的数据 | ✅ |
| 景点的固定介绍文字 | ❌ 不变的东西不需要 state |
第 8 步:你会在 AI 代码里看到的五样东西
这是本节的核心交付能力。 打开任意一个 React 文件,找这五样:
| # | 找什么 | 长什么样 | 意味着什么 |
|---|---|---|---|
| 1 | 组件定义 | export default function Home() |
这个文件对应的界面从这儿开始 |
| 2 | return 里的 JSX | return ( <div>... ) |
页面长什么样,看这里 |
| 3 | props | ({ name, price }) |
这个组件需要别人给它什么数据 |
| 4 | useState | const [x, setX] = useState() |
哪些东西是会变的 |
| 5 | map | {list.map(item => ...)} |
数据在这里变成了界面 |
💡 配合 4.4 第 6 步的四步法,你现在有了一套完整的读码方法: 先找数据(数组 / useState)→ 再找 map(数据怎么变成界面)→ 再找事件(onClick)→ 剩下的是样式,看 Tailwind class。
不用逐行读。 找到这几个点,整个文件的骨架就清楚了。
第 9 步:动手——把重复代码变成组件
这是本节交付物。 在 hello-node 里做。
动手前先存档(2.3 的规矩一):
git add -A && git commit -m "4.6 组件化练习前的存档点"第一步,先造出重复。 让 AI 做:
在 app/page.tsx 里,把现有内容替换成三个景点卡片,每张卡片包含景点名、价格、游玩时长,用 Tailwind 做出圆角和浅阴影。**故意不要抽组件,三张卡片的代码重复写三遍**,我要用它做组件化练习。跑起来看一眼(pnpm dev),然后打开 page.tsx——你会看到三段几乎一样的代码。
第二步,让它改造:
现在把这三段重复的卡片代码抽成一个组件:1. 组件名叫 AttractionCard,放在 components/AttractionCard.tsx;2. 通过 props 接收景点名、价格、时长;3. 在 page.tsx 里把景点数据写成一个数组,用 map 渲染出三张卡片,记得加 key;4. 页面最终效果要和现在**完全一样**。
改完告诉我:你新建了哪些文件、page.tsx 里的代码行数从多少变成了多少。第三步,验收(3.5 的四种验收):
git diff --stat # 看改动范围浏览器刷新 —— 页面看起来应该一模一样,但代码已经完全不同了。
第四步,检验组件化到底值不值。 再提一个需求:
给每张卡片加一个「加入行程」按钮,样式和卡片风格一致。注意它只改了一个文件(AttractionCard.tsx),三张卡片全都有了按钮。
📷 截图位 1:改造前后的 page.tsx 对比,以及加按钮时只改一个文件的 git diff
满意就存档:
git add -A && git commit -m "抽出 AttractionCard 组件,用 map 渲染列表"🔑 你刚才做的,就是第 5.4 节的核心内容。 到那时只是换成真实的六个成都景点和真实的图片——动作一模一样。
第 10 步:什么时候不需要框架
为了不让你以为“框架永远是对的”:
| 场景 | 建议 |
|---|---|
| 一个静态落地页,没有交互 | 纯 HTML + CSS 足够,上框架是负担 |
| 一个简单的工具页(算个数、转个格式) | 单个 HTML 文件就行 |
| 内容型网站、多页面、有重复区块 | 用框架(第 5 章) |
| 有数据、有登录、有交互状态 | 必须用框架(第 9 章) |
1.4 你用在线工具做的那个个人主页,背后很可能就是纯 HTML——那个场景确实不需要 React。
四、完成的标志是
- 我说得出纯 HTML 做重复区块的三个痛点
- 我知道 React 的核心是「改数据,界面自动变」
- 我知道 JSX 就是“写在 JS 里的标签”,且
class要写成className - 我知道 JSX 里
{}包的是会被执行的代码(看到它就知道这块是动态的) - 我知道组件就是个返回 JSX 的函数,首字母必须大写
- 我知道 props 是“给组件传数据”,和 HTML 属性是同一个思路
- 我看得懂
{list.map(item => <Card key={item.id} name={item.name} />)}在干什么 - 我知道
key是列表项的身份证,少了会有控制台警告 - 我知道 state 是“会变的数据”,改它必须用
setXxx,直接赋值不会更新界面 - 我记住了读 React 代码要找的五样东西
- 我完成了「重复三段 → 一个组件 + map」的改造,且页面效果不变
- 我体会到了加按钮时只需改一个文件
五、卡住了看这里
Q:控制台警告 Each child in a list should have a unique "key" prop。
map 渲染的列表少了 key。跟 AI 说:「给这个 map 渲染的列表加上 key,用数据里的 id」。这是警告不是错误,页面还能跑,但该修。
Q:报错 Objects are not valid as a React child。
你把一个对象直接放进 JSX 显示了。比如写了 {item} 而不是 {item.name}。把整段报错给 AI 即可。
Q:我改了数据,页面没变。
两种情况:① 改的是普通变量而不是 state → 要用 useState + setXxx;② 用了 state 但直接赋值(count = 1)而不是 setCount(1)。这两个都是“点了没反应”的典型原因。
Q:className 和 class 我老是写错。
不用担心,你不写代码。但 review 时见到 JSX 里写了 class= 要留意——样式不生效的一个隐蔽原因就是这个。
Q:组件抽出来之后,页面变样了 / 报错了。
用 2.3 的后悔药:git restore . 回到改造前,然后换个说法重来(3.5 讲的“三轮不对就回滚重开”)。常见原因是 props 名字对不上——AI 在组件里写了 title,在 page 里传的是 name。
Q:React 和 Next.js 什么关系? React 管“怎么把数据变成界面”,Next.js 在 React 外面加了一层:路由(网址怎么对应页面)、服务端渲染、项目结构约定。
类比:React 是发动机,Next.js 是整台车。 你的
hello-node就是 Next.js 项目,里面写的是 React。4.7、4.8 讲 Next.js 那一层。
Q:我需要专门去学 React 吗? 不需要。本节这些就是你做完第 5 章项目所需的全部 React 知识。 等你做完三个项目,回头再看 React 文档会轻松很多——那时你已经有实感了。
六、本节提示词
① 让 AI 解释一个 React 文件(按第 8 步的五样东西问):
我是零基础,正在学看懂 React。请讲解这个文件:【@文件名 或粘贴代码】
请按这个顺序讲,用大白话:1. **组件**:这个文件定义了哪些组件,分别对应页面上哪一块?2. **数据**:数据从哪来?是写死的、props 传进来的、还是 state?3. **动态部分**:JSX 里哪些 `{}` 是动态的,分别显示什么?4. **列表**:有没有 map?它把什么数据变成了什么界面?5. **交互**:有没有 onClick 之类的事件?点了会发生什么?6. 最后用三句话总结这个文件在干什么。
不要逐行翻译语法。② 抽组件(本节第 9 步的完整版):
我的页面里有重复的代码,想抽成组件。
文件:【@文件名】重复的部分:【描述,比如"三段几乎一样的卡片代码"】
请你:1. **先告诉我方案**:组件叫什么、放哪个文件、通过 props 接收哪些数据,等我确认再动手;2. 改造后页面效果必须**完全不变**;3. 数据抽成数组,用 map 渲染,记得加 key;4. 不要新增任何依赖包;5. 改完告诉我:新建了哪些文件、原文件代码行数的变化。③ 判断该不该用 state:
我想在页面上实现这个效果:【具体描述,比如"点击按钮展开/收起详情"】
请告诉我:1. 这里面哪些数据是"会变的",需要做成 state?2. 哪些是固定的,不需要 state?3. 给我实现思路(**先不要写代码**),说明用户操作之后数据怎么变、界面怎么跟着变;4. 这个功能有没有更简单的做法(比如纯 CSS 就能实现)?上一节 ← 4.5 一个前端项目由哪些文件组成 下一节 → 4.7 Next.js 快速上手:创建项目与目录结构