阅读视图

发现新文章,点击刷新页面。

React学习:通过TodoList,完整理解组件通信

React 组件通信从零到精通:用一个完整 Todo List 项目彻底搞懂父子、子父与兄弟通信

最近学习了React中完整点的组件通信,包括父传子,子传父,兄弟组件通信。概念听起来简单——props 向下传、回调向上传、状态提升——但真正写代码时,总觉得迷迷糊糊。于是我通过一个 Todo List 功能讲解,结合真实代码,一点点拆解了组件通信的每一个细节。

从最基础的父传子开始,到子传父的回调机制,再到兄弟组件的状态提升,最后深入到大家经常问的“为什么 onChange 要包箭头函数”这类细节。全程基于一个可运行的 Todo List 项目,代码全部贴出,讲解尽量通俗、细致,适合初学者反复阅读,也适合有经验的同学复习巩固。

项目整体结构:经典的状态提升模式

先看整个项目的组件树:

App(父组件)
├── TodoInput(添加输入框)
├── TodoList(列表展示 + 删除 + 切换完成状态)
└── TodoStats(统计 + 清除已完成任务)

核心数据 todos 数组只在 App 组件中用 useState 管理。三个子组件都不直接持有或修改 todos,而是通过 props 接收数据和修改方法。

这就是 React 官方推荐的状态提升(Lifting State Up) :把多个组件需要共享的状态提升到它们最近的共同父组件中统一管理。

这样做的好处:

  • 数据有单一真相来源(single source of truth)
  • 避免数据不同步的 bug
  • 逻辑集中,容易维护

一、父组件 → 子组件:单向数据流与 Props 传递

React 的核心原则是单向数据流:数据只能从父组件通过 props 向下传递,子组件不能直接修改父组件的数据。

在 App.jsx 中,我们把 todos 数据、统计数字、各种操作函数都通过 props 传给了子组件:

// App.jsx 关键片段
<TodoInput onAdd={addTodo} />
<TodoList
  todos={todos}
  onDelete={deleteTodo}
  onToggle={toggleTodo}
/>
<TodoStats
  total={todos.length}
  active={activeCount}
  completed={completedCount}
  onClearCompleted={onClearCompleted}
/>

子组件只需要接收 props,使用即可,完全不需要关心数据是怎么来的、怎么改的。

关于父传子的单项数据可以看我上一篇文章# React 学习:父传子的单项数据流——props

二、子组件 → 父组件:回调函数上报事件(深度详解)

子组件如何影响父组件的状态?这正是 React 组件通信中最核心、最容易混淆的部分。

很多人误以为“子传父”是子组件把数据直接塞给父组件,其实完全不是!

React 中“子传父”的正确姿势是:父组件提前定义好一个回调函数,通过 props 传给子组件;子组件在合适时机调用这个函数,把必要的信息“上报”给父组件,由父组件决定如何更新自己的状态。

这套机制在我们的三个子组件中都有体现,下面结合代码一步一步彻底拆解它的实现原理。

子传父的完整四步流程
  1. 父组件定义回调函数(负责真正修改状态)
  2. 父组件通过 props 把回调函数传给子组件
  3. 子组件接收回调,并在事件触发时调用它(上报数据或事件)
  4. 回调执行 → 父组件状态更新 → 触发重新渲染 → 新数据通过 props 再次向下传递

下面以“添加新 Todo”为例,逐行代码演示这个闭环。

示例 1:TodoInput 添加新事项(子传父经典案例)

步骤 1:父组件定义回调函数 addTodo

// App.jsx
const addTodo = (text) => {
  setTodos(prev => [...prev, {
    id: Date.now(),
    text,
    completed: false
  }]);
};

这个函数接收一个 text 参数,负责把新事项添加到状态中。

步骤 2:父组件通过 props 传递回调

<TodoInput onAdd={addTodo} />  // 注意:传的是函数本身,不是调用

步骤 3:子组件接收并在提交时调用

// TodoInput.jsx
const TodoInput = ({ onAdd }) => {  // 解构接收
  const [inputValue, setInputValue] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    const text = inputValue.trim();
    if (!text) return;

    onAdd(text);          // ← 关键!上报用户输入的文本
    setInputValue("");    // 清空输入框
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button type="submit">Add</button>
    </form>
  );
};

步骤 4:闭环完成

  • 用户输入并提交 → onAdd(text) 被调用 → 执行父组件的 addTodo
  • setTodos 更新状态 → App 重新渲染 → 新 todos 通过 props 传给 TodoList → 列表自动显示新项
