学习目标
- 掌握真实项目中的 API 集成与认证流程
- 理解常见设计模式与反模式
- 学会调试与问题排查
- 完成一个综合实战项目
- 梳理学习路径与后续方向
学习时间安排
总时长:8-9小时
- API 集成与认证:2小时
- 常见模式与反模式:1.5小时
- 调试与排查:1.5小时
- 综合实战项目:3-4小时
第一部分:API 集成与认证 (2小时)
1.1 统一 API 客户端
Axios 封装(详细注释版)
javascript
// src/services/apiClient.js
// 导入axios
import axios from 'axios';
// 创建axios实例
const apiClient = axios.create({
// 基础URL,从环境变量读取
baseURL: process.env.REACT_APP_API_URL || 'http://localhost:8080/api',
// 请求超时时间(毫秒)
timeout: 30000,
// 默认请求头
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
// 请求拦截器
apiClient.interceptors.request.use(
(config) => {
// 从localStorage获取token
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 添加请求时间戳(用于调试)
config.metadata = { startTime: new Date() };
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
apiClient.interceptors.response.use(
(response) => {
// 计算请求耗时
if (response.config.metadata) {
response.config.metadata.endTime = new Date();
response.duration = response.config.metadata.endTime - response.config.metadata.startTime;
}
return response;
},
(error) => {
// 统一错误处理
if (error.response) {
switch (error.response.status) {
case 401:
// 未授权,清除token并跳转登录
localStorage.removeItem('token');
window.location.href = '/login';
break;
case 403:
// 禁止访问
console.error('Access denied');
break;
case 404:
// 资源不存在
console.error('Resource not found');
break;
case 500:
// 服务器错误
console.error('Server error');
break;
default:
console.error('Request error:', error.response.status);
}
} else if (error.request) {
// 请求已发出但无响应
console.error('Network error:', error.message);
} else {
console.error('Error:', error.message);
}
return Promise.reject(error);
}
);
// 封装GET请求
export function get(url, params = {}, config = {}) {
return apiClient.get(url, { params, ...config });
}
// 封装POST请求
export function post(url, data = {}, config = {}) {
return apiClient.post(url, data, config);
}
// 封装PUT请求
export function put(url, data = {}, config = {}) {
return apiClient.put(url, data, config);
}
// 封装PATCH请求
export function patch(url, data = {}, config = {}) {
return apiClient.patch(url, data, config);
}
// 封装DELETE请求
export function del(url, config = {}) {
return apiClient.delete(url, config);
}
// 导出axios实例(需要自定义请求时使用)
export default apiClient;
1.2 认证流程实现
认证 Context(详细注释版)
javascript
// src/context/AuthContext.js
// 导入React
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
// 导入API客户端
import apiClient from '../services/apiClient';
// 创建AuthContext
const AuthContext = createContext(null);
// 定义AuthProvider组件
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [token, setToken] = useState(() => localStorage.getItem('token'));
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// 从服务器获取当前用户信息
const fetchCurrentUser = useCallback(async () => {
if (!token) {
setLoading(false);
return;
}
try {
const response = await apiClient.get('/auth/me');
setUser(response.data);
} catch (err) {
setUser(null);
setToken(null);
localStorage.removeItem('token');
} finally {
setLoading(false);
}
}, [token]);
// 初始化时获取用户信息
useEffect(() => {
fetchCurrentUser();
}, [fetchCurrentUser]);
// 登录
const login = useCallback(async (credentials) => {
setError(null);
try {
const response = await apiClient.post('/auth/login', credentials);
const { token: newToken, user: userData } = response.data;
localStorage.setItem('token', newToken);
setToken(newToken);
setUser(userData);
return userData;
} catch (err) {
const message = err.response?.data?.message || 'Login failed';
setError(message);
throw new Error(message);
}
}, []);
// 登出
const logout = useCallback(() => {
localStorage.removeItem('token');
setToken(null);
setUser(null);
setError(null);
}, []);
// 刷新token
const refreshToken = useCallback(async () => {
try {
const response = await apiClient.post('/auth/refresh');
const { token: newToken } = response.data;
localStorage.setItem('token', newToken);
setToken(newToken);
return newToken;
} catch (err) {
logout();
throw err;
}
}, [logout]);
const value = {
user,
token,
loading,
error,
isAuthenticated: !!user,
login,
logout,
refreshToken,
fetchCurrentUser,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// 定义useAuth Hook
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
受保护路由组件(详细注释版)
javascript
// src/components/ProtectedRoute.js
// 导入React
import React from 'react';
// 导入路由组件
import { Navigate, useLocation } from 'react-router-dom';
// 导入认证Hook
import { useAuth } from '../context/AuthContext';
// 定义ProtectedRoute组件
function ProtectedRoute({ children, requiredRoles = [] }) {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) {
return (
<div className="loading-container">
<div className="spinner"></div>
<p>Loading...</p>
</div>
);
}
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (requiredRoles.length > 0) {
const hasRole = requiredRoles.some(role => user.roles?.includes(role));
if (!hasRole) {
return <Navigate to="/unauthorized" replace />;
}
}
return children;
}
export default ProtectedRoute;
第二部分:常见模式与反模式 (1.5小时)
2.1 推荐模式
组合优于继承(详细注释版)
javascript
// src/patterns/Composition.js
// 组合模式示例:使用children和render props
// 基础布局组件
function Layout({ header, sidebar, children, footer }) {
return (
<div className="layout">
{header && <header className="layout-header">{header}</header>}
<div className="layout-body">
{sidebar && <aside className="layout-sidebar">{sidebar}</aside>}
<main className="layout-main">{children}</main>
</div>
{footer && <footer className="layout-footer">{footer}</footer>}
</div>
);
}
// 使用组合
function App() {
return (
<Layout
header={<Header />}
sidebar={<Sidebar />}
footer={<Footer />}
>
<PageContent />
</Layout>
);
}
// Render Props 模式
function DataFetcher({ url, render }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return render({ data, loading, error });
}
// 使用DataFetcher
function UserList() {
return (
<DataFetcher
url="/api/users"
render={({ data, loading, error }) => {
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}}
/>
);
}
容器与展示组件分离(详细注释版)
javascript
// src/patterns/ContainerPresentational.js
// 展示组件:只负责UI,不包含业务逻辑
function UserListPresentational({ users, onUserClick, loading }) {
if (loading) {
return <div>Loading users...</div>;
}
return (
<ul className="user-list">
{users.map(user => (
<li
key={user.id}
onClick={() => onUserClick(user)}
className="user-list-item"
>
{user.name} - {user.email}
</li>
))}
</ul>
);
}
// 容器组件:负责数据获取和业务逻辑
function UserListContainer() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
});
}, []);
const handleUserClick = (user) => {
navigate(`/users/${user.id}`);
};
return (
<UserListPresentational
users={users}
onUserClick={handleUserClick}
loading={loading}
/>
);
}
2.2 常见反模式及修正
反模式:在渲染中创建对象或函数(详细注释版)
javascript
// 反模式:每次渲染都创建新对象,导致子组件不必要的重渲染
function ParentBad() {
return (
<Child
style={{ color: 'red' }}
onClick={() => console.log('clicked')}
/>
);
}
// 正确做法:使用useMemo和useCallback
function ParentGood() {
const style = useMemo(() => ({ color: 'red' }), []);
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <Child style={style} onClick={handleClick} />;
}
反模式:在useEffect中遗漏依赖(详细注释版)
javascript
// 反模式:依赖不完整,可能使用过期的闭包
function CounterBad() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []); // 缺少count依赖,count始终为0
return <div>{count}</div>;
}
// 正确做法:使用函数式更新或完整依赖
function CounterGood() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <div>{count}</div>;
}
反模式:直接修改 state(详细注释版)
javascript
// 反模式:直接修改state
function TodoListBad() {
const [todos, setTodos] = useState([{ id: 1, text: 'Item', done: false }]);
const toggleBad = (id) => {
const todo = todos.find(t => t.id === id);
todo.done = !todo.done;
setTodos(todos);
};
return null;
}
// 正确做法:返回新对象或新数组
function TodoListGood() {
const [todos, setTodos] = useState([{ id: 1, text: 'Item', done: false }]);
const toggleGood = (id) => {
setTodos(prevTodos =>
prevTodos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
)
);
};
return null;
}
第三部分:调试与问题排查 (1.5小时)
3.1 React DevTools 使用
调试用 Hooks(详细注释版)
javascript
// src/hooks/useDebugValue.example.js
// 自定义Hook中使用useDebugValue,在DevTools中显示友好信息
import { useState, useDebugValue } from 'react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
useDebugValue(count, (value) => `Count: ${value}`);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
return { count, increment, decrement };
}
3.2 错误边界与错误上报
错误边界与上报(详细注释版)
javascript
// src/components/ErrorBoundaryWithReport.js
// 导入React
import React, { Component } from 'react';
// 错误上报函数(可替换为真实监控接口)
function reportError(error, errorInfo) {
console.error('Error reported:', error, errorInfo);
// 实际项目中发送到Sentry等
// Sentry.captureException(error, { extra: errorInfo });
}
class ErrorBoundaryWithReport extends Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
reportError(error, {
componentStack: errorInfo.componentStack,
digest: errorInfo.digest,
});
}
handleRetry = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
return (
<div className="error-boundary-fallback">
<h2>Something went wrong</h2>
<button onClick={this.handleRetry}>Try again</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundaryWithReport;
3.3 性能问题排查
渲染次数检测(详细注释版)
javascript
// src/hooks/useRenderCount.js
// 用于开发环境统计组件渲染次数
import { useRef, useEffect } from 'react';
function useRenderCount(componentName = 'Component') {
const renderCount = useRef(0);
renderCount.current += 1;
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
console.log(`[${componentName}] render count:`, renderCount.current);
}
});
return renderCount.current;
}
export default useRenderCount;
第四部分:综合实战项目 (3-4小时)
项目:待办与用户管理一体化应用
项目结构说明(详细注释版)
react-final-app/
├── public/
├── src/
│ ├── api/ # API 模块
│ │ ├── client.js
│ │ ├── todos.js
│ │ └── auth.js
│ ├── components/
│ │ ├── common/
│ │ ├── layout/
│ │ └── features/
│ ├── context/
│ │ └── AuthContext.js
│ ├── hooks/
│ ├── pages/
│ ├── routes/
│ │ └── ProtectedRoute.js
│ ├── store/ # 可选 Redux
│ ├── utils/
│ ├── App.js
│ └── index.js
├── .env.example
├── package.json
└── README.md
API 模块:待办(详细注释版)
javascript
// src/api/todos.js
import { get, post, put, del } from './client';
const BASE = '/todos';
export const todosApi = {
list(params) {
return get(BASE, params).then(res => res.data);
},
getById(id) {
return get(`${BASE}/${id}`).then(res => res.data);
},
create(payload) {
return post(BASE, payload).then(res => res.data);
},
update(id, payload) {
return put(`${BASE}/${id}`, payload).then(res => res.data);
},
remove(id) {
return del(`${BASE}/${id}`);
},
};
待办页面(详细注释版)
javascript
// src/pages/TodosPage.js
import React, { useState, useEffect, useCallback } from 'react';
import { todosApi } from '../api/todos';
function TodosPage() {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [text, setText] = useState('');
const [filter, setFilter] = useState('all');
const loadTodos = useCallback(async () => {
setLoading(true);
try {
const data = await todosApi.list();
setItems(Array.isArray(data) ? data : data?.items ?? []);
} catch (err) {
setItems([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadTodos();
}, [loadTodos]);
const addTodo = async (e) => {
e.preventDefault();
if (!text.trim()) return;
try {
const created = await todosApi.create({ text: text.trim(), completed: false });
setItems(prev => [...prev, created]);
setText('');
} catch (err) {
console.error(err);
}
};
const toggleTodo = async (id) => {
const item = items.find(i => i.id === id);
if (!item) return;
try {
const updated = await todosApi.update(id, { completed: !item.completed });
setItems(prev => prev.map(i => (i.id === id ? updated : i)));
} catch (err) {
console.error(err);
}
};
const removeTodo = async (id) => {
try {
await todosApi.remove(id);
setItems(prev => prev.filter(i => i.id !== id));
} catch (err) {
console.error(err);
}
};
const filtered = items.filter(item => {
if (filter === 'active') return !item.completed;
if (filter === 'completed') return item.completed;
return true;
});
if (loading) {
return <div className="page-loading">Loading...</div>;
}
return (
<div className="todos-page">
<h1>Todos</h1>
<form onSubmit={addTodo}>
<input
value={text}
onChange={e => setText(e.target.value)}
placeholder="What needs to be done?"
/>
<button type="submit">Add</button>
</form>
<div className="filters">
{['all', 'active', 'completed'].map(f => (
<button
key={f}
className={filter === f ? 'active' : ''}
onClick={() => setFilter(f)}
>
{f}
</button>
))}
</div>
<ul className="todo-list">
{filtered.map(item => (
<li key={item.id} className={item.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={item.completed}
onChange={() => toggleTodo(item.id)}
/>
<span>{item.text}</span>
<button type="button" onClick={() => removeTodo(item.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
export default TodosPage;
路由配置(详细注释版)
javascript
// src/App.js
import React from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './context/AuthContext';
import ProtectedRoute from './routes/ProtectedRoute';
import Layout from './components/layout/Layout';
import LoginPage from './pages/LoginPage';
import TodosPage from './pages/TodosPage';
function AppRoutes() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/"
element={
<ProtectedRoute>
<Layout />
</ProtectedRoute>
}
>
<Route index element={<Navigate to="/todos" replace />} />
<Route path="todos" element={<TodosPage />} />
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
);
}
export default function App() {
return (
<AuthProvider>
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
</AuthProvider>
);
}
登录页(详细注释版)
javascript
// src/pages/LoginPage.js
import React, { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [submitting, setSubmitting] = useState(false);
const { login, error } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from?.pathname || '/';
const handleSubmit = async (e) => {
e.preventDefault();
setSubmitting(true);
try {
await login({ email, password });
navigate(from, { replace: true });
} catch (err) {
// 错误由 context 处理
} finally {
setSubmitting(false);
}
};
return (
<div className="login-page">
<h1>Login</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
required
/>
</div>
{error && <div className="login-error">{error}</div>}
<button type="submit" disabled={submitting}>
{submitting ? 'Logging in...' : 'Login'}
</button>
</form>
</div>
);
}
export default LoginPage;
第五部分:学习路径总结与后续方向
5.1 14 天知识点回顾
- 第 1--2 天:ES6+(变量、函数、解构、模板字符串、模块、类)。
- 第 3 天:React 基础、JSX、组件、Props、State、事件。
- 第 4 天:Hooks(useState、useEffect、自定义 Hook)。
- 第 5 天:状态管理(Context、Redux 基础、组件通信)。
- 第 6 天:React Router、动态与嵌套路由、路由守卫。
- 第 7 天:性能(memo、useMemo、useCallback、虚拟列表、懒加载)。
- 第 8 天:测试(Jest、React Testing Library)。
- 第 9 天:React 18(useTransition、useDeferredValue、Suspense、批处理)。
- 第 10 天:TypeScript、构建工具、生产构建、项目结构。
- 第 11 天:SSR、Next.js、数据获取、API 路由。
- 第 12 天:企业实践(微前端、国际化、无障碍、错误处理)。
- 第 13 天:部署与 DevOps(构建、Docker、CI/CD、监控、安全)。
- 第 14 天:API 与认证、模式与反模式、调试、综合实战。
5.2 建议的后续学习方向
- 深入方向:React 源码与 Fiber、更多 Redux/状态库、GraphQL、React Native 基础。
- 工程化:Monorepo、微前端框架(qiankun/Module Federation)、E2E(Cypress/Playwright)。
- 领域:可视化(D3/ECharts)、实时(WebSocket)、PWA、Electron 等。
5.3 自测题(无答案,用于自查)
- 在 React 中,为什么更推荐组合而不是继承?
- 列出至少三个会导致不必要重渲染的写法,并说明如何避免。
- 如何设计一个可复用的、带加载与错误态的"请求数据 + 展示"的组件?
- 在生产环境中,你会如何设计前端的错误收集与上报?
- 简述你理解的 React 18 并发特性(如 useTransition)在体验上的作用。