从零搭建一个现代 React 项目:Vite + React 19 + React Router v6 实战详解
大家好,最近 React 19 正式发布,带来了更多优化和新特性,同时 Vite 作为新一代构建工具,已经彻底取代了 Create React App 的地位。今天我们就基于一个完整的实战小项目,手把手带你从零搭建一个现代 React 应用,涵盖 Vite 初始化、JSX 基础、Hooks 使用、路由配置 等核心知识点。
这个项目虽然简单,但麻雀虽小五脏俱全,能让你快速掌握当前最主流的 React 开发流程。
一、为什么选择 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 项目结构大概是这样:
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 />) - 属性用驼峰命名:
className、htmlFor、onClick - 表达式用
{}包裹:{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>(自动找最匹配的路由) component→element(必须是<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"防止安全问题
七、几个细节知识点
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,只有两条:
- 只在 React 函数组件或自定义 Hook 最顶层调用
- 不要在循环、条件或嵌套函数中调用
一旦违反,顺序乱了,整个状态系统就崩了。
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"> 点击后会干三件事:
- 跳转到新 URL
- 浏览器整页刷新
- 重新下载 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>。
八、最佳实践与性能建议
- 组件拆分:页面级组件放 pages/,通用组件放 components/
- 样式方案:推荐 Tailwind CSS 或 CSS Modules,避免全局污染
- 状态管理:小项目用 useContext + useReducer,中大型用 Zustand/Pinia(React 版)
- 错误边界:用 ErrorBoundary 包裹防止整站崩溃
- 代码分割:用
React.lazy + Suspense按需加载路由
九、总结:
- Vite 初始化 → 极快开发体验
- main.jsx 启动 → createRoot 挂载 App
- App.jsx 配置路由 → BrowserRouter + Link
- Routes 声明页面 → Home / About
- 函数组件 + Hooks → useState / useEffect 驱动一切
掌握了这个流程,你就拥有了当前最前沿的 React 开发能力。React 19 的新特性(如 Actions、use 等)也会无缝融入这个架构。