前言
今天来给大家聊聊用 Vite 搭建 React 项目这件事。为什么选 Vite + React?因为它快!真的快!比传统的 Create React App 启动速度快十几倍甚至上百倍,冷启动几乎是秒开。而且在 2025 年,React 已经升级到 19.x 版本,带来了更多好玩的新特性,咱们用最新的技术栈来玩,才够爽嘛。
文章基于一个真实的简单 demo 项目(包含路由、状态管理、数据请求、全局样式等),咱们边拆解代码边聊原理。
一、为什么现在学 React 还要选 Vite?
以前大家学 React 基本都用 Create React App(简称 CRA),它简单,一键搞定。但现在官方都不推荐新项目用 CRA 了,因为它底层是 Webpack,配置复杂,开发时热更新慢,启动也慢。
Vite 是新一代构建工具,由 Vue 作者尤雨溪团队打造,但它不挑框架,React 用起来也超级丝滑。它的核心优势:
- 极致快的开发服务器:基于原生 ES Modules(ESM),浏览器直接加载模块,不需要打包,冷启动基本瞬间完成。
- 开箱即用:支持 TypeScript、JSX、Stylus/Less/Sass 等,几乎零配置。
- 生产构建也很强:用 Rollup 打包,体积小、速度快。
- 完美支持现代 React:包括 React 19 的新特性(如 Actions、Server Components 等,虽然咱们这个 demo 是纯客户端)。
简单说:用 Vite 开发 React,幸福感翻倍!
创建项目超简单:
Bash
npm init vite
命名项目
选择相应语言
等待项目安装完成,打开项目的本地链接即可打开

再次打开可以运行以下命令
Bash
npm run dev
二、React 项目的基本结构长啥样?
一个标准的 Vite + React 项目目录大概是这样:
text
my-react-app/
├── public/ # 静态资源,直接拷贝到构建目录
├── src/
│ ├── assets/ # 图片、字体等资源
│ ├── components/ # 可复用组件
│ ├── pages/ # 页面级组件(Home、About 等) 手动添加
│ ├── router/ # 路由配置 手动添加
│ ├── App.jsx # 根组件
│ ├── main.jsx # 入口文件,挂载根组件
│ ├── index.css 或 styl # 全局样式
│ └── vite-env.d.ts # TypeScript 类型声明
├── index.html # HTML 入口
├── package.json # 依赖和脚本
└── vite.config.js # Vite 配置(可选扩展)
咱们这个 demo 就是这样的结构:有 Home 页、About 页,用 react-router-dom 管理路由,全局用 Stylus 写样式,还有 useState 和 useEffect 的实际应用。
三、React vs Vue:两个热门框架到底差在哪儿?
很多人学前端时会纠结 React 还是 Vue,我简单对比一下,帮助你选对方向(2025 年视角)。
| 方面 | React | Vue |
|---|---|---|
| 核心定位 | 库(只管 UI),灵活性极高 | 框架(开箱即用,很多东西内置) |
| 语法风格 | JSX(JS 里写 HTML),一切皆 JS | 模板语法(HTML 里写逻辑),更接近传统 HTML |
| 学习曲线 | 稍陡,需要懂 Hooks、JSX、生态工具 | 更平缓,上手快,适合新手 |
| 状态管理 | 靠 Hooks(如 useState、useReducer),大项目用 Redux/Zustand | 内置响应式 + Pinia,简单项目几乎不需要额外库 |
| 路由 | react-router-dom(咱们 demo 用的) | vue-router(官方内置) |
| 生态和就业 | 超级庞大,国内大厂基本都用 React,岗位多 | 也很强,但 React 市场份额更大 |
| 性能 | Virtual DOM + Fiber,优秀 | Virtual DOM + 更细粒度更新,略胜一筹 |
| 适用场景 | 大型复杂应用(如 Facebook、Netflix) | 中小型项目、快速迭代(如国内很多后台系统) |
简单说:如果你想深入 JS,追求灵活,准备去大厂,学 React 准没错。Vue 更友好,开发速度快,但 React 的生态和未来潜力更大(尤其 React 19 强化了服务器组件,往全栈方向走)。
。Vue 是完整的框架,React 是 UI 库 + react-dom 负责渲染。React 更像乐高积木,你想怎么搭就怎么搭。
四、深入 React 核心板块:原理 + 代码拆解
React 的魅力在于它的设计哲学:组件化、声明式、单向数据流。咱们通过 demo 代码,一块一块拆。
1. 入口文件:main.jsx 和 index.html
HTML
<!-- index.html -->
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
jsx
// main.jsx
import { createRoot } from 'react-dom/client';
import './index.styl'; // 全局样式
import App from './App.jsx';
createRoot(document.getElementById('root')).render(<App />);
原理:React 19 用 createRoot 挂载根组件到 DOM 的 #root 上。以前有 StrictMode 会渲染两次帮你查 bug,现在可以注释掉。Vite 会自动编译 Stylus 为 CSS,超级方便。
2.StrictMode
打开初始化的界面,点击中间的按钮可以自增count
我们在代码文件中选择随着按钮点击,在控制台输出count ( App.jsx)

