从零搭建一个现代 React 项目:Vite + React 19 + React Router v6 实战详解

155 阅读9分钟

从零搭建一个现代 React 项目:Vite + React 19 + React Router v6 实战详解

大家好,最近 React 19 正式发布,带来了更多优化和新特性,同时 Vite 作为新一代构建工具,已经彻底取代了 Create React App 的地位。今天我们就基于一个完整的实战小项目,手把手带你从零搭建一个现代 React 应用,涵盖 Vite 初始化、JSX 基础、Hooks 使用、路由配置 等核心知识点。

这个项目虽然简单,但麻雀虽小五脏俱全,能让你快速掌握当前最主流的 React 开发流程。

image.png


一、为什么选择 Vite + React 19?

以前我们用 create-react-app 起步,但它基于 Webpack,开发时冷启动慢、HMR(热更新)也慢。现在全行业都转向 Vite

  • 极快的冷启动:基于原生 ES 模块(ESM),浏览器直接加载模块,不需要打包
  • 闪电般的热更新:改代码几乎瞬间生效
  • 开箱即用支持 React 19:官方模板完美兼容新特性
  • 更小的生产包体积:Rollup 打包更高效

初始化一个 Vite + React 项目超级简单:

npm init vite
cd my-react-app
npm install
npm run dev

就这样,几秒钟就启动了开发服务器。vite 把 React 基建全交给自己管,我们只需专注写组件。


二、项目结构与入口解析

一个标准 Vite + React 项目结构大概是这样:

image.png

main.jsx:应用的启动器

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

createRoot(document.getElementById('root')).render(<App />)
  • createRoot 是 React 18+ 的新 API(Concurrent Mode 基础),比老的 ReactDOM.render 更强大
  • 直接把 <App /> 挂载到 #root
  • 注意:开发时可以包裹 <StrictMode>,它会故意执行两次渲染来帮你发现副作用问题(生产环境不会)

易错提醒:很多人忘了导入样式或 App,导致页面空白。记得路径正确!


三、JSX:React 的灵魂语法

React 的组件返回的不是字符串,而是 JSX —— 一种看起来像 HTML 的 JavaScript 扩展语法。

const About = () => {
  return (
    <div>
      <h1>About</h1>
    </div>
  )
}
export default About

底层逻辑:JSX 会被 Babel 编译成 React.createElement 调用,最终生成 Virtual DOM。

关键规则提醒(超级容易忘!):

  • 必须有一个根元素包裹(可以用 <></> 碎片)
  • 所有标签必须闭合(<br />
  • 属性用驼峰命名:classNamehtmlForonClick
  • 表达式用 {} 包裹:{repo.name}

四、Hooks:函数组件的超级力量

React 19 继续强化 Hooks,让函数组件几乎无所不能。我们重点看两个最常用的:

1. useState:响应式状态管理

const [repos, setRepos] = useState([]);
  • repos 是当前状态值
  • setRepos 是更新函数(异步批量更新)
  • 状态变化 → 组件重新渲染

底层逻辑:React 用 Fiber 架构记录每个组件的 Hooks 队列,按调用顺序匹配状态。绝对不能在条件判断、循环里调用 Hooks!

易错提醒

  • 不要直接修改状态:错 repos.push(...),对 setRepos([...repos, newItem])
  • setState 是异步的:setCount(c => c + 1) 用函数形式更安全

2. useEffect:副作用管理(相当于生命周期)

useEffect(() => {
  fetch('https://api.github.com/users/shunwuyu/repos')
    .then(res => res.json())
    .then(data => {
      setRepos(data)
    })
}, [])  // 依赖数组为空,只执行一次
  • 组件首次渲染后执行(类似 mounted)
  • 返回清理函数用于卸载时清理(类似 willUnmount)
  • 依赖数组控制执行时机:
    • [] → 只挂载时执行一次
    • [dep] → dep 变化时重新执行
    • 无数组 → 每次渲染都执行

易错提醒

  • 忘记写依赖数组 → 无限请求或内存泄漏
  • 把异步操作直接放组件体里 → 每次渲染都发请求,性能灾难
  • 清理不及时(如定时器、订阅)→ 组件卸载后还在执行

五、React Router v6:现代前端路由的王者

老版本 React Router(v5)配置繁琐,v6 彻底重构,更简洁、更强大。

安装与基本使用

npm i react-router-dom

核心组件

  • <BrowserRouter>:HTML5 history 模式(推荐,干净无 #)
  • <Link>:导航链接,替代 <a>,防止页面刷新
  • <Routes> + <Route>:声明式路由配置

项目中的配置:

// App.jsx
import { BrowserRouter as Router, Link } from 'react-router-dom'
import AppRoutes from './router'

function App() {
  return (
    <Router>
      <nav>
        <ul>
          <li><Link to='/'>Home</Link></li>
          <li><Link to='/about'>About</Link></li>
        </ul>
      </nav>
      <AppRoutes />
    </Router>
  )
}
// router/index.jsx
import { Routes, Route } from 'react-router-dom'
import Home from '../pages/Home'
import About from '../pages/About'

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

v6 重大变化与易错点

  • 不再用 <Switch>,改用 <Routes>(自动找最匹配的路由)
  • componentelement(必须是 <Component /> 形式)
  • 嵌套路由更简单,支持布局路由
  • useNavigate 替代 useHistory

HashRouter vs BrowserRouter

  • HashRouter 带 #,兼容性最好(老浏览器)
  • BrowserRouter 干净,但需要服务器配置(404 重定向到 index.html)

六、完整 Home 页面实战:结合 Hooks + API 请求

const Home = () => {
  const [repos, setRepos] = useState([]);

  useEffect(() => {
    fetch('https://api.github.com/users/******/repos')
      .then(res => res.json())
      .then(data => setRepos(data))
  }, [])

  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>
      ) : (
        <p>暂无仓库</p>
      )}
    </div>
  )
}
  • 加载状态处理:可以用 loading 状态更优雅
  • key 必须唯一稳定(用 id 最好)
  • 外部链接加 rel="noreferrer" 防止安全问题

