React 快速入门:菜谱应用实战教程

React 快速入门:菜谱应用实战教程

第一部分:React 开发准备

1.1 React 是什么?

React 是 Facebook 开发的一个用于构建用户界面的 JavaScript 库。它具有三个核心特点:

组件化(Component-Based)

将复杂的 UI 拆分成独立、可复用的组件,就像搭积木一样构建应用。

声明式编程(Declarative)

你只需要描述 UI 应该"是什么样子",React 会自动处理 DOM 更新。

单向数据流(Unidirectional Data Flow)

数据从父组件流向子组件,让数据流动可预测、易调试。

为什么需要构建工具?

直接在浏览器中使用 React 会遇到三个问题:

  1. JSX 语法 :浏览器不认识 <div>Hello</div> 这样的 JSX 代码
  2. 模块化 :无法使用 import/export 组织代码
  3. 现代 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 是一个模块打包器。它的工作流程:

  1. entry 入口文件开始

  2. 分析所有的 import 语句,构建依赖图

  3. 使用对应的 loader 处理每种类型的文件

  4. 将所有模块打包成一个或多个 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 的特点:

  1. 只读:子组件不能修改 props
  2. 单向流动:数据从父组件流向子组件
  3. 任意类型:可以传递字符串、数字、对象、数组、函数等
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> 子组件

创建 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} />);

代码说明:

  1. 导入依赖

    • React:React 核心库
    • createRoot:React 18 的新 API,用于创建根节点
    • Menu:我们的根组件
    • data:菜谱数据
  2. 创建根节点

    javascript 复制代码
    const root = createRoot(document.getElementById('root'));

    获取 HTML 中的 <div id="root"> 元素并创建 React 根节点

  3. 渲染应用

    javascript 复制代码
    root.render(<Menu recipes={data} />);

    将 Menu 组件渲染到根节点,并传入菜谱数据


第四部分:构建与运行应用

4.1 开发模式构建

在项目根目录运行:

bash 复制代码
npm run dev

构建过程:

  1. Webpack 读取 src/index.js 入口文件
  2. 分析所有 import 语句,构建依赖图
  3. 使用 babel-loader 转译 JSX 和 ES6+ 语法
  4. 打包所有模块到 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,你会看到:

  • 页面标题:"美味菜谱"
  • 三道菜谱卡片(意大利面、炒饭、番茄炒蛋)
  • 每个卡片包含配料和步骤

使用开发者工具调试:

  1. F12 打开开发者工具
  2. 切换到 Sources 面板
  3. 在左侧文件树中找到 webpack://src/ 目录
  4. 可以看到你的原始源代码(这就是 Source Map 的作用)
  5. 设置断点并调试

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 中添加一道你喜欢的菜,重新构建并查看效果。

操作步骤:

  1. 打开 src/data/recipes.json
  2. 在数组中添加新对象:
json 复制代码
{
  "name": "宫保鸡丁",
  "ingredients": [
    { "name": "鸡胸肉", "amount": 300, "measurement": "克" },
    { "name": "花生米", "amount": 100, "measurement": "克" },
    { "name": "干辣椒", "amount": 10, "measurement": "个" },
    { "name": "花椒", "amount": 1, "measurement": "勺" },
    { "name": "酱油", "amount": 2, "measurement": "汤匙" }
  ],
  "steps": [
    "鸡肉切丁,用料酒和淀粉腌制",
    "花生米炸至金黄盛出",
    "热锅下油,爆香干辣椒和花椒",
    "下鸡丁快速翻炒至变色",
    "加入酱油和白糖调味",
    "加入花生米翻炒均匀出锅"
  ]
}
  1. 保存文件
  2. 重新构建:npm run dev
  3. 刷新浏览器,看到新增的宫保鸡丁菜谱

学习目标:

  • 理解数据驱动视图的概念
  • 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 (所有代码合并)

工作流程:

  1. entry 入口文件开始
  2. 分析所有 import 语句
  3. 递归构建依赖图
  4. 使用对应的 loader 处理文件(JS、CSS、图片等)
  5. 将所有模块打包成一个或多个 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      │
└─────────────────┘

开发工作流:

  1. 开发阶段:
    • 编写组件代码
    • 运行 npm run dev
    • 在浏览器中查看效果
    • 修改代码 → 重新构建 → 刷新浏览器
  2. 部署阶段:
    • 运行 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 应用!现在你可以:

  1. 扩展功能:添加搜索、筛选、收藏等功能
  2. 学习状态管理 :使用 useStateuseEffect Hook
  3. 添加路由:使用 React Router 实现多页面
  4. 连接后端:通过 API 获取数据
  5. 学习样式方案:CSS Modules、Styled Components、Tailwind CSS

React 的学习之旅才刚刚开始,继续探索吧!🚀

相关推荐
我只会写Bug啊1 小时前
Vue文件预览终极方案:PNG/EXCEL/PDF/DOCX/OFD等10+格式一键渲染,开源即用!
前端·vue.js·pdf·excel·预览
扯蛋4383 小时前
LangChain的学习之路( 一 )
前端·langchain·mcp
Mr.Jessy3 小时前
Web APIs学习第一天:获取 DOM 对象
开发语言·前端·javascript·学习·html
ConardLi4 小时前
Easy Dataset 已经突破 11.5K Star,这次又带来多项功能更新!
前端·javascript·后端
冴羽4 小时前
10 个被严重低估的 JS 特性,直接少写 500 行代码
前端·javascript·性能优化
rising start4 小时前
四、CSS选择器(续)和三大特性
前端·css
一 乐4 小时前
高校后勤报修系统|物业管理|基于SprinBoot+vue的高校后勤报修系统(源码+数据库+文档)
java·前端·javascript·数据库·vue.js·毕设
爱喝水的小周4 小时前
《UniApp 页面配置文件pages.json》
前端·uni-app·json
mapbar_front5 小时前
React中useContext的基本使用和原理解析
前端·react.js
贪婪的君子5 小时前
【每日一面】实现一个深拷贝函数
前端·js