React 教程
React 是一个用于构建用户界面(UI)的 JavaScript 库,由 Facebook(Meta)开发并开源。它用"组件化 + 声明式"的方式构建界面,是目前最流行的前端框架之一。
React 的核心思想
- 组件化:UI 由独立、可复用的组件构成
- 声明式:描述"UI 应该长什么样",React 负责更新
- 单向数据流:数据从父组件流向子组件
- 虚拟 DOM:高效更新真实 DOM
一个 React 组件
function Welcome({ name }) {
return <h1>你好,{name}!</h1>;
}
function App() {
return (
<div>
<Welcome name="CodeNest" />
<Welcome name="React" />
</div>
);
}
React 的生态
| 领域 | 常用方案 |
|---|---|
| 构建工具 | Vite、Next.js、Create React App |
| 路由 | React Router |
| 状态管理 | Redux、Zustand、Context |
| UI 组件库 | Ant Design、Material UI、Tailwind |
| 服务端渲染 | Next.js、Remix |
谁在用 React
- Facebook / Instagram / WhatsApp
- Netflix、Airbnb、Uber
- 知乎、小红书等国内大厂
React 与 Vue 的选择
| 对比项 | React | Vue |
|---|---|---|
| 学习曲线 | JSX 较陡 | 模板较平缓 |
| 生态 | 庞大 | 完善 |
| 灵活度 | 高度灵活 | 约定更多 |
React 是"库"而非完整框架,路由、状态管理等由社区方案补齐;这也是它灵活的原因。
React 简介
深入了解 React 的核心概念:组件、JSX、Props、State,建立心智模型。
React 的哲学:声明式 UI
传统命令式开发(jQuery 时代):"先找到元素,再改内容"。React 声明式:"描述 UI 状态,React 自动更新"。
// 命令式(不推荐)
const el = document.getElementById('count');
el.textContent = count;
// 声明式(React 方式)
function Counter() {
const [count, setCount] = useState(0);
return <p>当前:{count}</p>;
}
五个核心概念
| 概念 | 说明 |
|---|---|
| 组件 | 函数或类,返回 UI 片段 |
| JSX | 在 JS 中写 HTML 的语法扩展 |
| Props | 父组件传给子组件的数据(只读) |
| State | 组件内部可变的数据 |
| Hooks | 函数组件中使用状态和副作用 |
组件树的渲染
App
├── Header
├── ArticleList
│ ├── ArticleCard
│ └── ArticleCard
└── Footer
数据自顶向下流动(单向),事件自底向上传递。
React 的版本演进
- React 16.8(2019):Hooks 引入,函数组件崛起
- React 18(2022):并发渲染、自动批处理
- React 19:Actions、编译器优化
什么时候用 React
- 复杂交互的 SPA(单页应用)
- 组件复用度高的大型前端
- 需要生态支持的团队项目
先理解"组件 + 数据流",比背 API 更重要——遇到问题都能从这两个概念推导出答案。
React 安装(NPM)
通过 NPM 安装 React 并搭建开发环境。
环境要求
- Node.js 18+(
node -v查看) - npm(随 Node 一起安装)
方式一:使用 Create React App(CRA)
# 创建项目(较慢,已不推荐新项目使用)
npx create-react-app my-app
cd my-app
npm start
方式二:Vite(推荐)
npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev
方式三:手动安装(理解原理)
mkdir react-demo && cd react-demo
npm init -y
npm install react react-dom
# 开发依赖
npm install -D vite @vitejs/plugin-react
验证安装
node -v # v22.x
npm -v # 10.x
项目结构(Vite)
my-app/
├── index.html # 入口 HTML
├── src/
│ ├── main.jsx # 应用入口
│ ├── App.jsx # 根组件
│ └── index.css
├── package.json
└── vite.config.js
常见问题
| 问题 | 解决 |
|---|---|
| npm 下载慢 | 配置淘宝镜像:npm config set registry https://registry.npmmirror.com |
| Node 版本过低 | 用 nvm 安装新版 Node |
| 端口被占用 | npm run dev -- --port 3001 |
新项目推荐 Vite:秒级启动、热更新快;CRA 已停止维护,不建议新项目使用。
React 安装(Vite)
使用 Vite 快速创建 React 项目——当前社区推荐的标配方案。
创建项目
npm create vite@latest my-react-app -- --template react
# 或者用类型模板
npm create vite@latest my-react-app -- --template react-ts
安装与启动
cd my-react-app
npm install
npm run dev # 启动开发服务器(默认 5173 端口)
构建生产版本
npm run build # 输出到 dist/
npm run preview # 本地预览构建结果
Vite 项目结构
my-react-app/
├── index.html # 入口(含 <div id="root">)
├── vite.config.js # Vite 配置
├── src/
│ ├── main.jsx # 入口:ReactDOM.createRoot
│ ├── App.jsx # 根组件
│ ├── App.css
│ └── assets/ # 静态资源
└── public/ # 公共文件
入口文件 main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
// 挂载到 root 节点
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
vite.config.js 常用配置
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3000, // 自定义端口
proxy: { // 开发代理
'/api': 'http://localhost:8000',
},
},
});
server.proxy 让开发时前端请求 /api 自动转发到后端(如 FastAPI 8000 端口),解决开发跨域。
React 安装(CDN)
通过 CDN 快速体验 React——无需安装任何东西,一个 HTML 文件即可运行。
最简 CDN 示例
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<!-- React CDN -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<!-- Babel 编译 JSX -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
function App() {
return <h1>Hello, React CDN!</h1>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
CDN 方式的特点
| 优点 | 缺点 |
|---|---|
| 零安装,双击即用 | 无打包优化,性能一般 |
| 适合学习/演示 | 不适合生产大型应用 |
| 可用于 Codepen 等在线平台 | 依赖 CDN 可用性 |
在线体验平台
- CodeSandbox:完整 React 环境
- CodePen:快速片段实验
- StackBlitz:Vite 在线开发
- PlayCode:轻量在线 IDE
什么时候用 CDN
- 快速验证一个想法
- 写教程/演示代码
- 小型落地页(不推荐,用原生更好)
生产环境不要用 CDN + Babel 方式(体积大、无优化);用 Vite 构建才是正道。
React VSCode
在 VS Code 中配置 React 开发环境,获得智能提示与高效开发体验。
1. 安装扩展
- ES7+ React/Redux/React-Native snippets:代码片段(输入
rfce快速生成组件) - Prettier:代码格式化
- ESLint:代码检查
- Auto Import:自动导入
2. 设置 JSX 高亮
VS Code 默认支持 .jsx/.tsx 文件;对 .js 文件启用 JSX:
// settings.json
{
"files.associations": {
"*.js": "javascriptreact"
}
}
3. 配置 Emmet(快速输入)
{
"emmet.includeLanguages": {
"javascript": "javascriptreact"
}
}
输入 div.container>h1+p 按 Tab 自动展开为 JSX 结构。
4. Prettier 配置
// .prettierrc
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}
5. 调试配置
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Debug React",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/src"
}
]
}
6. 常用快捷键
| 快捷键 | 作用 |
|---|---|
rfce + Tab | 生成函数组件 |
rafce + Tab | 生成箭头函数组件 |
Cmd/Ctrl + D | 选中下一个相同词 |
Alt + ↑/↓ | 移动行 |
React 开发者工具(浏览器扩展)是调试必备:可查看组件树、Props、State、性能分析。
React Qoder
使用 Qoder(AI 编程工具)辅助 React 开发,让 AI 帮你写代码、改 bug。
Qoder 的核心能力
- 代码生成:自然语言描述 → React 组件
- 智能补全:上下文感知的代码提示
- Bug 修复:报错信息直接提问
- 重构建议:优化组件结构
示例:让 AI 生成组件
用户输入:
"帮我写一个 React 计数器组件,支持加、减、重置,显示当前值"
AI 输出:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>当前值:{count}</p>
<button => setCount(count + 1)}>+1</button>
<button => setCount(count - 1)}>-1</button>
<button => setCount(0)}>重置</button>
</div>
);
}
AI 辅助开发的注意点
- 生成代码要理解后再用,不要无脑复制
- 关键逻辑自己把关(状态、副作用)
- 给 AI 清晰的上下文(组件结构、技术栈)
- 让 AI 解释代码,学习效果更好
推荐提示词模板
1. "用 React + Vite 写一个 XX 组件,要求:..."
2. "这段代码为什么报错:<粘贴代码和报错>"
3. "把这段 JSX 重构为可复用组件"
4. "这个 useEffect 有什么问题?如何改进?"
AI 工具是放大器:基础扎实的人用 AI 效率翻倍,基础薄弱的人容易被带偏。先学原理,再用工具。
React 在线生成
使用在线生成工具快速创建 React 应用/组件,无需本地环境。
在线沙箱平台
| 平台 | 特点 |
|---|---|
| CodeSandbox | 功能完整,支持 Vite/Next,可分享 |
| StackBlitz | 浏览器内运行 Vite,速度极快 |
| CodePen | 轻量,适合单个组件/效果演示 |
| Replit | 支持全栈(前后端一起) |
CodeSandbox 使用流程
- 打开 codesandbox.io
- 选择模板:React / React+TS / Vite
- 在线编写代码,实时预览
- 一键分享链接或导入 GitHub
生成 React 应用的在线工具
# 在浏览器中体验(无需安装)
1. 打开 StackBlitz → New Project → React
2. 模板已含 Vite + React,直接写代码
3. 支持导入 npm 包
AI 生成 UI 工具
- v0.dev(Vercel):文字描述 → UI 组件
- Builder.io:AI 生成组件代码
- Figma → React:设计稿转组件
什么时候用在线工具
- 临时实验、快速原型
- 面试演示、代码分享
- 没有本地环境的场景
在线沙箱适合学习与演示;正式项目仍建议本地 Vite 开发,配合 Git 管理版本。
React 创建第一个项目
创建你的第一个 React 项目,从零理解 React 应用的启动流程。
快速创建
npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev
打开 http://localhost:5173 看到 Vite + React 欢迎页即成功。
清空模板,写自己的代码
// src/App.jsx
function App() {
return (
<div>
<h1>我的第一个 React 应用</h1>
<p>欢迎来到 CodeNest React 教程!</p>
</div>
);
}
export default App;
理解启动流程
index.html加载/src/main.jsxmain.jsx用createRoot挂载<App />到#rootApp.jsx返回的 JSX 被编译渲染为页面- Vite 热更新:改代码保存,页面自动刷新
运行三种模式
npm run dev # 开发:热更新
npm run build # 构建:输出 dist/
npm run preview # 预览构建产物
检查 React 是否运行
浏览器按 F12 → Console,React 开发工具扩展会显示组件树;修改 App.jsx 内容保存,页面应实时变化。
开发模式(dev)带热更新和错误提示,构建模式(build)才是生产用的压缩产物。
React 项目说明
解析 Vite + React 项目的每个文件,理解它们各自的作用。
关键文件详解
| 文件 | 作用 |
|---|---|
index.html | 页面入口,含 <div id="root"> 挂载点 |
src/main.jsx | 应用入口,创建根节点并渲染 App |
src/App.jsx | 根组件,组合所有页面/组件 |
src/App.css | 根组件样式 |
src/index.css | 全局样式 |
vite.config.js | Vite 构建配置(插件、代理) |
package.json | 依赖与脚本(dev/build/preview) |
public/ | 静态资源(原样拷贝) |
index.html 要点
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>My App</title>
</head>
<body>
<!-- React 挂载点 -->
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
package.json 脚本
{
"scripts": {
"dev": "vite", // 开发
"build": "vite build", // 构建
"preview": "vite preview", // 预览
"lint": "eslint ." // 检查
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
组件组织建议
src/
├── components/ # 通用组件
│ ├── Button.jsx
│ └── Card.jsx
├── pages/ # 页面组件
│ ├── Home.jsx
│ └── About.jsx
├── hooks/ # 自定义 Hook
│ └── useAuth.js
├── api/ # 接口请求
│ └── client.js
├── utils/ # 工具函数
│ └── format.js
└── App.jsx
项目变复杂后,按"组件/页面/Hook/接口/工具"分类组织文件,代码可维护性大幅提升。
React 元素渲染
理解 React 的元素渲染机制:元素、渲染过程、更新逻辑。
React 元素
React 元素是描述 UI 的不可变对象,通过 JSX 创建:
const element = <h1>Hello, World!</h1>;
// 编译后等价于:
// const element = React.createElement('h1', null, 'Hello, World!');
渲染到 DOM
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
// 渲染
root.render(<App />);
更新已渲染的元素
// 元素不可变,更新 = 重新渲染
function tick() {
const element = (
<div>
<h1>当前时间</h1>
<p>{new Date().toLocaleTimeString()}</p>
</div>
);
root.render(element);
}
setInterval(tick, 1000); // 每秒重新渲染
React 只更新变化的部分
React 通过虚拟 DOM diff 对比前后差异,只更新变化节点——即使每秒重新渲染整个元素,也只改动时间文本。
渲染规则
| 规则 | 说明 |
|---|---|
| 元素不可变 | 创建后不能修改其属性/子元素 |
| 更新靠重新渲染 | 用新元素替换旧元素 |
| 最小化 DOM 操作 | diff 后只更新差异部分 |
实际开发中
日常开发很少直接操作 root.render()——通常由 useState 触发组件重新渲染。理解元素机制有助于理解 React 的更新模型。
"元素不可变 + 重新渲染"是 React 的核心心智:状态变了 → 重新调用组件函数 → 得到新 UI 描述 → 高效更新 DOM。
React JSX
JSX 是 React 的语法糖:在 JavaScript 中写类似 HTML 的结构,编译后变成 React.createElement 调用。
为什么用 JSX
- 直观:UI 结构一目了然
- 安全:默认防 XSS 注入
- 表达力强:可嵌入 JS 表达式
JSX 嵌入表达式
const name = 'CodeNest';
const user = { name: '张三', age: 25 };
function Greeting() {
return (
<div>
<h1>你好,{name}!</h1> {/* 变量 */}
<p>年龄:{user.age + 1}</p> {/* 表达式 */}
<p>长度:{"hello".length}</p> {/* 函数调用 */}
</div>
);
}
JSX 属性
const imgUrl = 'https://example.com/logo.png';
// 属性用驼峰命名(className 而非 class)
<div className="card" id="main">
<img src={imgUrl} alt="Logo" />
<button disabled={false}>点击</button>
</div>
JSX 子元素
// 嵌套
<div>
<h1>标题</h1>
<p>内容</p>
</div>
// 数组渲染
const items = ['A', 'B', 'C'];
<ul>
{items.map(item => <li key={item}>{item}</li>)}
</ul>
JSX 条件渲染
function Status({ isLoggedIn }) {
return (
<div>
{isLoggedIn ? <p>已登录</p> : <p>未登录</p>}
{/* 短路渲染 */}
{isLoggedIn && <button>退出</button>}
</div>
);
}
JSX 注意事项
| 注意点 | 说明 |
|---|---|
| 必须包裹 | 多个元素要包在 <div> 或 <Fragment> 中 |
| className | class 是保留字,用 className |
| 自闭合 | 空标签要写 <img />、<br /> |
| 注释 | JSX 内注释用 {/* ... */} |
JSX 不是模板语言,就是 JavaScript——可以随意使用表达式、函数、逻辑运算。
React 组件
组件是 React 的核心构建单元。理解函数组件与类组件,掌握组件的组合方式。
函数组件(推荐)
// 定义
function Welcome({ name }) {
return <h1>你好,{name}</h1>;
}
// 使用
function App() {
return <Welcome name="CodeNest" />;
}
箭头函数组件
const Welcome = ({ name }) => (
<h1>你好,{name}</h1>
);
export default Welcome;
组件命名规则
- 组件名大写字母开头(PascalCase)
- 文件名与组件名一致:
Welcome.jsx - React 区分组件(大写)与 HTML 标签(小写)
组件的组合
function Avatar({ user }) {
return <img src={user.avatar} alt={user.name} />;
}
function UserInfo({ user }) {
return (
<div className="user-info">
<Avatar user={user} />
<span>{user.name}</span>
</div>
);
}
function App() {
const user = { name: '张三', avatar: '/a.png' };
return <UserInfo user={user} />;
}
组件 vs 实例
同一个组件可以渲染多个实例,各自独立:
function App() {
return (
<div>
<Counter />
<Counter /> {/* 独立实例,互不影响 */}
<Counter />
</div>
);
}
组件设计原则
- 单一职责:一个组件只做一件事
- 可复用:通过 Props 配置差异
- 组件树:大组件拆分为小组件组合
把页面想象成"积木":先搭大积木(页面),再用小积木(组件)填充细节,每个积木可单独复用。
React 组件状态
组件状态(State)是组件内部可变的数据,通过 useState Hook 管理。
useState 基础
import { useState } from 'react';
function Counter() {
// count 是当前值,setCount 是更新函数
const [count, setCount] = useState(0);
return (
<div>
<p>点击了 {count} 次</p>
<button => setCount(count + 1)}>
点击
</button>
</div>
);
}
更新状态的正确姿势
function Counter() {
const [count, setCount] = useState(0);
// 推荐:函数式更新(基于旧值)
const increment = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 连加两次也正确
};
// 避免:直接依赖旧值(多次调用有问题)
// const increment = () => setCount(count + 1);
return <button
}
更新对象状态
function Profile() {
const [user, setUser] = useState({ name: '张三', age: 25 });
// 必须创建新对象(不可变更新)
const updateName = () => {
setUser(prev => ({ ...prev, name: '李四' }));
};
return (
<div>
<p>{user.name} - {user.age}岁</p>
<button
</div>
);
}
更新数组状态
function TodoList() {
const [todos, setTodos] = useState(['学习 React']);
const addTodo = () => {
setTodos(prev => [...prev, '新任务']); // 追加
};
const removeTodo = (index) => {
setTodos(prev => prev.filter((_, i) => i !== index)); // 删除
};
return (
<div>
<ul>{todos.map((t, i) => <li key={i}>{t}</li>)}</ul>
<button
</div>
);
}
State 的要点
| 要点 | 说明 |
|---|---|
| 不可变 | 永远创建新值,不直接修改旧值 |
| 异步更新 | setState 后不能立刻读新值(用函数式更新) |
| 触发重渲染 | 状态变化 → 组件重新渲染 |
Props 是"外部传入",State 是"内部自有"——两者的区别是 React 数据流的核心。
React Props
Props(属性)是父组件传给子组件的数据,在子组件中只读。
传递 Props
function Card({ title, content, author }) {
return (
<div className="card">
<h3>{title}</h3>
<p>{content}</p>
<small>作者:{author}</small>
</div>
);
}
function App() {
return (
<div>
<Card title="React 入门" content="..." author="老王" />
<Card title="JSX 详解" content="..." author="老王" />
</div>
);
}
Props 类型
function Product({ id, name, price, inStock, tags, onBuy }) {
return (
<div>
<p>{id} - {name}:¥{price}</p>
<p>{inStock ? '有货' : '缺货'},标签:{tags.join(', ')}</p>
<button => onBuy(id)}>购买</button>
</div>
);
}
// 传各种类型
<Product
id={1}
name="键盘"
price={299.0}
inStock={true}
tags={['外设', '办公']}
=> console.log('购买', id)}
/>
默认 Props
function Greeting({ name = '游客', age = 0 }) {
return <p>{name}({age}岁)</p>;
}
// 或
Greeting.defaultProps = { name: '游客' };
children(子元素)
function Card({ children, title }) {
return (
<div className="card">
<h3>{title}</h3>
{children} {/* 组件内部的内容 */}
</div>
);
}
<Card title="提示">
<p>这里是卡片内容</p>
</Card>
Props 单向流动
数据从父组件流向子组件,子组件不能修改 Props。要改数据,应该在父组件中通过状态管理:
function App() {
const [count, setCount] = useState(0);
return (
<Counter
count={count}
=> setCount(count + 1)}
/>
);
}
"Props 向下传,事件向上报"是 React 的数据流铁律——父组件管状态,子组件管展示。
React 事件处理
React 的事件处理:绑定事件、传递参数、合成事件。
绑定事件
function Button() {
const handleClick = () => {
console.log('点击了!');
};
return <button
}
内联写法
<button => console.log('click')}>内联</button>
事件对象
function Input() {
const handleChange = (e) => {
console.log(e.target.value); // 输入的值
console.log(e.target.name); // 元素 name
};
return <input name="username" />;
}
传递参数
function List({ items }) {
const handleDelete = (id) => {
console.log('删除', id);
};
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
<button => handleDelete(item.id)}>
删除
</button>
</li>
))}
</ul>
);
}
阻止默认行为
function Form() {
const handleSubmit = (e) => {
e.preventDefault(); // 阻止表单刷新
console.log('提交!');
};
return (
<form
<button type="submit">提交</button>
</form>
);
}
常见事件
| 事件 | 触发时机 |
|---|---|
onClick | 点击 |
onChange | 输入框内容变化 |
onSubmit | 表单提交 |
onMouseEnter | 鼠标移入 |
onKeyDown | 按键按下 |
onFocus / onBlur | 聚焦/失焦 |
React 事件是"合成事件"(统一封装),写法用驼峰(onClick),且事件委托在根节点,性能好。
React 条件渲染
条件渲染:根据条件显示不同 UI 的四种常用方式。
1. if / else 语句
function Status({ isLoggedIn }) {
if (isLoggedIn) {
return <p>欢迎回来!</p>;
}
return <p>请先登录</p>;
}
2. 三元运算符
function Status({ isLoggedIn }) {
return (
<div>
{isLoggedIn ? (
<button>退出登录</button>
) : (
<button>登录</button>
)}
</div>
);
}
3. 逻辑与(短路)
function Notification({ count }) {
return (
<div>
{count > 0 && <p>你有 {count} 条新消息</p>}
{/* count <= 0 时渲染 false(什么都不显示) */}
</div>
);
}
4. 逻辑或(默认值)
function Name({ user }) {
return <p>{user.name || '匿名用户'}</p>;
}
阻止渲染(返回 null)
function WarningBanner({ show }) {
if (!show) {
return null; // 不渲染任何内容
}
return <div className="warning">警告!</div>;
}
结合状态的条件渲染
function App() {
const [page, setPage] = useState('home');
return (
<div>
<nav>
<button => setPage('home')}>首页</button>
<button => setPage('about')}>关于</button>
</nav>
{page === 'home' && <Home />}
{page === 'about' && <About />}
</div>
);
}
多个分支用 if/else 或提前 return;简单二选一用三元;只显示/隐藏用 &&。
React 列表 & Keys
渲染列表与理解 Keys 的重要性——React 列表渲染的核心。
渲染列表
function ArticleList({ articles }) {
return (
<ul>
{articles.map(article => (
<li key={article.id}>
<h3>{article.title}</h3>
<p>{article.summary}</p>
</li>
))}
</ul>
);
}
为什么需要 key
key 帮助 React 识别哪些列表项被增删改,从而高效更新 DOM。没有 key(或用 index)在增删列表项时可能引发渲染错误。
// ✅ 用唯一 id
{items.map(item => <Item key={item.id} item={item} />)}
// ⚠️ 避免用 index(列表变化时可能错乱)
{items.map((item, index) => <Item key={index} item={item} />)}
key 的选择规则
| key | 适用 |
|---|---|
| 数据库 id | 首选(稳定唯一) |
| slug/URL | 有语义的唯一值 |
| index | 静态列表、无需重排时勉强可用 |
列表筛选
function FilteredList({ todos, filter }) {
const visible = todos.filter(todo => {
if (filter === 'active') return !todo.done;
if (filter === 'done') return todo.done;
return true;
});
return (
<ul>
{visible.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
提取列表项组件
function ArticleCard({ article, onSelect }) {
return (
<article => onSelect(article.id)}>
<h3>{article.title}</h3>
<p>{article.summary}</p>
</article>
);
}
// key 放在 map 的组件上
{articles.map(a => (
<ArticleCard key={a.id} article={a} />
))}
key 只需在同层兄弟节点间唯一;不要在组件内部自己生成 key(index 随机数都不可靠)。
React 组件 API
组件 API:React 组件对外暴露的属性和方法,主要是 Props 与 State 的规范使用。
组件的两种数据
| 类型 | 来源 | 可变 | 用途 |
|---|---|---|---|
| Props | 父组件传入 | ❌ 只读 | 配置组件 |
| State | 组件自身 | ✅ 可变 | 管理内部数据 |
Props 的规范使用
function UserCard({ user, onEdit, showEmail = true }) {
return (
<div>
<p>{user.name}</p>
{showEmail && <p>{user.email}</p>}
<button => onEdit(user.id)}>编辑</button>
</div>
);
}
State 提升(共享状态)
多个子组件需要共享状态时,把状态提升到共同的父组件:
function App() {
const [selectedId, setSelectedId] = useState(null);
return (
<div>
<List />
<Detail id={selectedId} /> {/* 两个组件共享状态 */}
</div>
);
}
受控组件(表单)
function ControlledInput() {
const [value, setValue] = useState('');
// 值由 React 状态控制 → 受控组件
return (
<input
value={value}
=> setValue(e.target.value)}
/>
);
}
非受控组件
function UncontrolledInput() {
// 值由 DOM 自己管理,用 ref 读取
const inputRef = useRef(null);
const readValue = () => {
console.log(inputRef.current.value);
};
return (
<div>
<input ref={inputRef} defaultValue="初始值" />
<button
</div>
);
}
组件通信总结
| 场景 | 方案 |
|---|---|
| 父 → 子 | Props |
| 子 → 父 | 回调函数(Props 传函数) |
| 兄弟组件 | 状态提升到父组件 |
| 深层/全局 | Context 或状态库(Redux/Zustand) |
大部分场景用"Props 向下 + 回调向上"即可解决;别一上来就上状态库,先想清楚层级关系。
React 组件生命周期
组件生命周期:组件从挂载到卸载的过程。函数组件用 useEffect 实现生命周期逻辑。
生命周期的三个阶段
| 阶段 | 时机 | 函数组件对应 |
|---|---|---|
| 挂载 | 组件第一次渲染 | useEffect(..., []) |
| 更新 | Props/State 变化 | useEffect(..., [deps]) |
| 卸载 | 组件从 DOM 移除 | useEffect 清理函数 |
挂载时执行一次
import { useEffect, useState } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// 组件挂载时请求数据
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser);
}, [userId]); // 依赖变化才重新执行
return user ? <p>{user.name}</p> : <p>加载中...</p>;
}
依赖数组的三种写法
// 1. 空数组:只挂载时执行一次
useEffect(() => { load(); }, []);
// 2. 带依赖:依赖变化时执行
useEffect(() => { load(id); }, [id]);
// 3. 不传:每次渲染都执行(慎用)
useEffect(() => { console.log('每次渲染'); });
卸载时清理
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器运行中');
}, 1000);
// 清理函数:组件卸载时执行
return () => clearInterval(timer);
}, []);
订阅/取消订阅
useEffect(() => {
// 订阅事件
window.addEventListener('resize', handleResize);
// 卸载时取消订阅(防止内存泄漏)
return () => window.removeEventListener('resize', handleResize);
}, []);
React 18 StrictMode 注意
开发模式下 StrictMode 会双调用 effect(挂载→卸载→挂载),用于暴露清理函数缺失的问题——生产环境无影响。
把"数据请求、订阅、计时器"等副作用统一放 useEffect,并记得在清理函数中回收资源。
React AJAX
在 React 中发送 AJAX 请求:请求时机、Loading 状态、错误处理。
用 useEffect 发请求
import { useEffect, useState } from 'react';
function ArticleList() {
const [articles, setArticles] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false; // 防止卸载后 setState
fetch('/api/articles')
.then(r => {
if (!r.ok) throw new Error('请求失败');
return r.json();
})
.then(data => {
if (!cancelled) {
setArticles(data.items);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err.message);
setLoading(false);
}
});
return () => { cancelled = true; };
}, []);
if (loading) return <p>加载中...</p>;
if (error) return <p>出错了:{error}</p>;
return (
<ul>
{articles.map(a => <li key={a.id}>{a.title}</li>)}
</ul>
);
}
封装请求 Hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(r => r.json())
.then(d => { if (!cancelled) { setData(d); setLoading(false); } })
.catch(e => { if (!cancelled) { setError(e); setLoading(false); } });
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
// 使用
function App() {
const { data, loading, error } = useFetch('/api/articles');
// ...
}
POST 请求
async function createArticle(formData) {
const res = await fetch('/api/articles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.message || '创建失败');
}
return res.json();
}
请求库 axios
import axios from 'axios';
async function getArticles() {
const res = await axios.get('/api/articles', {
params: { page: 1, page_size: 10 },
});
return res.data; // 自动解析 JSON
}
Loading 与错误处理最佳实践
- 三个状态:loading / data / error
- 卸载后不要 setState(用 cancelled 标志)
- 错误要展示给用户,而不是静默失败
- 重复请求可加防抖或取消(AbortController)
React 18 + Suspense 未来可简化数据请求;现在社区常用 TanStack Query(react-query)管理服务端状态。
React 表单与事件
React 表单与事件:受控表单、输入处理、表单提交的完整实践。
受控表单(推荐)
function LoginForm() {
const [form, setForm] = useState({ username: '', password: '' });
const handleChange = (e) => {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value })); // 动态更新字段
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('提交:', form);
// 发送请求...
};
return (
<form
<input
name="username"
value={form.username}
placeholder="用户名"
/>
<input
type="password"
name="password"
value={form.password}
placeholder="密码"
/>
<button type="submit">登录</button>
</form>
);
}
复选框与下拉
function Settings() {
const [prefs, setPrefs] = useState({ dark: false, lang: 'zh' });
return (
<div>
<label>
<input
type="checkbox"
checked={prefs.dark}
=> setPrefs({ ...prefs, dark: e.target.checked })}
/>
暗色模式
</label>
<select
value={prefs.lang}
=> setPrefs({ ...prefs, lang: e.target.value })}
>
<option value="zh">中文</option>
<option value="en">English</option>
</select>
</div>
);
}
多选与文件
// 多选
const handleTags = (e) => {
const options = [...e.target.selectedOptions];
const values = options.map(o => o.value);
setForm(f => ({ ...f, tags: values }));
};
// 文件
const handleFile = (e) => {
setForm(f => ({ ...f, file: e.target.files[0] }));
};
表单校验
function ValidatedForm() {
const [errors, setErrors] = useState({});
const validate = (form) => {
const err = {};
if (form.username.length < 3) err.username = '用户名至少 3 个字符';
if (!/^\S+@\S+\.\S+$/.test(form.email)) err.email = '邮箱格式不正确';
if (form.password.length < 6) err.password = '密码至少 6 位';
return err;
};
const handleSubmit = (e) => {
e.preventDefault();
const err = validate(form);
if (Object.keys(err).length > 0) {
setErrors(err);
return;
}
// 提交...
};
return (
<form
<input name="username" value={form.username} />
{errors.username && <p className="error">{errors.username}</p>}
{/* ... */}
</form>
);
}
复杂表单可用库:Formik / React Hook Form(性能好)+ zod/yup 校验,能省大量样板代码。
React Refs
Refs:访问 DOM 元素或保存跨渲染的数据,不触发重新渲染。
访问 DOM 元素
import { useRef, useEffect } from 'react';
function FocusInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // 挂载后自动聚焦
}, []);
return <input ref={inputRef} placeholder="自动聚焦" />;
}
测量元素尺寸
function MeasureBox() {
const boxRef = useRef(null);
const measure = () => {
const el = boxRef.current;
console.log(el.getBoundingClientRect()); // 位置尺寸
};
return (
<div>
<div ref={boxRef} className="box">测量我</div>
<button
</div>
);
}
保存可变值(不触发渲染)
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null); // 保存定时器 id
const start = () => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
};
return (
<div>
<p>{count} 秒</p>
<button
<button
</div>
);
}
useRef vs useState
| 对比项 | useRef | useState |
|---|---|---|
| 变化触发渲染 | ❌ 不触发 | ✅ 触发 |
| 用途 | DOM 访问、保存值 | UI 数据 |
| 读取 | ref.current | state 变量 |
forwardRef(透传 ref)
import { forwardRef } from 'react';
// 让父组件能访问子组件的 DOM
const CustomInput = forwardRef(function CustomInput(props, ref) {
return <input ref={ref} {...props} />;
});
function Parent() {
const inputRef = useRef(null);
return (
<div>
<CustomInput ref={inputRef} />
<button => inputRef.current.focus()}>聚焦</button>
</div>
);
}
能用 state 表达 UI 就用 state;需要"命令式"操作 DOM(聚焦、滚动、测量)时才用 ref。
React 条件判断
条件判断进阶:复杂条件渲染、切换组件、列表条件过滤。
多条件分支
function StatusBadge({ status }) {
if (status === 'pending') return <span className="badge yellow">待处理</span>;
if (status === 'processing') return <span className="badge blue">处理中</span>;
if (status === 'done') return <span className="badge green">已完成</span>;
if (status === 'failed') return <span className="badge red">失败</span>;
return <span>未知状态</span>;
}
用对象映射(优雅)
const STATUS_MAP = {
pending: { label: '待处理', cls: 'yellow' },
processing: { label: '处理中', cls: 'blue' },
done: { label: '已完成', cls: 'green' },
failed: { label: '失败', cls: 'red' },
};
function StatusBadge({ status }) {
const conf = STATUS_MAP[status] || { label: '未知', cls: 'gray' };
return <span className={`badge ${conf.cls}`}>{conf.label}</span>;
}
切换不同组件
function Page({ type }) {
switch (type) {
case 'home': return <Home />;
case 'about': return <About />;
case 'contact': return <Contact />;
default: return <NotFound />;
}
}
条件渲染与列表组合
function Dashboard({ todos }) {
const doneCount = todos.filter(t => t.done).length;
return (
<div>
<p>进度:{doneCount}/{todos.length}</p>
{todos.length === 0 ? (
<p className="empty">暂无任务 🎉</p>
) : (
<ul>
{todos.map(todo => (
<li key={todo.id} className={todo.done ? 'done' : ''}>
{todo.text}
</li>
))}
</ul>
)}
</div>
);
}
防抖搜索(条件 + 定时器)
function SearchBox({ onSearch }) {
const [keyword, setKeyword] = useState('');
useEffect(() => {
// 输入停止 300ms 后才搜索
const timer = setTimeout(() => onSearch(keyword), 300);
return () => clearTimeout(timer);
}, [keyword]);
return (
<input
value={keyword}
=> setKeyword(e.target.value)}
placeholder="搜索..."
/>
);
}
条件分支多时,优先"对象映射"或"switch",比堆叠三元表达式可读性强得多。
React 路由
React Router:实现多页面应用(SPA)的路由跳转。
安装
npm install react-router-dom
基本路由配置
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/articles">文章</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/articles" element={<ArticleList />} />
<Route path="/articles/:id" element={<ArticleDetail />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
Link 与 useNavigate
import { Link, useNavigate } from 'react-router-dom';
// 声明式导航
<Link to="/articles/42">查看文章 42</Link>
// 编程式导航
function LoginButton() {
const navigate = useNavigate();
const handleLogin = () => {
// 登录成功后跳转
navigate('/dashboard', { state: { from: 'login' } });
};
return <button
}
路由参数
import { useParams } from 'react-router-dom';
function ArticleDetail() {
const { id } = useParams(); // 读取 :id
useEffect(() => {
fetch(`/api/articles/${id}`).then(...);
}, [id]);
return <div>文章 {id}</div>;
}
查询参数
import { useSearchParams } from 'react-router-dom';
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const keyword = searchParams.get('q') || '';
const updateKeyword = (value) => {
setSearchParams({ q: value }); // URL 变为 ?q=xxx
};
return (
<input
value={keyword}
=> updateKeyword(e.target.value)}
/>
);
}
嵌套路由与 Outlet
import { Outlet } from 'react-router-dom';
function Layout() {
return (
<div>
<Header />
<main>
<Outlet /> {/* 子路由渲染位置 */}
</main>
<Footer />
</div>
);
}
// 配置
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
</Route>
用 Link 而非 <a> 做站内跳转(避免整页刷新);需要操作后用 useNavigate。
React Memo
React.memo:优化组件渲染性能,避免不必要的重复渲染。
为什么需要 memo
父组件重新渲染时,子组件默认也会重新渲染。如果子组件计算昂贵(或 props 没变),这就是浪费。
import { memo, useState } from 'react';
// 普通组件:父组件每次渲染都会重新渲染
function Expensive({ data }) {
console.log('渲染 Expensive');
return <div>{data.title}</div>;
}
// memo 组件:props 没变时不重新渲染
const MemoExpensive = memo(function Expensive({ data }) {
console.log('渲染 MemoExpensive(props 变了才渲染)');
return <div>{data.title}</div>;
});
function App() {
const [count, setCount] = useState(0);
return (
<div>
<button => setCount(count + 1)}>{count}</button>
<MemoExpensive data={{ title: '固定数据' }} />
</div>
);
}
memo 的注意事项
- memo 是浅比较 props:对象/数组每次都是新引用,memo 失效
- 搭配
useCallback/useMemo保持引用稳定 - 不是所有组件都需要 memo,先测性能再优化
useCallback(缓存函数)
import { useCallback, memo } from 'react';
const Button = memo(function Button({ onClick, label }) {
console.log('渲染 Button');
return <button
});
function App() {
const [count, setCount] = useState(0);
// 缓存函数:count 不变时引用不变 → Button 不重渲染
const handleClick = useCallback(() => {
console.log('点击');
}, []);
return (
<div>
<p>{count}</p>
<Button label="按钮" />
</div>
);
}
useMemo(缓存计算结果)
import { useMemo } from 'react';
function Stats({ list }) {
// 计算昂贵:只有 list 变化才重新计算
const total = useMemo(() => {
return list.reduce((sum, item) => sum + item.price, 0);
}, [list]);
return <p>总价:{total}</p>;
}
何时使用
| 工具 | 场景 |
|---|---|
memo | 组件渲染昂贵且 props 常不变 |
useCallback | 传给 memo 子组件的函数 |
useMemo | 计算昂贵的派生数据 |
性能优化原则:先测量,再优化。盲目加 memo 反而可能因浅比较开销拖慢性能。
React CSS
在 React 中使用 CSS:全局样式、CSS Modules、CSS-in-JS 三种主流方式。
1. 全局 CSS(最基础)
// App.css
.container { max-width: 1200px; margin: 0 auto; }
.card { border: 1px solid #eee; border-radius: 8px; padding: 16px; }
// 组件中引入
import './App.css';
function Card() {
return <div className="card">卡片内容</div>;
}
2. CSS Modules(推荐,作用域隔离)
// Card.module.css
.card { border: 1px solid #eee; }
.title { font-size: 18px; color: #333; }
// Card.jsx
import styles from './Card.module.css';
function Card() {
return (
<div className={styles.card}>
<h3 className={styles.title}>标题</h3>
</div>
);
}
CSS Modules 会自动生成唯一类名(如 Card_card_x1y2z),避免样式冲突。
3. CSS-in-JS(styled-components)
npm install styled-components
import styled from 'styled-components';
const Button = styled.button`
background: #4f46e5;
color: white;
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
&:hover {
background: #4338ca;
}
${props => props.primary && css`
font-size: 18px;
font-weight: bold;
`}
`;
function App() {
return (
<div>
<Button>默认按钮</Button>
<Button primary>主按钮</Button>
</div>
);
}
4. 内联样式
function InlineStyle() {
const style = {
color: '#fff',
background: '#4f46e5',
padding: '8px 16px',
borderRadius: 4,
};
return <button style={style}>内联样式</button>;
}
方案对比
| 方案 | 作用域 | 动态样式 | 适用 |
|---|---|---|---|
| 全局 CSS | 全局 | 弱 | 小项目 |
| CSS Modules | 组件级 | 中 | 中大型(推荐) |
| styled-components | 组件级 | 强 | 需要主题定制 |
| Tailwind | 原子类 | 强 | 快速开发(见后续章节) |
新项目推荐 CSS Modules 或 Tailwind;styled-components 适合组件库/主题化场景。
React Sass
在 React 中使用 Sass(SCSS):变量、嵌套、混入等增强 CSS 的能力。
安装 Sass
npm install -D sass
Vite 无需额外配置,直接支持 .scss 文件。
Sass 变量
// styles/_variables.scss
$primary: #4f46e5;
$danger: #ef4444;
$radius: 8px;
$spacing: 16px;
// Card.scss
@use './variables';
.card {
border: 1px solid #eee;
border-radius: variables.$radius;
padding: variables.$spacing;
.title {
color: variables.$primary;
}
}
嵌套语法
.nav {
display: flex;
ul { list-style: none; }
li {
margin: 0 8px;
&:hover { // & 引用父选择器
color: $primary;
}
}
a {
text-decoration: none;
&::after {
content: '→';
}
}
}
混入(Mixin)
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin button-style($bg, $color: white) {
background: $bg;
color: $color;
padding: 10px 20px;
border: none;
border-radius: 6px;
}
.btn-primary {
@include button-style($primary);
}
.btn-danger {
@include button-style($danger);
}
.loading {
@include flex-center;
}
在 React 中使用
// Card.scss
.card {
padding: 16px;
border: 1px solid #ddd;
}
// Card.jsx
import './Card.scss';
function Card({ children }) {
return <div className="card">{children}</div>;
}
Sass 与 CSS Modules 结合
// Card.module.scss
.card { ... }
// Card.jsx
import styles from './Card.module.scss';
function Card() {
return <div className={styles.card}>...</div>;
}
现代 CSS 已有变量(var())和嵌套支持,小项目可以直接用原生 CSS;Sass 更适合复杂设计系统。
React Tailwind CSS
在 React 中使用 Tailwind CSS:原子化 CSS 框架,快速构建界面。
安装 Tailwind
npm install -D tailwindcss @tailwindcss/vite
// vite.config.js
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});
/* index.css */
@import "tailwindcss";
原子类示例
function Card() {
return (
<div className="max-w-sm mx-auto bg-white rounded-xl shadow-md p-6">
<h3 className="text-xl font-bold text-gray-800 mb-2">标题</h3>
<p className="text-gray-600 mb-4">内容描述...</p>
<button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">
按钮
</button>
</div>
);
}
常用类速查
| 类别 | 示例 |
|---|---|
| 布局 | flex items-center justify-between gap-4 |
| 间距 | p-4 m-2 mt-6 mb-4 px-3 |
| 文字 | text-lg font-bold text-red-500 text-center |
| 背景 | bg-blue-500 bg-gradient-to-r |
| 圆角阴影 | rounded-lg shadow-md border |
| 响应式 | md:flex lg:grid-cols-3 |
| 状态 | hover:bg-blue-700 focus:outline-none |
条件样式
function Button({ variant = 'primary' }) {
const base = 'px-4 py-2 rounded-lg font-medium';
const styles = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
danger: 'bg-red-500 text-white hover:bg-red-600',
ghost: 'bg-transparent text-gray-700 hover:bg-gray-100',
};
return <button className={`${base} ${styles[variant]}`}>按钮</button>;
}
Tailwind vs 传统 CSS
| 对比项 | Tailwind | 传统 CSS |
|---|---|---|
| 开发速度 | 快(免起类名) | 中 |
| 样式体积 | 按需生成,很小 | 取决于写多少 |
| 学习成本 | 类名体系 | CSS 本身 |
| 可维护性 | 组件内聚 | 样式表集中 |
Tailwind 与组件化天生契合:样式写在组件里,删组件即删样式,不产生死代码。
React Hooks
React Hooks:函数组件中使用状态与副作用的神器。本节系统梳理常用 Hooks。
Hooks 总览
| Hook | 用途 |
|---|---|
useState | 组件状态 |
useEffect | 副作用(请求/订阅/定时器) |
useContext | 读取 Context 值 |
useRef | DOM 引用 / 保存可变值 |
useMemo | 缓存计算结果 |
useCallback | 缓存函数 |
useReducer | 复杂状态管理 |
useLayoutEffect | 同步副作用(测量布局) |
useContext 跨层传值
import { createContext, useContext, useState } from 'react';
// 1. 创建 Context
const ThemeContext = createContext('light');
// 2. Provider 提供值
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
// 3. 任意层组件消费
function Toolbar() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button => setTheme(theme === 'light' ? 'dark' : 'light')}>
当前:{theme},点击切换
</button>
);
}
useReducer 复杂状态
import { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'add': return [...state, action.todo];
case 'toggle':
return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
case 'remove':
return state.filter(t => t.id !== action.id);
default: return state;
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(reducer, []);
return (
<div>
<button => dispatch({ type: 'add', todo: { id: Date.now(), text: '任务' } })}>
添加
</button>
{todos.map(t => (
<div key={t.id}>
<span => dispatch({ type: 'toggle', id: t.id })}>
{t.done ? '✅' : '⬜'} {t.text}
</span>
</div>
))}
</div>
);
}
自定义 Hook
// useWindowWidth.js
import { useState, useEffect } from 'react';
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handler = () => setWidth(window.innerWidth);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
return width;
}
// 使用(任何组件都能用)
function Layout() {
const width = useWindowWidth();
return <p>窗口宽度:{width}px</p>;
}
Hooks 使用规则
- 只在组件顶层调用(不在循环/条件/嵌套函数中)
- 只在函数组件或自定义 Hook中调用
- 自定义 Hook 命名以
use开头
Hook 的核心价值:逻辑复用。把可复用的逻辑抽成自定义 Hook,组件只负责 UI。
React 参考手册
整理 React 常用参考:API 速查、资源链接、常见问题。
常用 API 速查
| API | 作用 |
|---|---|
ReactDOM.createRoot(el).render(vdom) | 挂载应用 |
useState(init) | 返回 [state, setState] |
useEffect(fn, deps) | 副作用 |
useRef(init) | 可变引用 |
useContext(Ctx) | 读取 Context |
memo(Comp) | 组件记忆化 |
lazy(() => import()) | 代码分割懒加载 |
Suspense | 懒加载占位 |
代码分割(懒加载)
import { lazy, Suspense } from 'react';
// 路由级懒加载
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
学习资源
| 资源 | 链接/说明 |
|---|---|
| React 官方文档 | react.dev(新版,含交互式教程) |
| React Router | reactrouter.com |
| Ant Design | ant.design(企业级组件库) |
| Tailwind CSS | tailwindcss.com |
| Next.js | nextjs.org(React 全栈框架) |
常见问题
| 问题 | 解决 |
|---|---|
| Hooks 报错 "Invalid hook call" | 检查是否在条件/循环中调用 |
| 状态更新不生效 | 确认用了 setState 而非直接赋值 |
| 列表渲染错乱 | 检查 key 是否稳定唯一 |
| 组件不更新 | 检查是否修改了对象/数组(要不可变更新) |
| 警告 "useEffect deps" | 按提示补全依赖或使用 eslint-plugin-react-hooks |
遇到报错先读控制台完整信息;React 的错误提示非常友好,通常直接告诉你原因和修法。
React 测验
本章测验检验你对 React 的掌握程度。先独立作答,再看解析。
选择题
1. React 中创建组件最推荐的方式?
- A. 函数组件 + Hooks ✅
- B. 类组件
- C. HTML 模板
- D. jQuery 插件
2. 下列哪个是更新状态的正确写法?
- A.
count = count + 1 - B.
this.count++ - C.
setCount(count + 1)✅ - D.
count.value++
3. 父组件传给子组件的数据叫什么?
- A. State
- B. Props ✅
- C. Ref
- D. Store
4. 列表渲染时元素需要什么属性?
- A.
id - B.
name - C.
key✅ - D.
ref
5. 哪个 Hook 用于处理副作用(请求/定时器)?
- A.
useState - B.
useEffect✅ - C.
useRef - D.
useMemo
6. JSX 中 class 应写成?
- A.
class - B.
className✅ - C.
class-name - D.
cssClass
判断题
7. Props 在子组件中可以被修改。
❌ 错误。Props 是只读的,修改数据应通过父组件的 State。
8. useEffect 的依赖数组为空时,只在组件挂载后执行一次。
✅ 正确。[] 表示"没有依赖",只在首次挂载后执行。
简答题
9. 写一个计数器组件(useState 版)。
function Counter() {
const [count, setCount] = useState(0);
return (
<button => setCount(count + 1)}>
{count}
</button>
);
}
10. 如何让列表渲染正确且高效?
{items.map(item => (
<Item key={item.id} data={item} />
))}
要点:稳定唯一的 key(用 id 而非 index)。
计分
| 答对 | 水平 |
|---|---|
| 9-10 | 优秀,可开发真实项目 |
| 6-8 | 良好,巩固薄弱点 |
| <6 | 建议重学基础章节 |
React 重在实践:写完测验后,试着用 React 重做一个小项目(如待办清单),收获最大。
React 博客项目
综合运用 React 知识,构建一个完整的博客项目(前后端分离模式)。
项目规划
功能:
1. 文章列表(分页、分类筛选)
2. 文章详情
3. 搜索
4. 路由跳转
5. 主题切换(暗色/亮色)
6. 打包部署
项目结构
my-blog/
├── src/
│ ├── components/ # 通用组件
│ │ ├── Header.jsx
│ │ ├── ArticleCard.jsx
│ │ └── Pagination.jsx
│ ├── pages/ # 页面
│ │ ├── Home.jsx
│ │ ├── ArticleDetail.jsx
│ │ └── Search.jsx
│ ├── api/ # 接口
│ │ └── client.js
│ ├── hooks/ # 自定义 Hook
│ │ └── useFetch.js
│ ├── App.jsx
│ └── main.jsx
├── index.html
└── package.json
API 封装
// src/api/client.js
const BASE = '/api';
async function request(path, options = {}) {
const res = await fetch(`${BASE}${path}`, {
headers: { 'Content-Type': 'application/json' },
...options,
});
if (!res.ok) throw new Error(`请求失败:${res.status}`);
return res.json();
}
export const getArticles = (params) => request(`/articles?${new URLSearchParams(params)}`);
export const getArticle = (id) => request(`/articles/${id}`);
export const searchArticles = (q) => request(`/search?q=${encodeURIComponent(q)}`);
文章列表页
// src/pages/Home.jsx
import { useEffect, useState } from 'react';
import { getArticles } from '../api/client';
import ArticleCard from '../components/ArticleCard';
function Home() {
const [articles, setArticles] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
getArticles({ page: 1, page_size: 10 })
.then(data => setArticles(data.items))
.finally(() => setLoading(false));
}, []);
if (loading) return <p>加载中...</p>;
return (
<div className="grid">
{articles.map(a => <ArticleCard key={a.id} article={a} />)}
</div>
);
}
export default Home;
卡片组件
// src/components/ArticleCard.jsx
import { Link } from 'react-router-dom';
function ArticleCard({ article }) {
return (
<article className="card">
<h3>
<Link to={`/articles/${article.id}`}>{article.title}</Link>
</h3>
<p>{article.summary}</p>
<small>{article.created_at} · {article.views} 次浏览</small>
</article>
);
}
export default ArticleCard;
路由配置
// src/App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import ArticleDetail from './pages/ArticleDetail';
import Header from './components/Header';
function App() {
return (
<BrowserRouter>
<Header />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/articles/:id" element={<ArticleDetail />} />
</Routes>
</BrowserRouter>
);
}
export default App;
前后端分离:React 负责 UI,后端(FastAPI 等)提供 API;开发时用 Vite proxy 解决跨域。
JSX 渲染文章列表
在博客项目中,用 JSX 渲染文章列表:数据处理、条件渲染、空状态。
数据处理与渲染
// src/components/ArticleList.jsx
function ArticleList({ articles, loading, error }) {
// 加载中
if (loading) {
return <div className="loading">加载中...</div>;
}
// 错误
if (error) {
return <div className="error">加载失败:{error.message}</div>;
}
// 空状态
if (articles.length === 0) {
return <div className="empty">暂无文章 🎉</div>;
}
// 正常列表
return (
<div className="article-list">
{articles.map(article => (
<article key={article.id} className="card">
<h3>{article.title}</h3>
<p className="summary">{article.summary}</p>
<div className="meta">
<span>{article.category_name}</span>
<span>{article.created_at?.slice(0, 10)}</span>
<span>👁 {article.views}</span>
</div>
{article.tags && (
<div className="tags">
{article.tags.map(tag => (
<span key={tag} className="tag">#{tag}</span>
))}
</div>
)}
</article>
))}
</div>
);
}
export default ArticleList;
渲染要点
- 四种状态都要处理:loading / error / empty / data
key用文章 id(稳定唯一)- 可选链
?.slice(0, 10)防字段缺失报错 - 嵌套数组(tags)用内层 map 渲染
父组件使用
function Home() {
const { data, loading, error } = useFetch('/api/articles');
return (
<ArticleList
articles={data?.items || []}
loading={loading}
error={error}
/>
);
}
把"列表渲染"抽成独立组件,天然支持复用和测试;数据从父组件通过 Props 传入。
响应式数据
理解 React 的响应式数据:状态变化如何驱动 UI 更新。
什么是响应式
React 中,"数据(State)变化 → 组件自动重新渲染 → UI 更新"——你只管更新数据,React 负责更新界面。
function LikeButton() {
const [liked, setLiked] = useState(false);
const [count, setCount] = useState(0);
const handleLike = () => {
setLiked(!liked);
setCount(c => c + 1);
};
return (
<button className={liked ? 'liked' : ''}>
{liked ? '❤️ 已赞' : '🤍 点赞'}({count})
</button>
);
}
响应式的核心机制
- 组件首次渲染时,读取所有 State
- State 变化 → React 标记该组件"需要更新"
- 重新调用组件函数 → 生成新 UI 描述(虚拟 DOM)
- diff 对比 → 只更新变化的部分(真实 DOM)
派生数据(响应式计算)
function Cart({ items }) {
// 总价是派生数据:由 items 计算而来,不需要单独 state
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
const count = items.length;
return (
<div>
<p>共 {count} 件,合计 ¥{total}</p>
</div>
);
}
状态更新是异步的
function Example() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // 还是旧值 0(异步更新)
};
// 需要新值时用函数式更新
const handleDouble = () => {
setCount(prev => prev * 2);
};
return <button
}
避免多余的 state
能用派生数据(计算得出)就不要存 state;两个 state 总是同步变化时,合并成一个对象或提升到父组件。
"State 是唯一数据源":UI 永远由当前 State 推导,不要试图手动改 DOM。
拆分组件
拆分组件:把大组件拆成可复用的小组件,提升代码质量。
为什么要拆组件
- 单一职责:每个组件只做一件事
- 可复用:相同 UI 只写一次
- 可维护:每个文件短小清晰
- 可测试:独立单元便于测试
拆分示例:文章卡片
// 拆分前:一个大组件
function ArticleCard({ article }) {
return (
<div className="card">
<div className="author">
<img src={article.author.avatar} alt="" />
<div>
<p>{article.author.name}</p>
<small>{article.created_at}</small>
</div>
</div>
<h3>{article.title}</h3>
<p>{article.summary}</p>
<div className="actions">
<button>收藏</button>
<button>分享</button>
</div>
</div>
);
}
// 拆分后
function Author({ author, date }) {
return (
<div className="author">
<img src={author.avatar} alt={author.name} />
<div>
<p>{author.name}</p>
<small>{date}</small>
</div>
</div>
);
}
function CardActions({ onFavorite, onShare }) {
return (
<div className="actions">
<button
<button
</div>
);
}
function ArticleCard({ article }) {
return (
<div className="card">
<Author author={article.author} date={article.created_at} />
<h3>{article.title}</h3>
<p>{article.summary}</p>
<CardActions
=> console.log('收藏')}
=> console.log('分享')}
/>
</div>
);
}
拆分原则
| 信号 | 行动 |
|---|---|
| 组件超过 100 行 | 考虑拆分 |
| 有可复用的 UI 片段 | 抽成独立组件 |
| Props 过多(5+) | 考虑分组/拆组件 |
| 同一逻辑多处出现 | 抽成 Hook 或组件 |
组合而非继承
function Panel({ title, actions, children }) {
return (
<div className="panel">
<div className="panel-header">
<h3>{title}</h3>
{actions}
</div>
{children}
</div>
);
}
// 使用:通过 children 组合不同内容
<Panel title="文章" actions={<button>新建</button>}>
<ArticleList />
</Panel>
拆分的尺度:可读性优先。不要拆得太碎(过度抽象),也不要一个大组件写到天荒地老。
路由跳转
博客项目的路由跳转:声明式导航、编程式导航、参数传递。
声明式导航(Link)
import { Link } from 'react-router-dom';
function Header() {
return (
<nav>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
{/* 带参数 */}
<Link to={`/articles/${article.id}`}>文章详情</Link>
{/* 带查询参数 */}
<Link to="/search?q=react">搜索 React</Link>
</nav>
);
}
编程式导航(useNavigate)
import { useNavigate } from 'react-router-dom';
function LoginPage() {
const navigate = useNavigate();
const handleLogin = async (e) => {
e.preventDefault();
// 登录成功后
navigate('/dashboard');
// 或带 state
navigate('/dashboard', { state: { from: 'login' } });
// 或替换历史(防后退)
navigate('/dashboard', { replace: true });
};
return <form
}
读取参数
import { useParams, useSearchParams, useLocation } from 'react-router-dom';
function ArticleDetail() {
const { id } = useParams(); // /articles/42 → 42
const [searchParams] = useSearchParams(); // ?page=2
const location = useLocation(); // 完整 location
const page = searchParams.get('page');
useEffect(() => {
fetch(`/api/articles/${id}?page=${page}`).then(...);
}, [id, page]);
return <div>文章 {id} 第 {page} 页</div>;
}
路由守卫(登录拦截)
import { Navigate } from 'react-router-dom';
function ProtectedRoute({ children }) {
const token = localStorage.getItem('token');
if (!token) {
return <Navigate to="/login" replace />; // 未登录跳转
}
return children;
}
// 使用
<Route path="/dashboard" element={
<ProtectedRoute><Dashboard /></ProtectedRoute>
} />
404 页面
function NotFound() {
return (
<div className="not-found">
<h1>404</h1>
<p>页面不存在</p>
<Link to="/">返回首页</Link>
</div>
);
}
// 路由末尾加通配
<Route path="*" element={<NotFound />} />
SPA 路由刷新 404 问题:需要服务器把未知路径都重写到 index.html(Nginx try_files)。
副作用与数据请求
博客项目的副作用与数据请求:加载、缓存、取消、竞态处理。
基础数据请求 Hook
// src/hooks/useFetch.js
import { useState, useEffect } from 'react';
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController(); // 取消控制器
let active = true;
setLoading(true);
fetch(url, { signal: controller.signal, ...options })
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then(d => { if (active) { setData(d); setError(null); } })
.catch(e => {
if (active && e.name !== 'AbortError') setError(e);
})
.finally(() => { if (active) setLoading(false); });
return () => {
active = false; // 防止卸载后 setState
controller.abort(); // 取消请求
};
}, [url]);
return { data, loading, error };
}
export default useFetch;
处理请求竞态
// 竞态:快速切换 userId 时,旧请求可能后到
function UserPage({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => {
if (!cancelled) setUser(data); // 只接受最新请求的结果
});
return () => { cancelled = true; }; // 切换时作废旧请求
}, [userId]);
return user ? <p>{user.name}</p> : null;
}
请求状态管理
// 状态枚举
const STATUS = { IDLE: 'idle', LOADING: 'loading', SUCCESS: 'success', ERROR: 'error' };
function useArticle(id) {
const [state, setState] = useState({ status: STATUS.IDLE, data: null, error: null });
const load = useCallback(() => {
setState(s => ({ ...s, status: STATUS.LOADING }));
fetch(`/api/articles/${id}`)
.then(r => r.json())
.then(data => setState({ status: STATUS.SUCCESS, data, error: null }))
.catch(error => setState({ status: STATUS.ERROR, data: null, error }));
}, [id]);
useEffect(load, [load]); // 加载
return { ...state, reload: load }; // 支持手动刷新
}
防抖搜索
function SearchBox({ onSearch }) {
const [keyword, setKeyword] = useState('');
useEffect(() => {
const timer = setTimeout(() => onSearch(keyword), 300);
return () => clearTimeout(timer); // 输入中断时清除
}, [keyword]);
return (
<input
value={keyword}
=> setKeyword(e.target.value)}
placeholder="搜索文章..."
/>
);
}
请求副作用三件套:loading 状态、错误处理、取消/防竞态。复杂应用可上 TanStack Query 自动处理这些。
useEffect 进阶
useEffect 进阶:依赖管理、防重复执行、与 ref 配合、严格模式。
依赖数组的深水区
function Example({ user }) {
// ❌ 对象依赖:每次渲染都是新引用 → 死循环风险
useEffect(() => {
// ...
}, [user]); // user 对象每次都是新引用
// ✅ 拆出原始值作为依赖
useEffect(() => {
// ...
}, [user.id]);
// ✅ 用 useMemo 稳定引用
const stableUser = useMemo(() => user, [user.id]);
useEffect(() => {
// ...
}, [stableUser]);
}
只在更新时执行(跳过首次)
function Counter() {
const [count, setCount] = useState(0);
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false; // 跳过首次
return;
}
console.log('count 变化了:', count); // 只在更新时打印
}, [count]);
return <button => setCount(c => c + 1)}>{count}</button>;
}
依赖函数时用 useCallback
function Parent() {
const [id, setId] = useState(1);
// 函数依赖:用 useCallback 保证引用稳定
const loadData = useCallback(() => {
fetch(`/api/data/${id}`).then(...);
}, [id]); // id 变化才重建函数
useEffect(() => {
loadData();
}, [loadData]); // 依赖稳定,不会死循环
return <Child />;
}
useLayoutEffect(测量布局)
import { useLayoutEffect, useRef, useState } from 'react';
function Measure() {
const ref = useRef(null);
const [width, setWidth] = useState(0);
// 同步执行(在浏览器绘制前),避免闪烁
useLayoutEffect(() => {
setWidth(ref.current.offsetWidth);
}, []);
return <div ref={ref}>宽度:{width}px</div>;
}
React 18 StrictMode 双执行
开发模式下 effect 会执行两次(mount → unmount → mount),用于暴露清理缺失问题。生产环境只执行一次,无需担心。
useEffect 的黄金法则:依赖数组写全(用 eslint-plugin-react-hooks 自动检查),清理函数记得返回。
自定义 Hook
自定义 Hook:抽取可复用逻辑,让组件更简洁。这是 React 逻辑复用的核心手段。
什么是自定义 Hook
以 use 开头的函数,内部可以使用其他 Hook,把可复用的逻辑(数据请求、表单、主题等)封装起来。
useToggle(布尔切换)
// useToggle.js
import { useState, useCallback } from 'react';
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
// 使用
function Modal() {
const { value: isOpen, setTrue: open, setFalse: close } = useToggle();
return (
<>
<button
{isOpen && <div className="modal"
</>
);
}
useLocalStorage(本地存储)
// useLocalStorage.js
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// 惰性初始化:读取已有值
const [value, setValue] = useState(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initialValue;
});
// 自动同步到 localStorage
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// 使用:主题持久化
function App() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<div className={theme}>
<button => setTheme(theme === 'light' ? 'dark' : 'light')}>
切换主题
</button>
</div>
);
}
useDebounce(防抖值)
// useDebounce.js
import { useState, useEffect } from 'react';
function useDebounce(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// 使用:搜索防抖
function Search() {
const [keyword, setKeyword] = useState('');
const debouncedKeyword = useDebounce(keyword, 500);
useEffect(() => {
if (debouncedKeyword) {
// 500ms 后执行搜索
searchAPI(debouncedKeyword);
}
}, [debouncedKeyword]);
return <input value={keyword} => setKeyword(e.target.value)} />;
}
自定义 Hook 的最佳实践
- 命名以
use开头(React 约定) - 返回"状态 + 操作"的组合
- 内部使用多个 Hook 没问题
- 放
src/hooks/目录统一管理
先写普通代码,发现多处重复时再抽成 Hook——重构优于提前设计。
全局状态
博客项目的全局状态管理:Context、Zustand、Redux 的选型与实践。
什么时候需要全局状态
- 多个组件共享同一份数据(用户信息、主题)
- 组件层级很深,Props 逐层传递很痛苦
- 跨页面共享状态(登录态、购物车)
方案一:Context(内置,够用)
// src/store/AuthContext.jsx
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [token, setToken] = useState(localStorage.getItem('token'));
const login = (data) => {
setToken(data.token);
setUser(data.user);
localStorage.setItem('token', data.token);
};
const logout = () => {
setToken(null);
setUser(null);
localStorage.removeItem('token');
};
return (
<AuthContext.Provider value={{ user, token, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);
// 使用
import { useAuth } from '../store/AuthContext';
function Header() {
const { user, logout } = useAuth();
return user ? (
<>
<span>你好,{user.name}</span>
<button
</>
) : (
<Link to="/login">登录</Link>
);
}
方案二:Zustand(轻量推荐)
npm install zustand
// src/store/useStore.js
import { create } from 'zustand';
const useStore = create((set) => ({
favorites: [],
addFavorite: (id) => set(s => ({ favorites: [...s.favorites, id] })),
removeFavorite: (id) => set(s => ({ favorites: s.favorites.filter(f => f !== id) })),
}));
// 任何组件中直接使用
function FavoriteButton({ articleId }) {
const favorites = useStore(s => s.favorites);
const addFavorite = useStore(s => s.addFavorite);
const isFav = favorites.includes(articleId);
return (
<button => (isFav ? removeFavorite(articleId) : addFavorite(articleId))}>
{isFav ? '❤️' : '🤍'}
</button>
);
}
方案对比
| 方案 | 复杂度 | 性能 | 适用 |
|---|---|---|---|
| Context | 低 | 中 | 低频全局数据(用户/主题) |
| Zustand | 低 | 高 | 中小项目(推荐) |
| Redux | 高 | 高 | 大型复杂应用 |
多数博客项目用 Context + useReducer 就够;状态复杂、更新频繁再引入 Zustand。
打包上线
React 项目打包上线:构建、部署、配置优化。
构建生产版本
npm run build
# 输出到 dist/ 目录(压缩、哈希文件名)
部署到 Nginx(静态托管)
server {
listen 443 ssl http2;
server_name blog.laowang.click;
root /www/wwwroot/blog/dist; # 构建产物目录
index index.html;
# SPA 路由:所有路径都回退到 index.html
location / {
try_files $uri $uri/ /index.html;
}
# 静态资源缓存
location /assets/ {
expires 30d;
add_header Cache-Control "public";
}
# API 反代到后端
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
SPA 刷新 404 的解决
关键配置:try_files $uri $uri/ /index.html;——所有未匹配的路径都返回 index.html,由前端路由接管。
环境变量
// .env.development
VITE_API_BASE=/api
// .env.production
VITE_API_BASE=https://api.laowang.click
// 代码中使用
const API = import.meta.env.VITE_API_BASE;
构建优化
- 路由懒加载(lazy + Suspense)
- 压缩图片、代码分包(Vite 自动)
- gzip/brotli 压缩(Nginx 开启)
gzip on;
gzip_types text/css application/javascript application/json;
gzip_min_length 1k;
部署到宝塔面板
- 本地
npm run build生成 dist - 宝塔新建纯静态站点,把 dist 内容上传到站点目录
- Nginx 配置 try_files 支持 SPA 路由
- 申请 SSL 证书,开启 HTTPS
- 配置 API 反代(/api → 后端 8000)
上线后检查
- 访问域名:页面正常、刷新不 404
- 控制台无报错
- API 请求正常(无跨域错误)
SPA 部署三件套:dist 上传、try_files 回退、API 反代。记住这三点,部署基本不踩坑。