七、几个细节知识点

fc962ce0cd306c49bc54248e80437e81.jpg

1. 为什么开发时 会故意渲染两次,生产环境不会?

<StrictMode>(严格模式)是 React 专门为开发阶段准备的“挑刺工具”。它会在开发环境下故意做一些“过分”的事,比如:

  • 组件的挂载阶段(mount)会故意多执行一次渲染(包括函数组件体、useEffect 的 setup 等)
  • 故意调用两次 useEffect 的 setup,然后立刻调用 cleanup,再真正执行

目的就是逼你暴露副作用问题

比如:

  • 你在 useEffect 里直接 new WebSocket() 或 addEventListener,却没在 cleanup 里清理 → 严格模式下会立刻报警告
  • 你在组件函数体里做了不纯的操作(比如直接修改 DOM 或全局变量) → 两次渲染后状态不一致,bug 立刻暴露

为什么生产环境不会两次渲染?

因为这些“故意找茬”的行为只在开发环境生效,生产构建时 React 会自动把 StrictMode 的这些检查代码剥离掉(tree-shaking),最终打包的代码完全干净、高效,不会有任何性能损耗。

简单说:
开发时戴着“放大镜”帮你查 bug,生产时摘掉眼镜,轻装上阵。

建议:永远在开发时包裹 <StrictMode>,上线前不用手动去掉,它自己就没了。

2. 为什么 Hooks 绝对不能在条件判断、循环里调用?

这个规则是 React Hooks 的核心设计原则Hooks 必须按固定顺序调用

React 是怎么知道哪个 useState 对应哪个状态的?不是靠变量名,而是靠调用顺序

内部实现大致是这样(伪代码):

let hookIndex = 0;
const hooks = [];

function useState(initial) {
  const currentIndex = hookIndex++;
  // 第一次渲染创建,之后根据 currentIndex 取旧值
  return [hooks[currentIndex] || initial, setValue];
}

function useEffect(...) {
  const currentIndex = hookIndex++;
  // 同理
}

每次组件渲染,React 从头到尾按顺序执行 Hooks,hookIndex++ 递增。

如果你把 Hooks 放条件里

if (condition) {
  const [a, setA] = useState(0); // 第0个hook
}

const [b, setB] = useState(1);   // condition=false时,这里变成第0个hook!

condition 为 true 和 false 时,hooks 的顺序完全不一样!React 傻眼了:这次第0个 hook 是 b?上次是 a?这状态怎么对应?直接崩!

正确做法:条件放在 Hook 内部,而不是 Hook 放在条件里

想根据条件有不同状态?这样写:

jsx