示例2. TodoList:删除和切换完成状态
// TodoList.jsx
const TodoList = ({ todos, onDelete, onToggle }) => {
  return (
    <ul className="todo-list">
      {todos.length === 0 ? (
        <li className="empty">No todos yet!</li>
      ) : (
        todos.map((todo) => (
          <li
            key={todo.id}
            className={todo.completed ? "completed" : ""}
          >
            <label>
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => onToggle(todo.id)}   // 上报 id
              />
              <span>{todo.text}</span>
            </label>
            <button
              className="delete-btn"
              onClick={() => onDelete(todo.id)}      // 上报 id
            >
              ×
            </button>
          </li>
        ))
      )}
    </ul>
  );
};

export default TodoList;

关键点:

  • 复选框也是受控组件:checked 值来自 props 中的 todo.completed
  • 点击复选框或删除按钮时,分别调用 onToggle(todo.id) 和 onDelete(todo.id),把当前事项的 id 上报给父组件
  • 父组件根据 id 找到对应项并更新状态

父组件中的实现:

const deleteTodo = (id) => {
  setTodos(prev => prev.filter(todo => todo.id !== id));
};

const toggleTodo = (id) => {
  setTodos(prev => prev.map(todo =>
    todo.id === id 
      ? { ...todo, completed: !todo.completed }
      : todo
  ));
};
示例3. TodoStats:清除已完成事项
// TodoStats.jsx
const TodoStats = ({ total, active, completed, onClearCompleted }) => {
  return (
    <div className="todo-stats">
      <p>
        Total: {total} | Active: {active} | Completed: {completed}
      </p>
      {completed > 0 && (
        <button onClick={onClearCompleted} className="clear-btn">
          Clear Completed
        </button>
      )}
    </div>
  );
};

export default TodoStats;

关键点:

  • 统计数字由父组件提前计算好传下来,避免子组件重复计算
  • 点击清除按钮时调用 onClearCompleted() 上报事件

父组件实现:

const onClearCompleted = () => {
  setTodos(prev => prev.filter(todo => !todo.completed));
};
子传父的核心本质总结
  • 不是子组件“给”父组件数据,而是子组件“通知”父组件:“嘿,发生了一件事(用户点了添加/删除/切换),需要的参数我给你,你自己看着办。”
  • 所有状态修改权永远掌握在父组件手里,子组件只有“上报权”。
  • 这种“事件向上冒泡、数据向下流动”的模式,正是 React 单向数据流的完美体现。

掌握了这个机制,你就真正理解了为什么 React 说“数据流是单向的”,却依然能轻松实现复杂的交互。

完整子组件代码(带详细注释)

TodoInput.jsx

import { useState } from "react";

const TodoInput = ({ onAdd }) => {
  const [inputValue, setInputValue] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    const text = inputValue.trim();
    if (!text) return;

    onAdd(text);          // 子 → 父:上报新事项文本
    setInputValue("");
  };

  return (
    <form className="todo-input" onSubmit={handleSubmit}>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="输入事项后按回车或点击添加"
      />
      <button type="submit">Add</button>
    </form>
  );
};

export default TodoInput;

TodoList.jsx

const TodoList = ({ todos, onDelete, onToggle }) => {
  return (
    <ul className="todo-list">
      {todos.length === 0 ? (
        <li className="empty">No todos yet! 快去添加一个吧~</li>
      ) : (
        todos.map((todo) => (
          <li key={todo.id} className={todo.completed ? "completed" : ""}>
            <label>
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => onToggle(todo.id)}   // 子 → 父:上报要切换的 id
              />
              <span>{todo.text}</span>
            </label>
            <button
              className="delete-btn"
              onClick={() => onDelete(todo.id)}      // 子 → 父:上报要删除的 id
            >
              ×
            </button>
          </li>
        ))
      )}
    </ul>
  );
};

export default TodoList;

TodoStats.jsx

const TodoStats = ({ total, active, completed, onClearCompleted }) => {
  return (
    <div className="todo-stats">
      <p>Total: {total} | Active: {active} | Completed: {completed}</p>
      {completed > 0 && (
        <button onClick={onClearCompleted} className="clear-btn">
          Clear Completed
        </button>
      )}
    </div>
  );
};

export default TodoStats;

三、为什么 onChange={() => onToggle(todo.id)} 必须包箭头函数?

这是初学者最容易踩的坑之一,我们详细拆解。

错误写法 1:直接调用
onChange={onToggle(todo.id)}  // 灾难性错误!

渲染时就会立即执行 onToggle(todo.id),导致:

  • 页面加载瞬间所有任务状态翻转
  • 可能引发无限渲染循环

SnowShot_Video_2025-12-24_14-57-17.gif

错误写法 2:只传函数不传参

jsx

onChange={onToggle}

React 会把 event 对象传给 onToggle,但我们需要的是 id,导致切换失败。

正确写法:箭头函数包裹
onChange={() => onToggle(todo.id)}

只有用户真正点击时才执行,并正确传递 id。

四、完整 App 组件:数据管理中心

