React 18.x 学习计划 - 第十四天:实战整合与进阶收尾

学习目标

  • 掌握真实项目中的 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 建议的后续学习方向

  1. 深入方向:React 源码与 Fiber、更多 Redux/状态库、GraphQL、React Native 基础。
  2. 工程化:Monorepo、微前端框架(qiankun/Module Federation)、E2E(Cypress/Playwright)。
  3. 领域:可视化(D3/ECharts)、实时(WebSocket)、PWA、Electron 等。

5.3 自测题(无答案,用于自查)

  1. 在 React 中,为什么更推荐组合而不是继承?
  2. 列出至少三个会导致不必要重渲染的写法,并说明如何避免。
  3. 如何设计一个可复用的、带加载与错误态的"请求数据 + 展示"的组件?
  4. 在生产环境中,你会如何设计前端的错误收集与上报?
  5. 简述你理解的 React 18 并发特性(如 useTransition)在体验上的作用。
相关推荐
The_Uniform_C@t22 小时前
PWN | 对CTF WIKI的复现+再学习 (第八期)
网络·学习·网络安全·二进制
前路不黑暗@3 小时前
Java项目:Java脚手架项目的登录认证服务(十三)
java·spring boot·笔记·学习·spring·spring cloud·maven
前路不黑暗@4 小时前
Java项目:Java脚手架项目的 C 端用户服务(十五)
java·开发语言·spring boot·学习·spring cloud·maven·mybatis
Hello_Embed5 小时前
Modbus 传感器开发:STM32F030 libmodbus 移植
笔记·stm32·学习·freertos·modbus
知识分享小能手5 小时前
SQL Server 2019入门学习教程,从入门到精通,SQL Server 2019 视图操作 — 语法知识点及使用方法详解(16)
sql·学习·sqlserver
_Eleven5 小时前
Tailwind CSS vs UnoCSS 深度对比
前端
NEXT066 小时前
TCP 与 UDP 核心差异及面试高分指南
前端·网络协议·面试
qq_24218863326 小时前
HTML 全屏烟花网页
前端·html