function GoodComponent({ isLogin }) {
  const [userName, setUserName] = useState(isLogin ? 'Cherry' : null);
  // 或者用 '' 空字符串,或者单独用一个 isLogin 的 state

  const [count, setCount] = useState(0);

  // 如果只想在登录时显示用户名
  return (
    <div>
      {isLogin && <p>欢迎 {userName}</p>}
      <p>计数: {count}</p>
    </div>
  );
}

这样 Hooks 顺序永远固定:useState → useState → useEffect,React 永远知道谁是谁。

底层逻辑:React 用一个链表/数组按顺序存储每个组件的 hooks 状态,依赖严格的调用顺序匹配。

总结:为什么必须顶层调用?

因为 React 的 Hooks 系统是基于调用顺序的“位置映射”,而不是“名字映射”。

  • 它简单高效(不需要解析变量名)
  • 它允许你用多个相同 Hook(如多个 useState)
  • 但代价就是:你不能改变调用顺序

这也是为什么官方规则叫 Rules of Hooks,只有两条:

  1. 只在 React 函数组件或自定义 Hook 最顶层调用
  2. 不要在循环、条件或嵌套函数中调用

一旦违反,顺序乱了,整个状态系统就崩了。

3. useEffect 依赖数组的三个情况到底啥意思?

useEffect 的第二个参数(依赖数组)决定了“什么时候重新运行 effect”。

  • [] 空数组
    只在组件 第一次挂载(mount)时运行一次。
    相当于 class 组件的 componentDidMount。
    适合:发请求、订阅事件、初始化第三方库。

  • [dep1, dep2]
    只要数组里的任何一个值发生变化(=== 严格相等比较),就重新运行 effect。
    包括首次渲染也会运行一次(因为从“无”到“有”也算变化)。
    适合:监听某个 state/props 变化做对应操作。

  • 不写依赖数组
    每次组件渲染(render)完成后都运行 effect。
    相当于 componentDidMount + componentDidUpdate 全监听。
    适合:极少数场景,比如调试时想看每次渲染都干啥(基本别用,容易无限循环)。

底层逻辑:React 会把上一次的依赖数组保存下来,和这次渲染的做浅比较(Object.is),有变化就重新执行 setup,并先执行上次的 cleanup。

易错提醒

  • 依赖漏写 → 闭包捕获旧值,bug 层出(React 会警告!)
  • 依赖多写 → 不必要的重复执行,性能浪费
  • 依赖写对象/数组 → 引用变了就触发(即使内容一样),建议拆成基本类型或用 useMemo

4. 为什么 要替代 < a > ,说是防止页面刷新?

普通 <a href="/about"> 点击后会干三件事:

  1. 跳转到新 URL
  2. 浏览器整页刷新
  3. 重新下载 HTML、JS、CSS,React 应用整个重新启动

这完全违背了 SPA(单页应用)的核心理念:页面切换不刷新,状态保留,体验丝滑

<Link> 是 React Router 提供的“智能链接”:

  • 它监听点击事件
  • 阻止默认的浏览器跳转行为(preventDefault)
  • 手动调用 history.pushState() 修改 URL(不刷新页面)
  • 触发 React Router 重新匹配路由,渲染新组件

结果:URL 变了,页面内容变了,但整个 React 应用不销毁不重启,状态、动画、输入框内容全保留。

底层逻辑:利用了 HTML5 History API(pushState/replaceState),配合 React 的组件渲染,实现“假跳转,真切换”。

额外福利

  • 支持预加载(prefetch)
  • 支持动画过渡
  • SEO 友好(服务端渲染配合)

总结口诀:想刷新用 <a>,想丝滑用 <Link>


八、最佳实践与性能建议

  1. 组件拆分:页面级组件放 pages/,通用组件放 components/
  2. 样式方案:推荐 Tailwind CSS 或 CSS Modules,避免全局污染
  3. 状态管理:小项目用 useContext + useReducer,中大型用 Zustand/Pinia(React 版)
  4. 错误边界:用 ErrorBoundary 包裹防止整站崩溃
  5. 代码分割:用 React.lazy + Suspense 按需加载路由

九、总结:

  1. Vite 初始化 → 极快开发体验
  2. main.jsx 启动 → createRoot 挂载 App
  3. App.jsx 配置路由 → BrowserRouter + Link
  4. Routes 声明页面 → Home / About
  5. 函数组件 + Hooks → useState / useEffect 驱动一切

掌握了这个流程,你就拥有了当前最前沿的 React 开发能力。React 19 的新特性(如 Actions、use 等)也会无缝融入这个架构。