import { useEffect, useState } from "react";
import "./styles/app.styl";
import TodoList from "./components/TodoList";
import TodoInput from "./components/TodoInput";
import TodoStats from "./components/TodoStats";

function App() {
  // 子组件共享的数据状态
  const [todos, setTodos] = useState(() => {
    // 高级用法
    const saved = localStorage.getItem("todos");
    return saved ? JSON.parse(saved) : [];
  });
  // 子组件修改数据的方法
  const addTodo = (text) => {
    setTodos([
      ...todos,
      {
        id: Date.now(), // 时间戳
        text,
        completed: false,
      },
    ]);
  };

  const deleteTodo = (id) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };
  const toggleTodo = (id) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id
          ? {
              ...todo,
              completed: !todo.completed,
            }
          : todo
      )
    );
  };

  const activeCount = todos.filter((todo) => !todo.completed).length;
  const completedCount = todos.filter((todo) => todo.completed).length;
  const onClearCompleted = () => {
    setTodos(todos.filter((todo) => !todo.completed));
  };

  useEffect(() => {
    localStorage.setItem("todos", JSON.stringify(todos));
  }, [todos]);

  return (
    <div className="todo-app">
      <h1>My Todo List</h1>
      {/* 自定义事件 */}
      <TodoInput onAdd={addTodo} />
      <TodoList todos={todos} onDelete={deleteTodo} onToggle={toggleTodo} />
      <TodoStats
        total={todos.length}
        active={activeCount}
        completed={completedCount}
        onClearCompleted={onClearCompleted}
      />
    </div>
  );
}

export default App;

五、最终效果展示:

初始状态

image.png

添加示例

image.png

勾选完成

image.png

六、总结:掌握这套模式,就掌握了 90% 的组件通信

通过这个 Todo List,我们完整实践了:

  1. 父传子:props 单向传递
  2. 子传父:回调函数上报事件(深度掌握)
  3. 兄弟通信:状态提升
  4. 常见坑避免:事件处理正确写法

这套模式简单、可靠、可预测,是 React 项目的基石。

当项目更大时,再学习 Context 或状态管理库。但请记住:万丈高楼平地起,先把这套基础打牢

希望这篇文章能帮你彻底弄懂 React 组件通信的本质。下次写代码时,遇到数据流动问题,先问自己:“这个状态该谁管?回调要不要传参?箭头函数包好了吗?”

React 组件通讯全攻略:拒绝 "Props" 焦虑,掌握数据流动的艺术

前言

React 的开发旅程中,组件就像是乐高积木,我们把它们一个个搭起来构建出复杂的页面。但光搭起来还不够,这些积木之间必须有“电流”流通——这就是数据。
React 的设计哲学是单向数据流,就像瀑布一样,水流默认只能从高处流向低处。但在实际业务中,我们经常需要逆流而上,或者在两个平行的水池间交换水流。今天我们就来盘点 React 中最主流的四种“引水”方式,打通你的组件经脉。


1. 父传子:顺流而下的 Props

这是 React 中最自然、最基础的通信方式。想象一下,父亲给孩子零花钱,父亲(父组件)只需要要把钱(数据)递过去,孩子(子组件)伸手接着就行。
在代码层面,父组件通过在子组件标签上写上自定义属性(msg)来传递数据。

import Child from "./Child"
export default function Parent() {
  const state = {
    name: '小饶'
  }
  
  return (
    <div>
      <h2>父组件</h2>
      {/* 父亲把 '小饶' 这个名字打包进 msg 属性传给孩子 */}
      <Child msg={state.name} />
    </div>
  )
}

孩子组件这边,所有接收到的礼物都装在一个叫 props 的盒子里。不过要注意,Props 是只读的——这意味着孩子只能使用这些数据,不能直接修改它。就像孩子不能自己修改父亲银行卡的余额一样,如果要改,必须请求父亲操作。

export default function Child(props) {
  // 打开盒子看看收到了什么
  console.log(props);
  
  return (
    <h3>子组件 -- {props.msg}</h3>
  )
}

2. 子传父:给孩子一个“遥控器”

既然水流默认向下,那子组件想要改变父组件的数据该怎么办?比如,孩子想告诉父亲:“我考了 100 分,请更新一下你的心情状态”。
这时候,父组件需要提前准备一个“遥控器”——也就是一个函数。父组件把这个函数通过 props 传给子组件,当子组件需要通信时,就按下这个遥控器(调用函数),并把数据作为参数传回去。 **父组件: 准备好 getNum 函数,用来接收数据并更新自己的 count。 **

import Child from "./Child"
import { useState } from 'react'

export default function Parent() {
  let [count, setCount] = useState(1)  

  // 这就是那个“遥控器”函数
  const getNum = (n) => {
    setCount(n)
  }

  return (
    <div>
      <h2>父组件二 -- {count}</h2>
      {/* 把遥控器交给孩子 */}
      <Child getNum={getNum}></Child>
    </div>
  )
}

