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 或更高版本)。验证安装:
bash
node -v
npm -v
创建项目目录
bash
mkdir recipes-app
cd recipes-app
初始化项目并安装依赖
bash
# 初始化 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):
bash
mkdir dist
mkdir src
mkdir src\components
mkdir src\data
type 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:
bash
mkdir -p dist src/components src/data
touch .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):
bash
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
文件,内容如下:
json
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
配置说明
@babel/preset-env
:将 ES6+ 语法(箭头函数、解构、模板字符串等)转译为 ES5@babel/preset-react
:将 JSX 语法转译为React.createElement()
函数调用
Babel 的作用
Babel 是一个 JavaScript 编译器,它让你能使用最新的语法编写代码,然后自动转换成浏览器能理解的旧版本代码。
转译示例:
jsx
// 你写的代码
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
文件:
javascript
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
字段:
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"
}
}
脚本说明
npm run dev
:开发模式构建(代码未压缩,构建快)npm run build
:生产模式构建(代码压缩,体积小)
2.4 HTML 入口页面(dist/index.html)
创建 dist/index.html
文件:
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 基本规则:
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
- 单向流动:数据从父组件流向子组件
- 任意类型:可以传递字符串、数字、对象、数组、函数等
jsx
// 父组件传递 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
文件:
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
:
javascript
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
:
javascript
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
:
javascript
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
:
javascript
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
:
javascript
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
:
javascript
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
:菜谱数据
-
创建根节点:
javascriptconst root = createRoot(document.getElementById('root'));
获取 HTML 中的
<div id="root">
元素并创建 React 根节点 -
渲染应用:
javascriptroot.render(<Menu recipes={data} />);
将 Menu 组件渲染到根节点,并传入菜谱数据
第四部分:构建与运行应用
4.1 开发模式构建
在项目根目录运行:
bash
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
:
javascript
!function(e){var t={};function n(r){if(t[r])return...
有了 Source Map,你可以直接调试源代码:
javascript
function Ingredient({ amount, measurement, name }) {
return <li>{amount} {measurement} {name}</li>;
}
4.3 生产模式构建
准备部署时,使用生产模式:
bash
npm run build
与开发模式的区别:
特性 | 开发模式 | 生产模式 |
---|---|---|
代码压缩 | ❌ | ✅ |
混淆变量名 | ❌ | ✅ |
移除注释 | ❌ | ✅ |
文件大小 | ~1.2 MB | ~150 KB |
构建速度 | 快 | 慢 |
调试体验 | 好 | 差 |
适用场景 | 本地开发 | 线上部署 |
生产模式的代码示例:
javascript
// 开发模式(可读)
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
- 在数组中添加新对象:
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
:
javascript
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
:
javascript
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
字段:
json
[
{
"name": "意大利面",
"rating": 5,
"ingredients": [...],
"steps": [...]
},
{
"name": "炒饭",
"rating": 4,
"ingredients": [...],
"steps": [...]
},
{
"name": "番茄炒蛋",
"rating": 5,
"ingredients": [...],
"steps": [...]
}
]
步骤 4:重新构建并查看
bash
npm run dev
刷新浏览器,每个菜谱下方会显示星级评分。
学习目标:
- 创建新组件的完整流程
- 在父组件中引入和使用子组件
- 通过 props 传递数据
- 更新数据结构以支持新功能
练习 3:添加样式优化 ⭐
任务描述:
修改 CSS 样式,让菜谱卡片更加美观。
步骤 1:修改 dist/index.html 中的样式
在 <style>
标签中添加或修改:
css
/* 为评分组件添加样式 */
.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 组件
javascript
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
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)
命令式(传统方式):
javascript
// 告诉计算机"怎么做"
const ul = document.createElement('ul');
data.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
ul.appendChild(li);
});
document.body.appendChild(ul);
声明式(React 方式):
javascript
// 告诉计算机"是什么"
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 转译:
jsx
// 源码
<div className="container">
<h1>Hello</h1>
</div>
// 转译后
React.createElement(
"div",
{ className: "container" },
React.createElement("h1", null, "Hello")
)
ES6+ 转译:
javascript
// 源码
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
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
json
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
webpack.config.js
javascript
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
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
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
javascript
import React from 'react';
function Ingredient({ amount, measurement, name }) {
return (
<li>
{amount} {measurement} {name}
</li>
);
}
export default Ingredient;
src/components/IngredientsList.js
javascript
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
javascript
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
javascript
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
javascript
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
javascript
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 调试
✅ 实战开发能力
- 从需求到组件设计
- 自底向上构建组件树
- 数据驱动视图更新
- 组件的创建、组合、复用
✅ 完整开发流程
需求分析 → 数据设计 → 组件拆分 → 编写代码 → 构建打包 → 浏览器运行
快速命令参考
bash
# 创建项目
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 的学习之旅才刚刚开始,继续探索吧!🚀