打开控制台,神奇的一幕出现了,为什么我只点击了一次按钮,控制台确输出了两次?

其实是React在初始化的时候默认有一个StrictMode组件

1).什么是StrictMode
StrictMode 是 React 提供的一个开发模式专属工具组件 ,它不会渲染任何可见的 UI(就像 < Fragment > 一样透明),但会在开发环境下对组件树进行一系列额外检查和警告,帮助你尽早发现潜在问题。
简单说:它就像一个严格的代码审查老师,专门挑你代码里的毛病,但只在开发时上课,上线(生产环境)时它就下班了,完全不影响性能。
官方推荐:新项目直接在根组件包裹 StrictMode,比如 Vite + React 项目默认就开了。
使用方式超简单:
jsx
javascript
// main.jsx 或 index.js
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
);
2). StrictMode 能帮你检查啥问题?
它主要做这些事(到 2025 年 React 19 时代,核心行为基本不变):
- 检测不安全的生命周期方法(针对老的 Class 组件):比如一些已经被标记为不安全的生命周期,会给你警告,提醒你升级。
- 警告使用已废弃的 API:比如旧的 context API、findDOMNode、字符串 ref 等。
- 检查组件的纯度(Purity) :故意让某些函数运行两次,帮你发现"副作用"隐藏的 bug。
- 模拟未来 Concurrent Mode 的行为:React 未来的并发渲染可能会暂停、恢复组件的挂载/卸载,StrictMode 提前让你适应这种场景。
最让人"抓狂"的就是双渲染和双运行 effect 这部分,咱们重点说说。
3). 为什么组件会渲染两次?useEffect 会运行两次?
<3.1>先简单说说 useEffect 是什么
useEffect 是 React 中最常用的 Hook 之一,专门用来处理"副作用"(side effects)。 副作用指的是那些不直接属于"渲染 UI"的操作,比如:
- 发送网络请求(fetch 数据)
- 操作 DOM(比如设置焦点)
- 设置定时器、事件监听、订阅等
useEffect 的基本用法是:
jsx
useEffect(() => {
// 这里放副作用代码
}, [依赖数组]); // 控制什么时候执行
- 如果依赖数组是 [](空数组),就只在组件第一次挂载后执行一次。
- 如果不写依赖数组,每次渲染后都会执行。
- 如果写上变量,只有这些变量变化时才会执行。
它还有一个"清理函数"机制(返回一个函数),用来在组件卸载或下次执行前"收尾"(比如清除定时器、取消请求)。
简单记住:useEffect 就是 React 给你的"生命周期钩子",让你在渲染完成后安全地干那些额外的事。
<3.2>再回到StrictMode
在开发模式下,StrictMode 会故意做这些"额外操作":
-
双渲染(Double Rendering) :组件的 render 函数(函数组件就是函数体本身)会被调用两次。
-
双挂载 effect:对于新挂载的组件,useEffect(依赖为空的)会这样执行:
- 先运行一次(mount)
- 立即清理(unmount,运行 cleanup 函数)
- 再运行一次(remount)
为什么这么设计?
因为 React 想让你写出纯净、可恢复状态的组件。未来的并发渲染(Concurrent Rendering)可能会因为优先级中断,导致组件"挂载一半又卸载,再重新挂载"。如果你的 effect 里有副作用(比如直接修改外部变量、多次发起请求),就会出问题。
举个经典例子:
jsx
function BadComponent() {
useEffect(() => {
// 坏味道:直接发请求,没考虑重复执行
fetch('/api/data').then(res => res.json()).then(setData);
}, []);
return <div>...</div>;
}
在 StrictMode 下,这个请求会发两次!这样你就立刻发现问题了:应该用 abort controller 取消,或者确保 effect 是幂等的(重复执行也没事)。
正确做法:添加 cleanup,或者用 ref 标志位避免重复逻辑(但最好是让 effect 本身安全)。
另一个例子:如果你在组件里直接 console.log("render"),StrictMode 下会打印两次,帮助你意识到"渲染应该是纯函数,不该有副作用"。
重要提醒:
- 生产环境完全不会双渲染、双 effect,性能不受影响。
- React 只取第一次渲染的结果,第二次只是为了检查,丢弃掉。
4). 看到双渲染怎么办?要关掉 StrictMode 吗?
强烈不推荐直接关掉! 关掉等于放弃了 React 给你的免费 bug 检测工具。
正确心态:
-
把双渲染当成"开发时的提醒",而不是 bug。
-
如果控制台日志太多烦人,可以装 React Developer Tools 浏览器扩展,里面有个选项:"Hide logs during second render in Strict Mode",打开后第二次的 log 会变灰或隐藏。
-
确保你的代码能经得起双挂载考验:
- useEffect 里返回 cleanup 函数(取消定时器、订阅、请求等)。
- 避免在渲染阶段做副作用(比如直接改 state 或发请求)。
- 数据请求放 effect 里,并处理取消。
5).StrictMode 是你的好朋友
- 开启它:帮你写出更健壮、更符合未来 React 方向的代码。
- 别怕双渲染:这是故意设计的"压力测试",生产环境一切正常。
- 多利用它:看到警告就去改,早改早安心。
下次看到组件渲染两次,别急着删 ,先检查自己的代码有没有隐藏的副作用~ 这才是 React 想教给你的"好习惯"。
2. 根组件 App.jsx:路由接管一切
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>
);
}
这里用了 react-router-dom@7(最新版更简洁,包更统一)。BrowserRouter 用 HTML5 history 模式(干净的 URL,不带 #)。Link 代替 a 标签,防止页面刷新。
原理:React Router 用上下文(Context)管理路由状态,切换页面时只重新渲染匹配的组件,不刷新整个页面。这就是单页应用(SPA)的核心。
BrowserRouter 和 HashRouter 的区别
在 React 项目中使用 react-router-dom 时,最常见的两种路由器就是 BrowserRouter (通常写成 import { BrowserRouter as Router })和 HashRouter(类似写成 import { HashRouter as Router })。
它们的作用都是实现单页应用(SPA)的客户端路由:点击链接切换页面时不刷新整个页面,只更新部分内容。但它们的实现方式和适用场景完全不同。下面咱们用大白话一个个掰扯清楚。
1. URL 长什么样?(最直观的区别)


-
BrowserRouter :干净、现代的 URL 示例:example.com/、https://ex...
-
HashRouter :URL 中带一个 #(hash)符号 示例:example.com/#/、https://...
' #' 后面的部分叫"hash fragment",浏览器不会把这部分发给服务器,全由前端自己处理。
2. 底层原理有什么不同?
-
BrowserRouter:
- 用 HTML5 的 History API(pushState、replaceState 等)来操控浏览器历史记录。
- 当你切换路由时,它会修改 URL,但不会向服务器发起新请求(页面不刷新)。
- 服务器会收到完整的 URL 请求。比如直接访问 /about,服务器必须返回你的 index.html(否则 404)。
-
HashRouter:
- 用 URL 的 hash 部分(# 后面)来存储路由信息。
- 浏览器默认行为:改变 hash 不会向服务器发请求,也不会刷新页面。
- 服务器永远只看到 # 前面的部分(通常就是 /),所以不管你怎么切换路由,服务器只返回根路径的 index.html。
3. 优缺点对比
| 方面 | BrowserRouter(推荐) | HashRouter |
|---|---|---|
| URL 美观度 | 干净、专业(如真实多页网站) | 带 #,看起来有点老派 |
| SEO 友好 | 好(搜索引擎能正常抓取路径) | 差(# 后面的内容搜索引擎基本忽略) |
| 服务器配置 | 需要配置:所有路径都要 fallback 到 index.html | 无需任何配置,开箱即用 |
| 刷新/直接访问 | 正常(服务器正确返回 index.html 后,React 接管) | 一定正常(服务器只看到根路径) |
| 兼容性 | 现代浏览器(IE10+) | 几乎所有浏览器(包括很老的) |
| 适用场景 | 正式上线项目、生产环境 | 静态托管(如 GitHub Pages、Netlify 免费版)、快速原型、没服务器控制权 |
4. 什么时候用 HashRouter?
虽然官方强烈推荐 BrowserRouter,但 HashRouter 仍有它的生存空间:
- 部署在纯静态文件服务器上:比如 GitHub Pages、Netlify 静态托管、S3 Bucket 等。这些地方你没法改服务器配置,直接访问 /about 会 404。
- 快速原型或演示项目:不想折腾服务器配置,先跑起来再说。
- 内嵌在其他页面或 iframe 中:避免和外层 URL 冲突。
- 支持极老浏览器:虽然现在基本没必要。
如果你有自己的后端服务器(Node、Nginx、Apache 等),一定优先用 BrowserRouter,只需简单配置一下所有非静态资源路径都指向 index.html 就行。
3. 路由配置:router/index.jsx
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>
);
}
超级简单!Routes 是路由总管,Route 定义具体路径和对应组件。
4. 页面组件:用 Hooks 管理状态和副作用
先看简单 About:
jsx
const About = () => {
return <div><h1>About</h1></div>;
};
export default About;
再看 Home:这里展示了 useState 和 useEffect 的经典用法。
jsx
import { useState, useEffect } from "react";
const Home = () => {
const [repos, setRepos] = useState([]);
useEffect(() => {
fetch("https://api.github.com/users/shunwuyu/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>
);
};
export default Home;
useState 原理:React 的"响应式状态"。调用 setRepos 会触发组件重新渲染,但只更新变化的部分(靠 Virtual DOM 对比)。
useEffect 原理:管理"副作用"(如请求数据、订阅事件)。依赖数组 [] 为空时,只在组件第一次渲染后跑一次,类似 Vue 的 onMounted。数据请求放这里,不会阻塞渲染,主线程更流畅。
JSX 原理:就是 JS 的语法糖,Babel/Vite 编译成 React.createElement 调用。写起来像 HTML,其实全是 JS,超级灵活。
Virtual DOM 原理:React 不直接操作真实 DOM,而是维护一个虚拟 DOM 树。状态变了,计算新旧差异(diff 算法),只 patch 必要的部分。高效!

5. 全局样式:Stylus 示例
stylus
// index.styl
*
margin: 0
padding: 0
body
background-color: pink
如何在 Vite 项目中使用 Stylus?
1). 安装 Stylus 依赖:
bash
npm install -D stylus
注意:只需要安装
stylus,Vite 内部通过@vitejs/plugin-vue(如果是 Vue 项目)或内置的 CSS 预处理器机制自动识别.styl或<style lang="stylus">并调用 Stylus 编译器。
2).在组件或样式文件中使用:
Vue 单文件组件中:
xml
```
vue
编辑
<style lang="stylus">
.example
color red
font-size 16px
</style>
```
独立的 .styl 文件:
css
stylus
// styles/main.styl
body
background #f0f0f0
然后在 JS/TS 中导入:
arduino
js
import './styles/main.styl'
3). 无需额外配置 (大多数情况下): 安装 stylus 后,Vite 会自动检测并使用它来处理 .styl 文件或 lang="stylus" 的 style 块。
6. package.json 和 package-lock.json
在 Node.js 项目(包括 React、Vue 等前端项目)中,这两个文件是 npm(或 yarn/pnpm)包管理的核心文件。它们都在项目根目录下,缺一不可。
1). package.json:项目的"身份证"和"依赖清单"
-
作用:手动创建和维护的文件,描述项目的基本信息和依赖包。
-
主要内容:
- 项目名称、版本、描述、入口文件(main)、脚本命令(scripts,如 "dev": "vite")。
- dependencies:生产环境需要的包(运行项目必须的,比如 react、react-router-dom)。
- devDependencies:开发环境需要的包(只在开发时用,比如 vite、eslint)。
- 依赖版本用语义化版本号,比如 "^19.2.0"(允许小版本更新)或 "~19.2.0"(允许补丁更新)。
-
特点:版本范围灵活,运行 npm install 时会安装最新兼容版本。
-
你需要手动修改它:添加新包用 npm install xxx(会自动更新),或直接编辑。
2). package-lock.json:依赖的"精确锁文件"
-
作用 :自动生成的"锁死"文件,确保每个人(包括 CI/CD、服务器)安装的依赖版本完全一致。
-
主要内容:
- 记录了整个依赖树(包括子依赖)的精确版本号、下载地址、完整性校验(hash)。
- 比如 package.json 里 react "^19.2.0",它会锁成具体的 "19.2.3"。
-
特点:
- 你不要手动修改它(改了容易出问题)。
- 当你运行 npm install 时,如果有 package-lock.json,就会严格按照里面的版本安装。
- 没有它时,npm 会根据 package.json 安装最新兼容版,可能导致不同机器版本不一致(著名的"在我机子上能跑"问题)。
简单对比
| 项目 | package.json | package-lock.json |
|---|---|---|
| 谁生成/维护 | 你手动创建和修改 | npm 自动生成和更新 |
| 版本描述 | 范围(如 ^19.2.0,允许自动升级小版本) | 精确版本(如 19.2.3) |
| 目的 | 定义项目信息和依赖范围 | 锁定依赖树,确保所有人安装相同版本 |
| 是否提交到 Git | 必须提交 | 必须提交(保证团队/部署一致性) |
| 大小 | 较小 | 很大(记录所有子依赖) |
五、总结
通过这个简单 demo,你已经掌握了 Vite + React 的核心流程:创建项目 → 配置路由 → 用 Hooks 管理状态和数据 → 全局样式。
React 的精髓就是:用组件思考问题,用状态驱动 UI。多练多写,很快就能上手复杂项目。
建议新手:
- 先撸几个小 demo(TodoList、博客)。
- 学 TypeScript 版本(vite 模板选 react-ts)。
- 了解状态管理库如 Zustand 或 Redux Toolkit。
- 看看 Next.js(基于 React 的全栈框架)。