子组件: 在合适的时机(比如点击按钮时),通过 props 拿到并按下这个“遥控器”。

export default function Child(props) {
  
  const state = {
    num: 100
  }

  function send() {
    // 此时调用的是父组件里的函数,把 100 传了回去
    props.getNum(state.num)
  }

  return (
    <div>
      <h3>子组件二</h3>
      <button onClick={send}>发送</button>
    </div>
  )
}

3. 兄弟组件:找个共同的“家长”

兄弟组件之间没有直接的连线,就像你和你的表弟住在不同的屋子里,想聊天得通过大厅里的长辈传话。 这种模式在 React 中通常被称为状态提升。既然 Brother1 想要给 Brother2 传值,那我们就把这个值保存在他们共同的父亲身上。

  • Brother1 -> Parent:Brother1 先把数据传给父亲(利用上面的子传父技巧)。
  • Parent -> Brother2:父亲拿到数据后,更新自己的状态,再把这个新状态顺手传给 Brother2(利用父传子技巧)。

父组件(中间枢纽):

import { useState } from "react"
import Child1 from "./Child1"
import Child2 from "./Child2"

export default function Parent() {
  let [message, setMessage] = useState()

  // 接收老大传来的消息
  const getMsg = (msg) => {
    setMessage(msg)
  }

  return (
    <div>
      <h2> 父组件三 </h2>
      {/* 接收者:从 Child1 收信 */}
      <Child1 getMsg={getMsg} />
      {/* 发送者:把信转交给 Child2 */}
      <Child2 message={message} />
    </div>
  )
}

Child1(消息发送方):

export default function Child1(props) {
  const state = {
    msg: '1 中的数据'
  }

  function send() {
    props.getMsg(state.msg)
  }
  
  return (
    <div>
      <h3>子组件1</h3>
      <button onClick={send}>1</button>
    </div>
  )
}

Child2(消息接收方):

export default function Child2(props) {
  return (
    <div>
      {/* 坐等父亲把兄弟的消息送过来 */}
      <h3>子组件2 --- {props.message}</h3>
    </div>
  )
}

4. 跨代组件通信:Context 传送门

如果组件层级很深,比如“爷爷 -> 爸爸 -> 儿子 -> 孙子”,如果还用 Props 一层层传,那中间的爸爸和儿子就成了无辜的“搬运工”,代码会变得非常臃肿麻烦。
为了解决这个问题,React 提供了一个 Context(上下文)机制。这就像在家族里设立了一个“广播站”,爷爷在顶层广播,底下的任何一代子孙,只要想听,就可以直接接收信号,完全不需要中间人转手。
爷组件(数据源头):
我们需要先 createContext 创建一个信号塔,然后用 <Context.Provider> 把所有后代包起来,value 就是我们要广播的数据。

import Parent from "./Parent"
import { createContext } from 'react'

export const Context = createContext()  // 1. 建立信号塔

export default function Grand() {

  return (
    <div>
      <h2> 爷组件 </h2>
      {/* 2. 发射信号,内容是 value 中的数据 */}
      <Context.Provider value= {'爷组件的数据'}>
        <Parent/>
      </Context.Provider>
    </div>
  )
}

父组件(路人甲):
你看,父组件完全不需要碰这些数据,它只需要安静地渲染它的子组件即可。

import Child from "./Child"
export default function Parent() {

  return (
    <div>
      <h3>父组件</h3>
      <Child></Child>
    </div>
  )
}

孙子组件(数据接收者):
孙子组件不需要管它离爷爷隔了多少代,直接用 useContext 这个钩子函数,就能连上信号塔拿到数据。

import { useContext } from 'react'
import { Context } from './Grand'  // 3. 引入信号塔定义

export default function Child() {
  // 4. 接收信号
  const msg = useContext(Context)

  return (
    <div>
      <h4>孙子组件 --- {msg}</h4>
    </div>
  )
}

结语

组件通信是 React 开发中最基本也最重要的内功。

  • 简单的父子关系,PropsCallback 是最轻量的选择;
  • 兄弟组件,记得找共同的父级帮忙周转;
  • 当层级太深感到繁琐时,Context 就是你的救星。

掌握了这四招,你就能从容应对绝大多数的组件交互场景,让数据在你的应用中流动得井井有条。

从零搭一个 Vue 小家:用 Vite + 路由轻松入门现代前端开发

从零开始,轻松走进 Vue 的世界:一个“全家桶”小项目的搭建之旅

如果你刚刚接触前端开发,听到“Vue”、“Vite”、“路由”这些词时是不是有点懵?别担心!我们可以把写代码想象成搭积木、装修房子、甚至安排一场家庭旅行。今天,我们就通过一个名为 all-vue 的小项目,带你一步步理解现代 Vue 应用是怎么“搭起来”的。


