React 快速入门:菜谱应用实战教程
React 快速入门:菜谱应用实战教程
第一部分:React 开发准备
1.1 React 是什么?
React 是 Facebook 开发的一个用于构建用户界面的 JavaScript 库。它具有三个核心特点:
组件化(Component-Based)
将复杂的 UI 拆分成独立、可复用的组件,就像搭积木一样构建应用。
声明式编程(Declarative)
你只需要描述 UI 应该"是什么样子",React 会自动处理 DOM 更新。
单向数据流(Unidirectional Data Flow)
数据从父组件流向子组件,让数据流动可预测、易调试。
为什么需要构建工具?
直接在浏览器中使用 React 会遇到三个问题:
- JSX 语法:浏览器不认识
<div>Hello</div>
这样的 JSX 代码 - 模块化:无法使用
import/export
组织代码 - 现代 JavaScript:ES6+ 语法在旧浏览器中不兼容
这就需要 Webpack 和 Babel 这两个工具来解决。
1.2 开发环境准备
前置要求
确保已安装 Node.js(推荐 14.x 或更高版本)。验证安装:
node -v
npm -v
创建项目目录
mkdir recipes-app
cd recipes-app
初始化项目并安装依赖
# 初始化 package.json
npm init -y# 安装 React 核心库
npm install react react-dom# 安装 Webpack 打包工具
npm install -D webpack webpack-cli# 安装 Babel 转译工具
npm install -D babel-loader @babel/core @babel/preset-env @babel/preset-react
依赖说明
依赖包 | 作用 |
---|---|
react | React 核心库 |
react-dom | React DOM 操作库 |
webpack | 模块打包器 |
webpack-cli | Webpack 命令行工具 |
babel-loader | Webpack 的 Babel 加载器 |
@babel/core | Babel 核心库 |
@babel/preset-env | 转译 ES6+ 语法 |
@babel/preset-react | 转译 JSX 语法 |
1.3 项目结构搭建
目标结构
recipes-app/
├── package.json
├── .babelrc
├── webpack.config.js
├── dist/
│ └── index.html
└── src/├── index.js├── components/│ ├── Menu.js│ ├── Recipe.js│ ├── IngredientsList.js│ ├── Ingredient.js│ └── Instructions.js└── data/└── recipes.json
创建目录和文件
Windows (CMD/PowerShell):
mkdir dist
mkdir src
mkdir src\components
mkdir src\datatype nul > .babelrc
type nul > webpack.config.js
type nul > dist\index.html
type nul > src\index.js
type nul > src\components\Menu.js
type nul > src\components\Recipe.js
type nul > src\components\IngredientsList.js
type nul > src\components\Ingredient.js
type nul > src\components\Instructions.js
type nul > src\data\recipes.json
macOS/Linux:
mkdir -p dist src/components src/datatouch .babelrc webpack.config.js
touch dist/index.html
touch src/index.js
touch src/components/{Menu,Recipe,IngredientsList,Ingredient,Instructions}.js
touch src/data/recipes.json
跨平台(Node.js):
node -e "const fs=require('fs');const dirs=['dist','src','src/components','src/data'];dirs.forEach(d=>fs.mkdirSync(d,{recursive:true}));const files=['.babelrc','webpack.config.js','dist/index.html','src/index.js','src/components/Menu.js','src/components/Recipe.js','src/components/IngredientsList.js','src/components/Ingredient.js','src/components/Instructions.js','src/data/recipes.json'];files.forEach(f=>fs.writeFileSync(f,''));"
第二部分:配置构建工具
2.1 Babel 配置(.babelrc)
创建 .babelrc
文件,内容如下:
{"presets": ["@babel/preset-env", "@babel/preset-react"]
}
配置说明
@babel/preset-env
:将 ES6+ 语法(箭头函数、解构、模板字符串等)转译为 ES5@babel/preset-react
:将 JSX 语法转译为React.createElement()
函数调用
Babel 的作用
Babel 是一个 JavaScript 编译器,它让你能使用最新的语法编写代码,然后自动转换成浏览器能理解的旧版本代码。
转译示例:
// 你写的代码
const greeting = (name) => <h1>Hello, {name}!</h1>;// Babel 转译后
var greeting = function(name) {return React.createElement("h1", null, "Hello, ", name, "!");
};
2.2 Webpack 配置(webpack.config.js)
创建 webpack.config.js
文件:
const path = require('path');module.exports = {entry: './src/index.js',output: {path: path.join(__dirname, 'dist', 'assets'),filename: 'bundle.js'},module: {rules: [{test: /\.jsx?$/,exclude: /node_modules/,loader: 'babel-loader'}]},devtool: 'source-map'
};
配置说明
配置项 | 作用 |
---|---|
entry | 应用的入口文件,Webpack 从这里开始构建依赖图 |
output.path | 打包后文件的输出目录 |
output.filename | 打包后文件的名称 |
module.rules | 定义如何处理不同类型的文件 |
test: /\.jsx?$/ | 匹配所有 .js 和 .jsx 文件 |
loader: 'babel-loader' | 使用 Babel 转译 JS/JSX 文件 |
devtool: 'source-map' | 生成源码映射,便于浏览器调试 |
Webpack 的作用
Webpack 是一个模块打包器。它的工作流程:
- 从
entry
入口文件开始 - 分析所有的
import
语句,构建依赖图 - 使用对应的 loader 处理每种类型的文件
- 将所有模块打包成一个或多个 bundle 文件
src/index.js (入口)↓ import Menu
src/components/Menu.js↓ import Recipe
src/components/Recipe.js↓ import IngredientsList, Instructions
...↓ 打包
dist/assets/bundle.js (输出)
2.3 添加构建脚本(package.json)
在 package.json
中添加 scripts
字段:
{"name": "recipes-app","version": "1.0.0","scripts": {"dev": "webpack --mode development","build": "webpack --mode production"},"dependencies": {"react": "^18.2.0","react-dom": "^18.2.0"},"devDependencies": {"@babel/core": "^7.23.0","@babel/preset-env": "^7.23.0","@babel/preset-react": "^7.22.0","babel-loader": "^9.1.3","webpack": "^5.89.0","webpack-cli": "^5.1.4"}
}
脚本说明
npm run dev
:开发模式构建(代码未压缩,构建快)npm run build
:生产模式构建(代码压缩,体积小)
2.4 HTML 入口页面(dist/index.html)
创建 dist/index.html
文件:
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>React 菜谱应用</title><style>* {margin: 0;padding: 0;box-sizing: border-box;}body {font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Arial', sans-serif;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);min-height: 100vh;padding: 20px;}#root {max-width: 1200px;margin: 0 auto;}article > header {text-align: center;margin-bottom: 40px;}article > header h1 {color: white;font-size: 3rem;text-shadow: 2px 2px 4px rgba(0,0,0,0.3);margin-bottom: 10px;}.recipes {display: grid;grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));gap: 30px;}.recipe {background: white;border-radius: 16px;padding: 30px;box-shadow: 0 10px 30px rgba(0,0,0,0.2);transition: transform 0.3s ease;}.recipe:hover {transform: translateY(-5px);}.recipe h2 {color: #667eea;font-size: 2rem;margin-bottom: 20px;border-bottom: 3px solid #667eea;padding-bottom: 10px;}.ingredients {background: #f8f9fa;border-radius: 12px;padding: 20px;margin-bottom: 25px;border-left: 5px solid #667eea;}.ingredients li {list-style: none;padding: 8px 0;color: #495057;font-size: 1.05rem;}.ingredients li:before {content: "✓ ";color: #667eea;font-weight: bold;margin-right: 8px;}.instructions {margin-top: 20px;}.instructions h3 {color: #495057;font-size: 1.3rem;margin-bottom: 15px;}.instructions ol {padding-left: 25px;}.instructions li {margin: 12px 0;line-height: 1.6;color: #6c757d;font-size: 1.05rem;}.instructions li:before {font-weight: bold;color: #667eea;}</style>
</head>
<body><div id="root"></div><script src="assets/bundle.js"></script>
</body>
</html>
关键点说明
<div id="root"></div>
:React 应用的挂载点<script src="assets/bundle.js"></script>
:引入 Webpack 打包后的文件- CSS 样式:为菜谱应用提供美观的视觉效果
第三部分:React 菜谱应用实战
3.1 React 基础概念
在开始编写组件之前,我们需要理解两个核心概念:
JSX 语法
JSX 是 JavaScript 的语法扩展,让你能在 JavaScript 中编写类似 HTML 的代码。
JSX 基本规则:
// 1. JSX 必须有一个根元素
function App() {return (<div><h1>标题</h1><p>段落</p></div>);
}// 2. 在 JSX 中使用 JavaScript 表达式(用 {} 包裹)
function Greeting({ name }) {return <h1>Hello, {name}!</h1>;
}// 3. 使用 className 代替 class(因为 class 是 JS 关键字)
function Button() {return <button className="btn-primary">点击</button>;
}// 4. 自闭合标签必须有 /
function Image() {return <img src="photo.jpg" alt="照片" />;
}// 5. 可以在 JSX 中使用 map 渲染列表
function List({ items }) {return (<ul>{items.map((item, index) => (<li key={index}>{item}</li>))}</ul>);
}
Props(属性)
Props 是父组件向子组件传递数据的方式,类似于函数的参数。
Props 的特点:
- 只读:子组件不能修改 props
- 单向流动:数据从父组件流向子组件
- 任意类型:可以传递字符串、数字、对象、数组、函数等
// 父组件传递 props
function Parent() {return <Child name="张三" age={25} />;
}// 子组件接收 props(方式一:对象解构)
function Child({ name, age }) {return <p>{name} 今年 {age} 岁</p>;
}// 子组件接收 props(方式二:props 对象)
function Child(props) {return <p>{props.name} 今年 {props.age} 岁</p>;
}
3.2 应用需求分析
我们要构建一个菜谱应用,具有以下功能:
功能需求
- 展示多道菜谱
- 每道菜谱包含:菜名、配料列表、烹饪步骤
数据结构设计
创建 src/data/recipes.json
文件:
[{"name": "意大利面","ingredients": [{ "name": "意大利面", "amount": 200, "measurement": "克" },{ "name": "番茄酱", "amount": 100, "measurement": "克" },{ "name": "洋葱", "amount": 1, "measurement": "个" },{ "name": "大蒜", "amount": 3, "measurement": "瓣" },{ "name": "橄榄油", "amount": 2, "measurement": "汤匙" }],"steps": ["煮沸一锅盐水","加入意大利面煮 8-10 分钟","热锅加橄榄油,爆香蒜末和洋葱丁","加入番茄酱翻炒均匀","将煮好的面条加入酱汁中拌匀","装盘即可享用"]},{"name": "炒饭","ingredients": [{ "name": "米饭", "amount": 2, "measurement": "碗" },{ "name": "鸡蛋", "amount": 2, "measurement": "个" },{ "name": "胡萝卜", "amount": 50, "measurement": "克" },{ "name": "青豆", "amount": 30, "measurement": "克" },{ "name": "酱油", "amount": 1, "measurement": "汤匙" }],"steps": ["鸡蛋打散炒熟后盛出","胡萝卜切丁,与青豆一起炒熟","加入米饭翻炒","加入炒好的鸡蛋","倒入酱油调味","翻炒均匀后出锅"]},{"name": "番茄炒蛋","ingredients": [{ "name": "番茄", "amount": 3, "measurement": "个" },{ "name": "鸡蛋", "amount": 4, "measurement": "个" },{ "name": "白糖", "amount": 1, "measurement": "勺" },{ "name": "盐", "amount": 1, "measurement": "勺" },{ "name": "食用油", "amount": 2, "measurement": "汤匙" }],"steps": ["番茄切块,鸡蛋打散","热锅下油,炒鸡蛋至半熟盛出","再下油,炒番茄块至出汁","加入白糖和盐调味","倒入炒好的鸡蛋","翻炒均匀后出锅"]}
]
3.3 组件设计思路
我们采用自底向上的方式设计组件,从最小的组件开始构建:
Menu(菜单)└─ Recipe(单个菜谱)├─ IngredientsList(配料列表)│ └─ Ingredient(单个配料)└─ Instructions(步骤说明)
组件职责划分:
组件 | 职责 | Props |
---|---|---|
Ingredient | 显示单个配料 | amount, measurement, name |
IngredientsList | 渲染配料列表 | list |
Instructions | 显示烹饪步骤 | title, steps |
Recipe | 组合配料和步骤 | name, ingredients, steps |
Menu | 渲染多个菜谱 | recipes |
3.4 编写基础组件
3.4.1 Ingredient 组件
创建 src/components/Ingredient.js
:
import React from 'react';function Ingredient({ amount, measurement, name }) {return (<li>{amount} {measurement} {name}</li>);
}export default Ingredient;
组件说明:
- 职责:显示单个配料,格式为"数量 单位 名称"
- Props:
amount
:配料数量measurement
:计量单位name
:配料名称
- 返回:一个
<li>
元素
3.4.2 IngredientsList 组件
创建 src/components/IngredientsList.js
:
import React from 'react';
import Ingredient from './Ingredient';function IngredientsList({ list }) {return (<ul className="ingredients">{list.map((ingredient, i) => (<Ingredient key={i} {...ingredient} />))}</ul>);
}export default IngredientsList;
组件说明:
- 职责:循环渲染配料列表
- Props:
list
:配料数组
- 关键技术:
- 使用
map
方法遍历数组 key={i}
:React 要求列表项必须有唯一的 key{...ingredient}
:展开运算符,等价于amount={ingredient.amount} measurement={ingredient.measurement} name={ingredient.name}
- 使用
3.4.3 Instructions 组件
创建 src/components/Instructions.js
:
import React from 'react';function Instructions({ title, steps }) {return (<section className="instructions"><h3>{title}</h3><ol>{steps.map((step, i) => (<li key={i}>{step}</li>))}</ol></section>);
}export default Instructions;
组件说明:
- 职责:显示烹饪步骤说明
- Props:
title
:步骤标题steps
:步骤数组
- 返回:带有标题和有序列表的 section
3.5 组合组件
3.5.1 Recipe 组件
创建 src/components/Recipe.js
:
import React from 'react';
import IngredientsList from './IngredientsList';
import Instructions from './Instructions';function Recipe({ name, ingredients, steps }) {return (<section className="recipe"><h2>{name}</h2><IngredientsList list={ingredients} /><Instructions title="烹饪步骤" steps={steps} /></section>);
}export default Recipe;
组件说明:
- 职责:组合配料列表和烹饪步骤,展示完整的单道菜谱
- Props:
name
:菜名ingredients
:配料数组steps
:步骤数组
- 组合方式:使用
<IngredientsList>
和<Instructions>
子组件
3.5.2 Menu 组件
创建 src/components/Menu.js
:
import React from 'react';
import Recipe from './Recipe';function Menu({ recipes }) {return (<article><header><h1>美味菜谱</h1></header><div className="recipes">{recipes.map((recipe, i) => (<Recipe key={i} {...recipe} />))}</div></article>);
}export default Menu;
组件说明:
- 职责:应用的根组件,渲染所有菜谱
- Props:
recipes
:菜谱数组
- 结构:包含标题和多个 Recipe 组件
3.6 应用入口
创建 src/index.js
:
import React from 'react';
import { createRoot } from 'react-dom/client';
import Menu from './components/Menu';
import data from './data/recipes.json';const root = createRoot(document.getElementById('root'));
root.render(<Menu recipes={data} />);
代码说明:
-
导入依赖:
React
:React 核心库createRoot
:React 18 的新 API,用于创建根节点Menu
:我们的根组件data
:菜谱数据
-
创建根节点:
const root = createRoot(document.getElementById('root'));
获取 HTML 中的
<div id="root">
元素并创建 React 根节点 -
渲染应用:
root.render(<Menu recipes={data} />);
将 Menu 组件渲染到根节点,并传入菜谱数据
第四部分:构建与运行应用
4.1 开发模式构建
在项目根目录运行:
npm run dev
构建过程:
- Webpack 读取
src/index.js
入口文件 - 分析所有
import
语句,构建依赖图 - 使用 babel-loader 转译 JSX 和 ES6+ 语法
- 打包所有模块到
dist/assets/bundle.js
生成的文件:
dist/assets/
├── bundle.js # 应用代码(未压缩,约 1.2 MB)
├── bundle.js.map # 源码映射文件
└── bundle.js.LICENSE.txt # 第三方库许可证
文件说明:
- bundle.js:包含你的代码和 React 库的完整应用
- bundle.js.map:Source Map 文件,用于浏览器调试
- bundle.js.LICENSE.txt:React 等第三方库的开源许可证信息
4.2 在浏览器中查看
用浏览器打开 dist/index.html
,你会看到:
- 页面标题:“美味菜谱”
- 三道菜谱卡片(意大利面、炒饭、番茄炒蛋)
- 每个卡片包含配料和步骤
使用开发者工具调试:
- 按
F12
打开开发者工具 - 切换到
Sources
面板 - 在左侧文件树中找到
webpack://
→src/
目录 - 可以看到你的原始源代码(这就是 Source Map 的作用)
- 设置断点并调试
Source Map 的价值:
没有 Source Map,你只能看到压缩后的 bundle.js
:
!function(e){var t={};function n(r){if(t[r])return...
有了 Source Map,你可以直接调试源代码:
function Ingredient({ amount, measurement, name }) {return <li>{amount} {measurement} {name}</li>;
}
4.3 生产模式构建
准备部署时,使用生产模式:
npm run build
与开发模式的区别:
特性 | 开发模式 | 生产模式 |
---|---|---|
代码压缩 | ❌ | ✅ |
混淆变量名 | ❌ | ✅ |
移除注释 | ❌ | ✅ |
文件大小 | ~1.2 MB | ~150 KB |
构建速度 | 快 | 慢 |
调试体验 | 好 | 差 |
适用场景 | 本地开发 | 线上部署 |
生产模式的代码示例:
// 开发模式(可读)
function Ingredient(_ref) {var amount = _ref.amount,measurement = _ref.measurement,name = _ref.name;return /*#__PURE__*/ (0, _jsxRuntime.jsxs)("li", {children: [amount, " ", measurement, " ", name]});
}// 生产模式(压缩混淆)
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t()...
使用场景:
- 开发时:
npm run dev
(快速迭代) - 上线前:
npm run build
(优化性能)
第五部分:动手实践
5.1 练习任务
练习 1:添加新菜谱 ⭐
任务描述:
在 recipes.json
中添加一道你喜欢的菜,重新构建并查看效果。
操作步骤:
- 打开
src/data/recipes.json
- 在数组中添加新对象:
{"name": "宫保鸡丁","ingredients": [{ "name": "鸡胸肉", "amount": 300, "measurement": "克" },{ "name": "花生米", "amount": 100, "measurement": "克" },{ "name": "干辣椒", "amount": 10, "measurement": "个" },{ "name": "花椒", "amount": 1, "measurement": "勺" },{ "name": "酱油", "amount": 2, "measurement": "汤匙" }],"steps": ["鸡肉切丁,用料酒和淀粉腌制","花生米炸至金黄盛出","热锅下油,爆香干辣椒和花椒","下鸡丁快速翻炒至变色","加入酱油和白糖调味","加入花生米翻炒均匀出锅"]
}
- 保存文件
- 重新构建:
npm run dev
- 刷新浏览器,看到新增的宫保鸡丁菜谱
学习目标:
- 理解数据驱动视图的概念
- React 会自动根据数据变化更新 UI
- 无需手动操作 DOM
练习 2:新增评分组件 ⭐⭐
任务描述:
创建一个 Rating 组件,在每个菜谱中显示星级评分。
步骤 1:创建 Rating 组件
创建 src/components/Rating.js
:
import React from 'react';function Rating({ rating }) {const stars = [];for (let i = 1; i <= 5; i++) {if (i <= rating) {stars.push(<span key={i} style={{ color: '#FFD700', fontSize: '1.5rem' }}>★</span>);} else {stars.push(<span key={i} style={{ color: '#ddd', fontSize: '1.5rem' }}>★</span>);}}return <div style={{ margin: '10px 0' }}>{stars}</div>;
}export default Rating;
步骤 2:在 Recipe 组件中使用
修改 src/components/Recipe.js
:
import React from 'react';
import IngredientsList from './IngredientsList';
import Instructions from './Instructions';
import Rating from './Rating'; // 新增导入function Recipe({ name, ingredients, steps, rating }) { // 新增 rating 参数return (<section className="recipe"><h2>{name}</h2><Rating rating={rating} /> {/* 新增评分组件 */}<IngredientsList list={ingredients} /><Instructions title="烹饪步骤" steps={steps} /></section>);
}export default Recipe;
步骤 3:更新数据文件
在 src/data/recipes.json
中为每道菜添加 rating
字段:
[{"name": "意大利面","rating": 5,"ingredients": [...],"steps": [...]},{"name": "炒饭","rating": 4,"ingredients": [...],"steps": [...]},{"name": "番茄炒蛋","rating": 5,"ingredients": [...],"steps": [...]}
]
步骤 4:重新构建并查看
npm run dev
刷新浏览器,每个菜谱下方会显示星级评分。
学习目标:
- 创建新组件的完整流程
- 在父组件中引入和使用子组件
- 通过 props 传递数据
- 更新数据结构以支持新功能
练习 3:添加样式优化 ⭐
任务描述:
修改 CSS 样式,让菜谱卡片更加美观。
步骤 1:修改 dist/index.html 中的样式
在 <style>
标签中添加或修改:
/* 为评分组件添加样式 */
.recipe .rating {display: flex;align-items: center;margin: 15px 0;
}/* 让配料项悬停时高亮 */
.ingredients li:hover {background-color: #e9ecef;padding-left: 10px;transition: all 0.3s ease;
}/* 为步骤添加更好的视觉效果 */
.instructions li {background: #f8f9fa;padding: 12px;margin: 10px 0;border-radius: 8px;border-left: 3px solid #667eea;
}.instructions li:hover {background: #e9ecef;transform: translateX(5px);transition: all 0.3s ease;
}/* 为卡片添加渐变边框效果 */
.recipe {position: relative;border: 2px solid transparent;background-clip: padding-box;
}.recipe:before {content: '';position: absolute;top: -2px;left: -2px;right: -2px;bottom: -2px;background: linear-gradient(135deg, #667eea, #764ba2);border-radius: 16px;z-index: -1;opacity: 0;transition: opacity 0.3s ease;
}.recipe:hover:before {opacity: 1;
}
步骤 2:查看效果
直接刷新浏览器(不需要重新构建,因为改的是 HTML 文件),体验:
- 配料项悬停高亮
- 步骤项悬停移动
- 卡片悬停渐变边框
学习目标:
- 理解样式与组件的关系
- CSS 可以独立于 React 组件修改
- 使用现代 CSS 技术增强用户体验
5.2 参考实现
完整的 Rating 组件
import React from 'react';function Rating({ rating }) {// 确保 rating 在 0-5 之间const normalizedRating = Math.max(0, Math.min(5, rating));const stars = [];for (let i = 1; i <= 5; i++) {stars.push(<span key={i} style={{ color: i <= normalizedRating ? '#FFD700' : '#ddd',fontSize: '1.5rem',marginRight: '2px'}}>★</span>);}return (<div className="rating" style={{ margin: '10px 0' }}>{stars}<span style={{ marginLeft: '10px', color: '#666' }}>({normalizedRating}/5)</span></div>);
}export default Rating;
完整更新后的 recipes.json
[{"name": "意大利面","rating": 5,"ingredients": [{ "name": "意大利面", "amount": 200, "measurement": "克" },{ "name": "番茄酱", "amount": 100, "measurement": "克" },{ "name": "洋葱", "amount": 1, "measurement": "个" },{ "name": "大蒜", "amount": 3, "measurement": "瓣" },{ "name": "橄榄油", "amount": 2, "measurement": "汤匙" }],"steps": ["煮沸一锅盐水","加入意大利面煮 8-10 分钟","热锅加橄榄油,爆香蒜末和洋葱丁","加入番茄酱翻炒均匀","将煮好的面条加入酱汁中拌匀","装盘即可享用"]},{"name": "炒饭","rating": 4,"ingredients": [{ "name": "米饭", "amount": 2, "measurement": "碗" },{ "name": "鸡蛋", "amount": 2, "measurement": "个" },{ "name": "胡萝卜", "amount": 50, "measurement": "克" },{ "name": "青豆", "amount": 30, "measurement": "克" },{ "name": "酱油", "amount": 1, "measurement": "汤匙" }],"steps": ["鸡蛋打散炒熟后盛出","胡萝卜切丁,与青豆一起炒熟","加入米饭翻炒","加入炒好的鸡蛋","倒入酱油调味","翻炒均匀后出锅"]},{"name": "番茄炒蛋","rating": 5,"ingredients": [{ "name": "番茄", "amount": 3, "measurement": "个" },{ "name": "鸡蛋", "amount": 4, "measurement": "个" },{ "name": "白糖", "amount": 1, "measurement": "勺" },{ "name": "盐", "amount": 1, "measurement": "勺" },{ "name": "食用油", "amount": 2, "measurement": "汤匙" }],"steps": ["番茄切块,鸡蛋打散","热锅下油,炒鸡蛋至半熟盛出","再下油,炒番茄块至出汁","加入白糖和盐调味","倒入炒好的鸡蛋","翻炒均匀后出锅"]},{"name": "宫保鸡丁","rating": 5,"ingredients": [{ "name": "鸡胸肉", "amount": 300, "measurement": "克" },{ "name": "花生米", "amount": 100, "measurement": "克" },{ "name": "干辣椒", "amount": 10, "measurement": "个" },{ "name": "花椒", "amount": 1, "measurement": "勺" },{ "name": "酱油", "amount": 2, "measurement": "汤匙" }],"steps": ["鸡肉切丁,用料酒和淀粉腌制","花生米炸至金黄盛出","热锅下油,爆香干辣椒和花椒","下鸡丁快速翻炒至变色","加入酱油和白糖调味","加入花生米翻炒均匀出锅"]}
]
第六部分:核心概念回顾
6.1 React 核心思想
组件化(Component-Based)
将 UI 拆分成独立、可复用的组件:
应用(大)↓ 拆分
Menu 组件(中)↓ 拆分
Recipe 组件(中)↓ 拆分
IngredientsList、Instructions(小)↓ 拆分
Ingredient(最小)
优势:
- 代码复用:Ingredient 组件可以在任何地方使用
- 职责单一:每个组件只做一件事
- 易于维护:修改某个组件不影响其他组件
- 团队协作:不同开发者可以并行开发不同组件
声明式编程(Declarative)
命令式(传统方式):
// 告诉计算机"怎么做"
const ul = document.createElement('ul');
data.forEach(item => {const li = document.createElement('li');li.textContent = item;ul.appendChild(li);
});
document.body.appendChild(ul);
声明式(React 方式):
// 告诉计算机"是什么"
function List({ data }) {return (<ul>{data.map(item => <li>{item}</li>)}</ul>);
}
React 自动处理 DOM 更新,你只需描述 UI 的最终状态。
单向数据流(Unidirectional Data Flow)
数据从父组件流向子组件,通过 props 传递:
Menu (recipes 数据)↓ props
Recipe (单个菜谱数据)↓ props
IngredientsList (配料数组)↓ props
Ingredient (单个配料)
优势:
- 数据流向清晰,易于追踪
- 便于调试,知道数据来自哪里
- 避免数据混乱,子组件不能修改 props
6.2 工程化工具链
Webpack 的作用
Webpack 是一个模块打包器,核心概念是依赖图:
入口文件 (src/index.js)↓ import Menu
Menu.js↓ import Recipe
Recipe.js↓ import IngredientsList, Instructions
IngredientsList.js↓ import Ingredient
Ingredient.js
Instructions.js↓ 打包
bundle.js (所有代码合并)
工作流程:
- 从
entry
入口文件开始 - 分析所有
import
语句 - 递归构建依赖图
- 使用对应的 loader 处理文件(JS、CSS、图片等)
- 将所有模块打包成一个或多个 bundle 文件
为什么需要打包?
- 浏览器不直接支持 ES6 模块
- 减少 HTTP 请求(多个文件→一个文件)
- 代码压缩优化
Babel 的作用
Babel 是一个JavaScript 编译器,负责语法转译:
JSX 转译:
// 源码
<div className="container"><h1>Hello</h1>
</div>// 转译后
React.createElement("div",{ className: "container" },React.createElement("h1", null, "Hello")
)
ES6+ 转译:
// 源码
const greeting = (name) => `Hello, ${name}`;// 转译后
var greeting = function(name) {return "Hello, " + name;
};
为什么需要转译?
- 浏览器不认识 JSX
- 旧浏览器不支持 ES6+ 语法
- 让你能使用最新的 JavaScript 特性
6.3 开发流程总结
完整的构建流程:
┌─────────────────┐
│ 编写源码 │ JSX + ES6+ + 模块化
│ src/index.js │
│ src/components/ │
└────────┬────────┘↓
┌─────────────────┐
│ Webpack 读取 │ 从 entry 开始构建依赖图
└────────┬────────┘↓
┌─────────────────┐
│ Babel 转译 │ JSX → JS, ES6+ → ES5
│ (babel-loader) │
└────────┬────────┘↓
┌─────────────────┐
│ 打包输出 │ 生成 bundle.js
│ dist/assets/ │
└────────┬────────┘↓
┌─────────────────┐
│ 浏览器加载 │ 执行 bundle.js
│ index.html │
└─────────────────┘
开发工作流:
- 开发阶段:
- 编写组件代码
- 运行
npm run dev
- 在浏览器中查看效果
- 修改代码 → 重新构建 → 刷新浏览器
- 部署阶段:
- 运行
npm run build
- 生成优化后的生产代码
- 将
dist/
目录部署到服务器
- 运行
附录:完整代码清单
A. 配置文件
package.json
{"name": "recipes-app","version": "1.0.0","scripts": {"dev": "webpack --mode development","build": "webpack --mode production"},"dependencies": {"react": "^18.2.0","react-dom": "^18.2.0"},"devDependencies": {"@babel/core": "^7.23.0","@babel/preset-env": "^7.23.0","@babel/preset-react": "^7.22.0","babel-loader": "^9.1.3","webpack": "^5.89.0","webpack-cli": "^5.1.4"}
}
.babelrc
{"presets": ["@babel/preset-env", "@babel/preset-react"]
}
webpack.config.js
const path = require('path');module.exports = {entry: './src/index.js',output: {path: path.join(__dirname, 'dist', 'assets'),filename: 'bundle.js'},module: {rules: [{test: /\.jsx?$/,exclude: /node_modules/,loader: 'babel-loader'}]},devtool: 'source-map'
};
dist/index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>React 菜谱应用</title><style>* {margin: 0;padding: 0;box-sizing: border-box;}body {font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Arial', sans-serif;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);min-height: 100vh;padding: 20px;}#root {max-width: 1200px;margin: 0 auto;}article > header {text-align: center;margin-bottom: 40px;}article > header h1 {color: white;font-size: 3rem;text-shadow: 2px 2px 4px rgba(0,0,0,0.3);margin-bottom: 10px;}.recipes {display: grid;grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));gap: 30px;}.recipe {background: white;border-radius: 16px;padding: 30px;box-shadow: 0 10px 30px rgba(0,0,0,0.2);transition: transform 0.3s ease;}.recipe:hover {transform: translateY(-5px);}.recipe h2 {color: #667eea;font-size: 2rem;margin-bottom: 20px;border-bottom: 3px solid #667eea;padding-bottom: 10px;}.ingredients {background: #f8f9fa;border-radius: 12px;padding: 20px;margin-bottom: 25px;border-left: 5px solid #667eea;}.ingredients li {list-style: none;padding: 8px 0;color: #495057;font-size: 1.05rem;}.ingredients li:before {content: "✓ ";color: #667eea;font-weight: bold;margin-right: 8px;}.instructions {margin-top: 20px;}.instructions h3 {color: #495057;font-size: 1.3rem;margin-bottom: 15px;}.instructions ol {padding-left: 25px;}.instructions li {margin: 12px 0;line-height: 1.6;color: #6c757d;font-size: 1.05rem;}.instructions li:before {font-weight: bold;color: #667eea;}</style>
</head>
<body><div id="root"></div><script src="assets/bundle.js"></script>
</body>
</html>
B. 数据文件
src/data/recipes.json
[{"name": "意大利面","ingredients": [{ "name": "意大利面", "amount": 200, "measurement": "克" },{ "name": "番茄酱", "amount": 100, "measurement": "克" },{ "name": "洋葱", "amount": 1, "measurement": "个" },{ "name": "大蒜", "amount": 3, "measurement": "瓣" },{ "name": "橄榄油", "amount": 2, "measurement": "汤匙" }],"steps": ["煮沸一锅盐水","加入意大利面煮 8-10 分钟","热锅加橄榄油,爆香蒜末和洋葱丁","加入番茄酱翻炒均匀","将煮好的面条加入酱汁中拌匀","装盘即可享用"]},{"name": "炒饭","ingredients": [{ "name": "米饭", "amount": 2, "measurement": "碗" },{ "name": "鸡蛋", "amount": 2, "measurement": "个" },{ "name": "胡萝卜", "amount": 50, "measurement": "克" },{ "name": "青豆", "amount": 30, "measurement": "克" },{ "name": "酱油", "amount": 1, "measurement": "汤匙" }],"steps": ["鸡蛋打散炒熟后盛出","胡萝卜切丁,与青豆一起炒熟","加入米饭翻炒","加入炒好的鸡蛋","倒入酱油调味","翻炒均匀后出锅"]},{"name": "番茄炒蛋","ingredients": [{ "name": "番茄", "amount": 3, "measurement": "个" },{ "name": "鸡蛋", "amount": 4, "measurement": "个" },{ "name": "白糖", "amount": 1, "measurement": "勺" },{ "name": "盐", "amount": 1, "measurement": "勺" },{ "name": "食用油", "amount": 2, "measurement": "汤匙" }],"steps": ["番茄切块,鸡蛋打散","热锅下油,炒鸡蛋至半熟盛出","再下油,炒番茄块至出汁","加入白糖和盐调味","倒入炒好的鸡蛋","翻炒均匀后出锅"]}
]
C. 组件文件
src/components/Ingredient.js
import React from 'react';function Ingredient({ amount, measurement, name }) {return (<li>{amount} {measurement} {name}</li>);
}export default Ingredient;
src/components/IngredientsList.js
import React from 'react';
import Ingredient from './Ingredient';function IngredientsList({ list }) {return (<ul className="ingredients">{list.map((ingredient, i) => (<Ingredient key={i} {...ingredient} />))}</ul>);
}export default IngredientsList;
src/components/Instructions.js
import React from 'react';function Instructions({ title, steps }) {return (<section className="instructions"><h3>{title}</h3><ol>{steps.map((step, i) => (<li key={i}>{step}</li>))}</ol></section>);
}export default Instructions;
src/components/Recipe.js
import React from 'react';
import IngredientsList from './IngredientsList';
import Instructions from './Instructions';function Recipe({ name, ingredients, steps }) {return (<section className="recipe"><h2>{name}</h2><IngredientsList list={ingredients} /><Instructions title="烹饪步骤" steps={steps} /></section>);
}export default Recipe;
src/components/Menu.js
import React from 'react';
import Recipe from './Recipe';function Menu({ recipes }) {return (<article><header><h1>美味菜谱</h1></header><div className="recipes">{recipes.map((recipe, i) => (<Recipe key={i} {...recipe} />))}</div></article>);
}export default Menu;
D. 入口文件
src/index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import Menu from './components/Menu';
import data from './data/recipes.json';const root = createRoot(document.getElementById('root'));
root.render(<Menu recipes={data} />);
总结
通过本教程,你已经学会了:
✅ React 核心概念
- 组件化开发思维
- JSX 语法的使用
- Props 数据传递
- 声明式编程范式
✅ 工程化工具配置
- Webpack 打包配置
- Babel 转译配置
- 开发与生产构建
- Source Map 调试
✅ 实战开发能力
- 从需求到组件设计
- 自底向上构建组件树
- 数据驱动视图更新
- 组件的创建、组合、复用
✅ 完整开发流程
需求分析 → 数据设计 → 组件拆分 → 编写代码 → 构建打包 → 浏览器运行
快速命令参考
# 创建项目
mkdir recipes-app && cd recipes-app# 安装依赖
npm init -y
npm install react react-dom
npm install -D webpack webpack-cli babel-loader @babel/core @babel/preset-env @babel/preset-react# 开发构建
npm run dev# 生产构建
npm run build# 查看效果
# 打开 dist/index.html
项目结构总览
recipes-app/
├── package.json # 项目配置和依赖
├── .babelrc # Babel 转译配置
├── webpack.config.js # Webpack 打包配置
├── dist/ # 构建输出目录
│ ├── index.html # 应用入口页面
│ └── assets/
│ ├── bundle.js # 打包后的代码
│ ├── bundle.js.map # Source Map
│ └── bundle.js.LICENSE # 第三方库许可
└── src/ # 源代码目录├── index.js # 应用入口文件├── components/ # 组件目录│ ├── Menu.js # 菜单组件(根组件)│ ├── Recipe.js # 菜谱组件│ ├── IngredientsList.js # 配料列表组件│ ├── Ingredient.js # 单个配料组件│ └── Instructions.js # 步骤说明组件└── data/ # 数据目录└── recipes.json # 菜谱数据
恭喜你完成了第一个 React 应用!现在你可以:
- 扩展功能:添加搜索、筛选、收藏等功能
- 学习状态管理:使用
useState
和useEffect
Hook - 添加路由:使用 React Router 实现多页面
- 连接后端:通过 API 获取数据
- 学习样式方案:CSS Modules、Styled Components、Tailwind CSS
React 的学习之旅才刚刚开始,继续探索吧!🚀