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 的学习之旅才刚刚开始,继续探索吧!🚀

相关推荐
西洼工作室3 小时前
Vue CLI为何不显示webpack配置
前端·vue.js·webpack
黄智勇3 小时前
xlsx-handlebars 一个用于处理 XLSX 文件 Handlebars 模板的 Rust 库,支持多平台使
前端
brzhang4 小时前
为什么 OpenAI 不让 LLM 生成 UI?深度解析 OpenAI Apps SDK 背后的新一代交互范式
前端·后端·架构
brzhang5 小时前
OpenAI Apps SDK ,一个好的 App,不是让用户知道它该怎么用,而是让用户自然地知道自己在做什么。
前端·后端·架构
水冗水孚6 小时前
React中使用map+area标签实现img图片特定区域标记功能(可用Photoshop精准拾取对应点位)
react.js·html·photoshop
井柏然6 小时前
前端工程化—实战npm包深入理解 external 及实例唯一性
前端·javascript·前端工程化
IT_陈寒6 小时前
Redis 高性能缓存设计:7个核心优化策略让你的QPS提升300%
前端·人工智能·后端
井柏然7 小时前
从 npm 包实战深入理解 external 及实例唯一性
前端·javascript·前端工程化
羊锦磊7 小时前
[ vue 前端框架 ] 基本用法和vue.cli脚手架搭建
前端·vue.js·前端框架