🏠 第一步:选好地基——用 Vite 快速建项目

什么是vite?

Vite(法语,意为“快”)是一个由 Vue.js 作者 尤雨溪(Evan You) 主导开发的现代化前端构建工具。它旨在解决传统打包工具(如 Webpack)在开发阶段启动慢、热更新(HMR)延迟高等问题,提供极速的开发体验。

想象你要盖一栋房子。传统方式可能要先打地基、砌砖、铺电线……繁琐又耗时。而 Vite 就像一位超级高效的建筑承包商,你只要说一句:“我要一个 Vue 房子”,它立刻给你搭好框架,连水电都通好了!

在终端里运行:

npm init vite@latest all-vue -- --template vue

几秒钟后,你就得到了一个结构清晰的项目目录。其中最关键的是:

  • index.html:这是你房子的“大门”,浏览器一打开就看到它。
  • src/main.js:这是整栋房子的“总开关”,负责启动整个应用。
  • src/App.vue:这是“客厅”,所有房间(页面)都要从这里进出。

Vite 的优势在于——修改代码后,浏览器几乎瞬间刷新,就像你换了个沙发,家人马上就能坐上去试舒服不舒服。


🏗️ 第二步:认识整栋楼——项目结构概览

运行 npm init vite@latest all-vue -- --template vue 后,你会得到这样一栋“数字公寓”:

项目结构简略预览:

/all-vue
├── public/            # 公共资源(如 logo.png)
├── src/
│   ├── assets/        # 图片、字体等静态资源
│   ├── components/    # 可复用的小部件(按钮、卡片等)
│   ├── views/         # 独立页面(首页、关于页等)
|   |     |—— About.vue # 关于页面的Vue组件
|   |     |—— Home.vue # 主页的vue组件
│   ├── router/        # 室内导航系统
|   |     |—— index.js # 路由总控
│   ├── App.vue        # 中央控制台(客厅)
│   └── main.js        # 智能钥匙
├── index.html         # 入户大门
├── package.json       # 公寓的“住户手册 + 装修清单”
└── vite.config.js     # 建筑规范说明书

其中,package.json 就像这栋楼的住户手册 + 装修材料清单。打开它,你会看到:

{
  "name": "all-vue",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.4.0",
    "vue-router": "^4.3.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.0",
    "vite": "^5.0.0"
  }
}
  • dependencies:这是“入住必需品”,比如 Vue 框架本身、路由系统——没有它们,房子没法正常运转;
  • devDependencies:这是“装修工具包”,只在开发时用(比如 Vite 构建工具),住户入住后就不需要了;
  • scripts:这是“快捷指令”,比如 npm run dev 就是“启动预览模式”,npm run build 是“打包交付”。

有了这份清单,任何开发者都能一键还原你的整套环境——就像照着宜家说明书组装家具一样可靠。


🚪 第三步:认识“大门”——index.html 的两个秘密

虽然现代 Vue 应用的逻辑几乎全在 JavaScript 和 .vue 文件里,但一切的起点,其实是这个看似简单的 index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>all-vue</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>

别小看这十几行代码,它藏着两个关键设计:

🔌 1. <div id="app"></div>:Vue 的“插座”

你可以把它想象成墙上预留的一个智能插座面板。它本身空无一物,但一旦通电(Vue 应用启动),就会自动“投影”出整个用户界面。

main.js 中,我们这样写:

createApp(App).mount('#app')

这句话的意思就是:“请把 App.vue 这个‘客厅’的内容,投射到 id 为 app 的那个插座上。”
没有这个插座,Vue 再厉害也无处施展;有了它,动态内容才能在静态 HTML 中生根发芽。

⚡ 2. <script type="module" src="/src/main.js"></script>:原生 ES 模块的魔法

注意这里的 type="module"。这是现代浏览器支持的一种原生模块加载方式。传统脚本是“一股脑全塞进来”,而模块化脚本则像快递包裹——每个文件独立打包,按需引用,互不干扰。

Vite 正是利用了这一特性,无需打包即可直接在浏览器中运行模块化的代码。这意味着:

  • 开发时启动飞快(冷启动快);
  • 修改文件后热更新极快(HMR 精准替换);
  • 代码结构清晰,符合现代工程规范。

所以,index.html 不仅是入口,更是连接静态 HTML 世界动态 Vue 世界的桥梁。


🔑 第四步:打造“钥匙”——main.js 如何启动应用

有了大门,就得有钥匙。main.js 就是这把精密的电子钥匙,负责激活整套智能家居系统:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './style.css'

createApp(App).use(router).mount('#app')

这段代码做了三件事,环环相扣:

  1. 引入核心模块:从 Vue 拿到“造房子”的工具(createApp),从本地拿到“客厅设计图”(App.vue)和“导航系统”(router);
  2. 组装系统:用 .use(router) 把导航插件装进主程序;
  3. 插入插座.mount('#app') 表示:“请把这套系统通电安装在 index.html 中 id 为 app 的插座上。”

没有这把钥匙,再漂亮的客厅也只是一堆图纸;有了它,整个房子才真正“活”起来。


💡 第五步:点亮客厅——根组件 App.vue

钥匙转动,门开了,我们走进 App.vue —— 这是所有功能的总控中心:

<template>
  <div id="app">
    <nav>
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </nav>
    <router-view />
  </div>
</template>

多人一开始会直接写 <div>Home | About</div>,但这只是静态文字。要让它们变成可点击的导航,就得用 Vue Router 提供的 <router-link> 组件。

这里有两个核心元素:

  • <router-link> :智能门把手,点击不刷新页面,只切换内容;
  • <router-view /> :魔法地板,当前该展示哪个房间,它就实时投影出来。

虽然原始文件只写了 HomeAbout,但正确的写法应如上所示——让文字变成可交互的导航。


🗺️ 第六步:装上导航系统——配置 Vue Router

路由,就像是你家里的智能导航系统。没有它,你只能待在客厅;有了它,你才能自由穿梭于各个房间。

我们在 src/router/index.js 中这样配置:

import { createRouter, createWebHashHistory } from 'vue-router';
import Home from '../views/Home.vue'
import About from '../views/About.vue'

const routes = [
  { path: '/', name: 'Home', component: Home },
  { path: '/about', name: 'About', component: About }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router

这段代码的意思是:

  • 当用户访问 /(也就是主页),就显示 Home.vue 这个房间;
  • 当用户访问 /about,就带他去 About.vue 那个房间。

注意这里用了 createWebHashHistory(),这意味着网址会变成 http://localhost:5173/#/about。那个 # 就像门牌号里的“分隔符”,告诉系统:“后面的部分是内部房间号,不是新地址”。


🛋️ 第七步:布置房间——编写页面组件

现在,我们来装修两个房间。

首页(Home.vue)

<template>
  <div>
    <h1>Home</h1>
  </div>
</template>

关于页(About.vue)

<template>
  <div>
    <h1>About</h1>
  </div>
</template>

每个 .vue 文件都是一个自包含的“功能单元”:有自己的结构(template)、逻辑(script)和样式(style)。它们彼此隔离,却能通过路由无缝切换。


🎨 第八步:美化家园——全局样式 style.css

虽然功能齐备,但房子还是灰扑扑的。这时候,style.css 就派上用场了。你可以在这里写:

body {
  font-family: 'Arial', sans-serif;
  background-color: #f5f5f5;
}

nav {
  padding: 1rem;
  background: white;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

就像给墙壁刷漆、给地板打蜡,让整个家更温馨舒适。


▶️ 最后一步:启动你的 Vue 家园!

现在,所有“装修材料”都已就位——地基打好了(Vite 项目)、大门装上了(index.html)、钥匙配好了(main.js)、客厅布置妥当(App.vue),连房间(Home.vueAbout.vue)和导航系统(Vue Router)也都调试完毕。是时候打开电闸,点亮整栋房子了!

请在终端(命令行)中依次执行以下两条命令(确保你已在 all-vue 项目目录下):

# 第一步:安装“住户手册”里列出的所有依赖(比如 Vue 和路由)
npm install

# 第二步:启动开发服务器——相当于按下“智能家居总开关”
npm run dev

运行成功后,你会看到类似这样的提示:

  VITE v5.0.0  ready in 320 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose

这时,只需打开浏览器,访问 http://localhost:5173/ (端口号可能略有不同),就能看到你的 Vue 小家啦!

image.png

  • 点击 Home,客厅中央显示 “Home”;
  • 点击 About,瞬间切换到 “About” 页面——全程无需刷新,就像在家自由走动一样丝滑。

🎉 恭喜你!你不仅看懂了代码,还亲手让它跑起来了!

这不再是一堆抽象的文件,而是一个真正能交互的 Web 应用。你已经完成了从“零”到“一”的飞跃——而这,正是所有伟大项目的起点。


🧩 总结:Vue 项目的“生活化”逻辑链

让我们用一次智能家居入住体验来串起全过程:

  1. Vite 是开发商:提供标准化精装修样板间;
  2. index.html 是入户门:设有智能插座(#app)和模块化接线口(type="module");
  3. main.js 是电子钥匙:插入后激活整套系统;
  4. App.vue 是中央控制台:集成导航与内容展示区;
  5. Vue Router 是室内导航图:定义各房间路径;
  6. Home.vue / About.vue 是功能房间:各自独立,按需进入;
  7. style.css 是全屋软装方案:统一视觉风格。

✨ 写在最后:你已经站在 Vue 的门口

这个 all-vue 项目虽小,却包含了现代 Vue 应用的核心骨架:组件化 + 路由 + 响应式 + 工程化构建。你不需要一开始就懂所有细节,就像学骑自行车,先扶稳车把,再慢慢蹬脚踏。

当你运行 npm run dev,看到浏览器里出现“Home”和“About”两个链接,并能自由切换时——恭喜你,你已经成功迈出了 Vue 开发的第一步!

接下来,你可以:

  • 在 Home 里加一张图片;
  • 在 About 里写一段自我介绍;
  • 用 CSS 让导航栏变彩色;
  • 甚至添加第三个页面……

编程不是魔法,而是一步步搭建的过程。而你,已经搭好了第一块积木。

现代前端工程化实战:从 Vite 到 React Router demo的构建之旅

前端技术的迭代从未停歇。当我们谈论现代前端开发时,React 19Vite 已经成为了不可忽视的标准配置。React 19 带来了更高效的并发渲染机制,而 Vite 则凭借基于 ESM 的极致冷启动速度,彻底改变了开发体验。

本文将通过一个名为 react-demo 的实战项目,带你从零开始理解如何搭建、配置并开发一个标准的现代 React 应用。我们将涵盖工程化配置、路由管理、Hooks 状态逻辑以及样式预处理等核心知识点。

一、 极速启动:Vite 与 ESM 的革命

在过去,Webpack 是构建工具的王者,但它在启动大型项目时往往需要漫长的打包等待。现代开发推荐使用 Vite(法语意为“快”)作为脚手架。

1. 为什么是 Vite?

Vite 的核心优势在于它利用了浏览器原生的 ES Modules (ESM) 机制。在开发阶段 (npm run dev),Vite 不需要对代码进行全量打包,而是按需提供模块,这实现了极致的“冷启动”体验。

当我们运行 npm init vite 拉取项目模板后,项目结构非常清晰。观察项目的 package.json 脚本配置:

"scripts": {
  "dev": "vite",
  "build": "vite build",
  "preview": "vite preview"
}

这对应了完整的开发生命周期:dev(开发) -> build(构建生产包) -> preview(本地预览生产包)。

2. 依赖管理的艺术:Dev vs Prod

在安装依赖时,区分“开发依赖”和“生产依赖”至关重要。

  • dependencies (生产依赖) :如 reactreact-dom。React 19.2.0 是核心库,负责组件定义和 diff 算法;而 react-dom 负责将组件渲染到浏览器 DOM 中。这类似于 Vue 的生态,React Core 对应 Vue Core,React DOM 对应 Vue 的渲染器。对应配置为 package.json 中的dependencies
  • devDependencies (开发依赖) :如 stylus。我们使用 npm i -D stylus 安装它,因为 Stylus 只是在开发阶段帮助我们将 .styl 文件编译为 CSS,上线后的代码并不需要 Stylus 引擎。对应配置为 package.json 中的devDependencies
// 生产依赖
  "dependencies": {
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "react-router-dom": "^7.10.1"
  },
// 开发依赖
  "devDependencies": {
    "@eslint/js": "^9.39.1",
    "@types/react": "^19.2.5",
    "@types/react-dom": "^19.2.3",
    "@vitejs/plugin-react": "^5.1.1",
    "eslint": "^9.39.1",
    "eslint-plugin-react-hooks": "^7.0.1",
    "eslint-plugin-react-refresh": "^0.4.24",
    "globals": "^16.5.0",
    "stylus": "^0.64.0",
    "vite": "^7.2.4"
  }

二、 入口与渲染:React 19 的严谨模式

项目的入口文件 main.jsx 展示了 React 19 最标准的挂载方式。

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.styl'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

严格模式 (StrictMode)

你可能会发现,在开发环境下组件的生命周期函数(如 useEffect)会执行两次。这并非 Bug,而是 <StrictMode> 的有意为之。它通过双重调用来帮助开发者检测不安全的副作用(Side Effects)和过时的 API 使用,确保代码在生产环境中更加健壮。

样式预处理

我们引入了全局样式 index.styl。Stylus 的魅力在于其极简的语法——省略花括号、冒号和分号,通过缩进来组织代码:

*
  margin: 0
  padding: 0

body
  background-color pink

Vite 内置了对 CSS 预处理器的支持,无需繁琐的 Webpack Loader 配置,安装即用,安装指令为npm i -D stylus。其中的-D就代表了开发依赖,如果不书写-D则会默认安装至生产依赖。

三、 路由架构:单页应用的骨架

单页应用(SPA)的核心在于:页面不刷新,URL 改变,内容切换,在一个页面 (index.html) 中实现 "多页面" 的切换效果。我们使用 react-router-dom v7 来实现这一功能。首先需要通过npm i react-router-dom指令安装路由。

1. 路由模式选择

App.jsx 中,我们采用了 BrowserRouter(别名为 Router)。相比于 URL 中带有 # 号的 HashRouterBrowserRouter 利用 HTML5 History API,提供了更现代化、更美观的 URL 结构,是目前的行业标准。

2. 声明式导航:Link vs A

在 React Router 中,我们严禁使用传统的 <a href> 标签进行内部跳转。因为 <a> 标签会导致浏览器强制刷新页面,从而重置 React 的所有状态。

相反,我们使用 <Link> 组件:

<nav>
  <ul>
    <li><Link to="/">Home</Link></li>
    <li><Link to="/about">About</Link></li>
  </ul>
</nav>

<Link> 组件在内部“消化”了点击事件,通过 JavaScript 修改 URL 并通过 Context 通知路由系统更新视图,实现了无缝的页面切换。

3. 路由配置分离

为了保持代码的整洁,我们将具体的路由规则抽离到了 router/index.jsx 中:

import { Routes, Route } from 'react-router-dom';
import Home from '../pages/Home.jsx';
import About from '../pages/About.jsx';

export default function AppRoutes() {
    return (
        <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
        </Routes>
    )
}

这种集中管理路由表的方式,使得 App.jsx 只需关注整体布局,而将路由细节交给 AppRoutes 组件处理。

四、 核心业务逻辑:Hooks 驱动的数据流

Home.jsx 组件展示了 React 函数式组件的核心逻辑:Hooks。我们的目标是调用 GitHub API 并展示数据。

1. 响应式状态:useState

const [repos, setRepos] = useState([]);

useState 是 React 响应式系统的基石。它返回当前状态 repos 和更新函数 setRepos。每当调用 setRepos 时,React 就会感知数据变化,并触发组件的重新渲染(Re-render),更新视图。

2. 副作用管理:useEffect

网络请求属于“副作用”(Side Effect),不能直接写在组件渲染逻辑中。我们使用 useEffect 来处理组件挂载后的逻辑:

useEffect(() => {
    // 组件挂载完成 (onMounted)
    fetch('https://api.github.com/users/shunwuyu/repos')
        .then(res => res.json())
        .then(json => setRepos(json))
}, [])
  • 执行时机useEffect 确保代码在组件渲染并挂载到 DOM 之后执行,避免阻塞 UI 渲染。
  • 依赖数组 [] :第二个参数传入空数组 [],意味着这个 Effect 只在组件初始化时执行一次(相当于类组件的 componentDidMount)。如果不传此参数,每次渲染都会触发请求,导致无限循环。

3. 条件渲染与列表 Key

在 JSX 中,我们利用 JavaScript 的灵活性来构建 UI。

return (
        <div>
            <h1>Home</h1>
            {
                repos.length ? (
                    <ul>
                        {
                            repos.map(repo => (
                                <li key={repo.id}>
                                    <a href={repo.html_url} target="_blank" rel="noreferrer">
                                        {repo.name}
                                    </a>
                                </li>
                            ))
                        }
                    </ul>
                ) : null
            }
        </div>
    );
  • Diff 算法的关键:在遍历列表时,必须为每个元素提供唯一的 key(如 repo.id)。这能帮助 React 的 Diff 算法高效地识别元素的增删改,最小化 DOM 操作。
  • 条件渲染:通过三元运算符检查 repos.length,在数据加载前不渲染列表,防止页面报错。

五、 总结

通过这个项目,我们不仅搭建了一个简单的 GitHub 仓库浏览器,更重要的是实践了现代 React 开发的标准范式:

  1. 工程化:利用 Vite 极速构建,区分开发与生产依赖。
  2. 组件化:通过 Props 和 Hooks 实现逻辑复用。
  3. 路由化:使用 React Router 实现 SPA 的无感跳转。
  4. 响应式:利用 useStateuseEffect 驱动数据流向。

从 React 19 的底层优化到 Vite 的工程实践,这套技术栈为开发者提供了极其高效的开发体验,是构建未来 Web 应用的坚实基础。

六、Home.jsx 源代码

import { useState, useEffect } from 'react';

const Home = () => {
    const [repos, setRepos] = useState([]);
    // render 是第一位的
    // console.log('Home 组件渲染了');
    useEffect(() => {
        // home 组件可以看到了
        // console.log('Home 组件挂载了');
        // 发送api请求,不会和组件渲染去争抢
        fetch('https://api.github.com/users/shunwuyu/repos')
            .then(res => res.json())
            .then(json => setRepos(json))
    }, [])
    return (
        <div>
            <h1>Home</h1>
            {
                repos.length ? (
                    <ul>
                        {
                            repos.map(repo => (
                                <li key={repo.id}>
                                    <a href={repo.html_url} target="_blank" rel="noreferrer">
                                        {repo.name}
                                    </a>
                                </li>
                            ))
                        }
                    </ul>
                ) : null
            }
        </div>
    );
}

export default Home;
❌