React API集成与路由

React API集成与路由

一、API集成

1. Fetch API

javascript 复制代码
import { useState, useEffect } from 'react';

function ApiWithFetch() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // GET请求
  const fetchUsers = async () => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch('https://jsonplaceholder.typicode.com/users', {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer your-token-here' // 如果有的话
        }
      });
      
      if (!response.ok) {
        throw new Error(`HTTP错误: ${response.status}`);
      }
      
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  // POST请求
  const createUser = async (userData) => {
    try {
      const response = await fetch('https://jsonplaceholder.typicode.com/users', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(userData)
      });
      
      if (!response.ok) {
        throw new Error('创建失败');
      }
      
      const newUser = await response.json();
      return newUser;
    } catch (err) {
      console.error('创建用户失败:', err);
      throw err;
    }
  };

  // PUT/PATCH请求
  const updateUser = async (id, updates) => {
    try {
      const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
        method: 'PUT', // 或 'PATCH'
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(updates)
      });
      
      if (!response.ok) {
        throw new Error('更新失败');
      }
      
      return await response.json();
    } catch (err) {
      console.error('更新用户失败:', err);
      throw err;
    }
  };

  // DELETE请求
  const deleteUser = async (id) => {
    try {
      const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
        method: 'DELETE',
      });
      
      if (!response.ok) {
        throw new Error('删除失败');
      }
      
      return true;
    } catch (err) {
      console.error('删除用户失败:', err);
      throw err;
    }
  };

  useEffect(() => {
    fetchUsers();
  }, []);

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;

  return (
    <div>
      <h1>用户列表</h1>
      {data && (
        <ul>
          {data.map(user => (
            <li key={user.id}>
              {user.name} - {user.email}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

2. Axios(推荐)

javascript 复制代码
# 安装axios
npm install axios
javascript 复制代码
import { useState, useEffect } from 'react';
import axios from 'axios';

// 创建axios实例
const api = axios.create({
  baseURL: 'https://jsonplaceholder.typicode.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  }
});

// 请求拦截器
api.interceptors.request.use(
  (config) => {
    // 添加token等
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 响应拦截器
api.interceptors.response.use(
  (response) => response.data,
  (error) => {
    if (error.response) {
      // 服务器返回错误状态码
      switch (error.response.status) {
        case 401:
          // 未授权,跳转登录
          window.location.href = '/login';
          break;
        case 403:
          // 权限不足
          console.error('权限不足');
          break;
        case 404:
          console.error('资源不存在');
          break;
        case 500:
          console.error('服务器错误');
          break;
        default:
          console.error('请求失败');
      }
    } else if (error.request) {
      // 请求已发出但没有响应
      console.error('网络错误,请检查网络连接');
    } else {
      // 请求配置错误
      console.error('请求配置错误:', error.message);
    }
    return Promise.reject(error);
  }
);

function ApiWithAxios() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // GET请求
  const fetchUsers = async () => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await api.get('/users');
      setUsers(response);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  // POST请求
  const createUser = async (userData) => {
    try {
      const response = await api.post('/users', userData);
      return response;
    } catch (err) {
      console.error('创建失败:', err);
      throw err;
    }
  };

  // 并发请求
  const fetchMultipleData = async () => {
    try {
      const [users, posts] = await Promise.all([
        api.get('/users'),
        api.get('/posts')
      ]);
      
      return { users: users.data, posts: posts.data };
    } catch (err) {
      console.error('并发请求失败:', err);
      throw err;
    }
  };

  // 带取消请求
  const fetchWithCancel = async () => {
    const source = axios.CancelToken.source();
    
    try {
      const response = await api.get('/users', {
        cancelToken: source.token
      });
      return response.data;
    } catch (err) {
      if (axios.isCancel(err)) {
        console.log('请求被取消:', err.message);
      } else {
        throw err;
      }
    }
    
    // 取消请求
    return () => {
      source.cancel('组件卸载,取消请求');
    };
  };

  useEffect(() => {
    fetchUsers();
  }, []);

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;

  return (
    <div>
      <h1>用户列表 (Axios)</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

3. React Query(强烈推荐)

javascript 复制代码
# 安装React Query
npm install @tanstack/react-query
javascript 复制代码
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from './api'; // 你的axios实例

function UsersWithReactQuery() {
  const queryClient = useQueryClient();

  // 获取用户列表
  const { 
    data: users, 
    isLoading, 
    error, 
    refetch 
  } = useQuery({
    queryKey: ['users'], // 查询键
    queryFn: () => api.get('/users'),
    staleTime: 5 * 60 * 1000, // 5分钟内数据不过期
    cacheTime: 10 * 60 * 1000, // 10分钟后清除缓存
    retry: 3, // 失败重试3次
    retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
  });

  // 创建用户
  const createUserMutation = useMutation({
    mutationFn: (userData) => api.post('/users', userData),
    onSuccess: () => {
      // 用户创建成功后,使users查询无效,触发重新获取
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
    onError: (error) => {
      console.error('创建用户失败:', error);
    }
  });

  // 更新用户
  const updateUserMutation = useMutation({
    mutationFn: ({ id, ...updates }) => api.put(`/users/${id}`, updates),
    onSuccess: (updatedUser) => {
      // 乐观更新:立即更新UI,不用等重新获取
      queryClient.setQueryData(['users'], (oldUsers) =>
        oldUsers.map(user =>
          user.id === updatedUser.id ? updatedUser : user
        )
      );
    }
  });

  // 删除用户
  const deleteUserMutation = useMutation({
    mutationFn: (id) => api.delete(`/users/${id}`),
    onMutate: async (id) => {
      // 取消正在进行的查询
      await queryClient.cancelQueries({ queryKey: ['users'] });
      
      // 保存之前的用户列表
      const previousUsers = queryClient.getQueryData(['users']);
      
      // 乐观更新
      queryClient.setQueryData(['users'], (oldUsers) =>
        oldUsers.filter(user => user.id !== id)
      );
      
      return { previousUsers };
    },
    onError: (err, id, context) => {
      // 出错时回滚
      queryClient.setQueryData(['users'], context.previousUsers);
    },
    onSettled: () => {
      // 完成后重新获取
      queryClient.invalidateQueries({ queryKey: ['users'] });
    }
  });

  if (isLoading) return <div>加载中...</div>;
  if (error) return <div>错误: {error.message}</div>;

  return (
    <div>
      <h1>用户列表 (React Query)</h1>
      <button onClick={() => refetch()}>刷新</button>
      
      <button onClick={() => 
        createUserMutation.mutate({ 
          name: '新用户', 
          email: 'new@example.com' 
        })
      }>
        添加用户
      </button>
      
      {users?.map(user => (
        <div key={user.id} style={{ margin: '10px 0', padding: '10px', border: '1px solid #ddd' }}>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
          <button onClick={() => 
            updateUserMutation.mutate({ 
              id: user.id, 
              name: `${user.name} (已更新)` 
            })
          }>
            更新
          </button>
          <button onClick={() => deleteUserMutation.mutate(user.id)}>
            删除
          </button>
        </div>
      ))}
    </div>
  );
}

4. 自定义API Hook

javascript 复制代码
// hooks/useApi.js
import { useState, useCallback } from 'react';
import { api } from '../services/api';

export function useApi() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [data, setData] = useState(null);

  const request = useCallback(async (config) => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await api({
        method: config.method || 'GET',
        url: config.url,
        data: config.data,
        params: config.params
      });
      
      setData(response);
      return response;
    } catch (err) {
      setError(err.response?.data?.message || err.message);
      throw err;
    } finally {
      setLoading(false);
    }
  }, []);

  const get = useCallback((url, params) => 
    request({ method: 'GET', url, params }), [request]);
  
  const post = useCallback((url, data) => 
    request({ method: 'POST', url, data }), [request]);
  
  const put = useCallback((url, data) => 
    request({ method: 'PUT', url, data }), [request]);
  
  const del = useCallback((url) => 
    request({ method: 'DELETE', url }), [request]);

  return {
    loading,
    error,
    data,
    request,
    get,
    post,
    put,
    delete: del,
    setData,
    setError,
    clear: () => {
      setError(null);
      setData(null);
    }
  };
}

// 使用示例
function ApiHookExample() {
  const { 
    loading, 
    error, 
    data: users, 
    get: fetchUsers,
    post: createUser,
    put: updateUser,
    delete: deleteUser
  } = useApi();

  const handleFetch = async () => {
    await fetchUsers('/users');
  };

  const handleCreate = async () => {
    await createUser('/users', {
      name: '新用户',
      email: 'new@example.com'
    });
    handleFetch(); // 重新获取
  };

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;

  return (
    <div>
      <button onClick={handleFetch}>获取用户</button>
      <button onClick={handleCreate}>创建用户</button>
      
      {users && (
        <ul>
          {users.map(user => (
            <li key={user.id}>
              {user.name}
              <button onClick={() => deleteUser(`/users/${user.id}`)}>删除</button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

二、路由(React Router)

1. 基础路由配置

复制代码
# 安装React Router
npm install react-router-dom
javascript 复制代码
// App.jsx
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import { Home, About, Users, UserDetail, Dashboard, Settings, NotFound } from './pages';

function App() {
  return (
    <BrowserRouter>
      <div className="app">
        {/* 导航栏 */}
        <nav>
          <ul>
            <li><Link to="/">首页</Link></li>
            <li><Link to="/about">关于</Link></li>
            <li><Link to="/users">用户</Link></li>
            <li><Link to="/dashboard">仪表盘</Link></li>
          </ul>
        </nav>

        {/* 路由配置 */}
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/users" element={<Users />} />
          <Route path="/users/:id" element={<UserDetail />} />
          <Route path="/dashboard/*" element={<DashboardLayout />} />
          <Route path="*" element={<NotFound />} />
        </Routes>
      </div>
    </BrowserRouter>
  );
}

// 嵌套路由布局组件
function DashboardLayout() {
  return (
    <div className="dashboard-layout">
      <aside>
        <h3>仪表盘菜单</h3>
        <ul>
          <li><Link to="/dashboard">概览</Link></li>
          <li><Link to="/dashboard/settings">设置</Link></li>
          <li><Link to="/dashboard/analytics">分析</Link></li>
        </ul>
      </aside>
      
      <main>
        <Routes>
          <Route index element={<Dashboard />} />
          <Route path="settings" element={<Settings />} />
          <Route path="analytics" element={<Analytics />} />
        </Routes>
      </main>
    </div>
  );
}

2. 动态路由与参数

javascript 复制代码
// pages/UserDetail.jsx
import { useParams, useNavigate, useLocation, useSearchParams } from 'react-router-dom';

function UserDetail() {
  // 获取路由参数
  const { id } = useParams();
  
  // 编程式导航
  const navigate = useNavigate();
  
  // 获取location对象
  const location = useLocation();
  
  // 获取查询参数
  const [searchParams, setSearchParams] = useSearchParams();
  const tab = searchParams.get('tab') || 'info';
  const page = searchParams.get('page') || '1';
  
  const handleEdit = () => {
    // 导航到编辑页面
    navigate(`/users/${id}/edit`, { 
      state: { from: location.pathname }, // 传递状态
      replace: false // 替换当前记录
    });
  };
  
  const handleGoBack = () => {
    // 返回上一页
    navigate(-1);
  };
  
  const changeTab = (tabName) => {
    // 更新查询参数
    setSearchParams({ tab: tabName, page });
  };
  
  return (
    <div>
      <h1>用户详情 ID: {id}</h1>
      
      <div>
        <button onClick={handleEdit}>编辑用户</button>
        <button onClick={handleGoBack}>返回</button>
      </div>
      
      {/* Tab切换 */}
      <div>
        <button onClick={() => changeTab('info')}>基本信息</button>
        <button onClick={() => changeTab('posts')}>帖子</button>
        <button onClick={() => changeTab('comments')}>评论</button>
      </div>
      
      {/* 根据tab显示不同内容 */}
      {tab === 'info' && <UserInfo userId={id} />}
      {tab === 'posts' && <UserPosts userId={id} />}
      {tab === 'comments' && <UserComments userId={id} />}
    </div>
  );
}

3. 路由守卫(认证保护)

javascript 复制代码
// components/PrivateRoute.jsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';

function PrivateRoute({ children, roles = [] }) {
  const { isAuthenticated, user, loading } = useAuth();
  const location = useLocation();
  
  if (loading) {
    return <div>检查权限...</div>;
  }
  
  if (!isAuthenticated) {
    // 未登录,重定向到登录页
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  
  if (roles.length > 0 && !roles.includes(user.role)) {
    // 无权限,重定向到无权限页面
    return <Navigate to="/unauthorized" replace />;
  }
  
  return children;
}

// 使用示例
function AppRoutes() {
  return (
    <Routes>
      {/* 公开路由 */}
      <Route path="/" element={<Home />} />
      <Route path="/login" element={<Login />} />
      
      {/* 需要登录的路由 */}
      <Route path="/dashboard" element={
        <PrivateRoute>
          <Dashboard />
        </PrivateRoute>
      } />
      
      {/* 需要管理员权限的路由 */}
      <Route path="/admin" element={
        <PrivateRoute roles={['admin', 'superadmin']}>
          <AdminPanel />
        </PrivateRoute>
      } />
      
      {/* 404页面 */}
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

4. 懒加载路由

javascript 复制代码
import { Suspense, lazy } from 'react';
import { Routes, Route } from 'react-router-dom';

// 懒加载组件
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Users = lazy(() => import('./pages/Users'));
const UserDetail = lazy(() => import('./pages/UserDetail'));

// 加载中组件
const Loading = () => <div>加载中...</div>;

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<Loading />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/users" element={<Users />} />
          <Route path="/users/:id" element={<UserDetail />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

5. 路由配置集中管理

javascript 复制代码
// routes/index.js
import { lazy } from 'react';

// 懒加载页面组件
const Home = lazy(() => import('../pages/Home'));
const About = lazy(() => import('../pages/About'));
const Users = lazy(() => import('../pages/Users'));
const UserDetail = lazy(() => import('../pages/UserDetail'));
const Login = lazy(() => import('../pages/Login'));
const Dashboard = lazy(() => import('../pages/Dashboard'));
const Admin = lazy(() => import('../pages/Admin'));
const NotFound = lazy(() => import('../pages/NotFound'));

// 路由配置
export const routes = [
  {
    path: '/',
    element: <Home />,
    meta: {
      title: '首页',
      requiresAuth: false
    }
  },
  {
    path: '/about',
    element: <About />,
    meta: {
      title: '关于我们',
      requiresAuth: false
    }
  },
  {
    path: '/login',
    element: <Login />,
    meta: {
      title: '登录',
      requiresAuth: false
    }
  },
  {
    path: '/users',
    element: <Users />,
    meta: {
      title: '用户列表',
      requiresAuth: true
    }
  },
  {
    path: '/users/:id',
    element: <UserDetail />,
    meta: {
      title: '用户详情',
      requiresAuth: true
    }
  },
  {
    path: '/dashboard',
    element: <Dashboard />,
    meta: {
      title: '仪表盘',
      requiresAuth: true,
      roles: ['user', 'admin']
    }
  },
  {
    path: '/admin',
    element: <Admin />,
    meta: {
      title: '管理后台',
      requiresAuth: true,
      roles: ['admin']
    }
  },
  {
    path: '*',
    element: <NotFound />,
    meta: {
      title: '页面不存在',
      requiresAuth: false
    }
  }
];

// 路由渲染组件
export function renderRoutes(routes) {
  return (
    <Routes>
      {routes.map((route, index) => (
        <Route
          key={index}
          path={route.path}
          element={
            <RouteGuard
              element={route.element}
              requiresAuth={route.meta?.requiresAuth}
              roles={route.meta?.roles}
            />
          }
        />
      ))}
    </Routes>
  );
}

三、完整实战:博客系统

javascript 复制代码
// App.jsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import Layout from './components/Layout';
import Home from './pages/Home';
import Posts from './pages/Posts';
import PostDetail from './pages/PostDetail';
import CreatePost from './pages/CreatePost';
import EditPost from './pages/EditPost';
import Login from './pages/Login';
import Register from './pages/Register';
import Profile from './pages/Profile';
import Admin from './pages/Admin';
import NotFound from './pages/NotFound';
import { AuthProvider } from './contexts/AuthContext';
import PrivateRoute from './components/PrivateRoute';

// 创建React Query客户端
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5分钟
      cacheTime: 10 * 60 * 1000, // 10分钟
      retry: 2,
      refetchOnWindowFocus: false,
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <AuthProvider>
        <BrowserRouter>
          <Routes>
            {/* 公开路由 */}
            <Route path="/login" element={<Login />} />
            <Route path="/register" element={<Register />} />
            
            {/* 需要布局的受保护路由 */}
            <Route element={<PrivateRoute><Layout /></PrivateRoute>}>
              <Route path="/" element={<Home />} />
              <Route path="/posts" element={<Posts />} />
              <Route path="/posts/:id" element={<PostDetail />} />
              <Route path="/posts/create" element={
                <PrivateRoute roles={['admin', 'author']}>
                  <CreatePost />
                </PrivateRoute>
              } />
              <Route path="/posts/:id/edit" element={
                <PrivateRoute roles={['admin', 'author']}>
                  <EditPost />
                </PrivateRoute>
              } />
              <Route path="/profile" element={<Profile />} />
              <Route path="/admin" element={
                <PrivateRoute roles={['admin']}>
                  <Admin />
                </PrivateRoute>
              } />
            </Route>
            
            {/* 404 */}
            <Route path="*" element={<NotFound />} />
          </Routes>
        </BrowserRouter>
      </AuthProvider>
      
      {/* 开发工具 */}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}
javascript 复制代码
// pages/Posts.jsx
import { useQuery } from '@tanstack/react-query';
import { Link, useSearchParams } from 'react-router-dom';
import { api } from '../services/api';
import Pagination from '../components/Pagination';
import Loading from '../components/Loading';
import Error from '../components/Error';

function Posts() {
  const [searchParams, setSearchParams] = useSearchParams();
  const page = parseInt(searchParams.get('page') || '1');
  const limit = parseInt(searchParams.get('limit') || '10');
  const search = searchParams.get('search') || '';
  
  const { data, isLoading, error, isError } = useQuery({
    queryKey: ['posts', { page, limit, search }],
    queryFn: () => 
      api.get('/posts', {
        params: { 
          _page: page, 
          _limit: limit,
          q: search
        }
      }),
  });
  
  const handleSearch = (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const searchValue = formData.get('search');
    setSearchParams({ page: '1', search: searchValue });
  };
  
  if (isLoading) return <Loading />;
  if (isError) return <Error message={error.message} />;
  
  return (
    <div className="posts-page">
      <div className="page-header">
        <h1>文章列表</h1>
        <Link to="/posts/create" className="btn btn-primary">
          新建文章
        </Link>
      </div>
      
      {/* 搜索 */}
      <form onSubmit={handleSearch} className="search-form">
        <input
          type="text"
          name="search"
          defaultValue={search}
          placeholder="搜索文章..."
        />
        <button type="submit">搜索</button>
      </form>
      
      {/* 文章列表 */}
      <div className="posts-list">
        {data?.map(post => (
          <div key={post.id} className="post-card">
            <h3>
              <Link to={`/posts/${post.id}`}>{post.title}</Link>
            </h3>
            <p>{post.body.substring(0, 100)}...</p>
            <div className="post-meta">
              <span>作者: {post.author?.name || '未知'}</span>
              <span>日期: {new Date(post.createdAt).toLocaleDateString()}</span>
              <span>阅读: {post.views || 0}</span>
            </div>
            <div className="post-actions">
              <Link to={`/posts/${post.id}`} className="btn btn-sm btn-info">
                查看
              </Link>
              <Link to={`/posts/${post.id}/edit`} className="btn btn-sm btn-warning">
                编辑
              </Link>
            </div>
          </div>
        ))}
      </div>
      
      {/* 分页 */}
      {data?.length > 0 && (
        <Pagination
          currentPage={page}
          totalPages={Math.ceil(data.total / limit)}
          onPageChange={(newPage) => 
            setSearchParams({ page: newPage.toString(), search })
          }
        />
      )}
    </div>
  );
}
javascript 复制代码
// pages/PostDetail.jsx
import { useParams, useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '../services/api';
import { useAuth } from '../contexts/AuthContext';
import Loading from '../components/Loading';
import Error from '../components/Error';

function PostDetail() {
  const { id } = useParams();
  const navigate = useNavigate();
  const queryClient = useQueryClient();
  const { user } = useAuth();
  
  // 获取文章详情
  const { data: post, isLoading, isError, error } = useQuery({
    queryKey: ['post', id],
    queryFn: () => api.get(`/posts/${id}`),
  });
  
  // 获取评论
  const { data: comments } = useQuery({
    queryKey: ['comments', id],
    queryFn: () => api.get(`/posts/${id}/comments`),
  });
  
  // 删除文章
  const deleteMutation = useMutation({
    mutationFn: () => api.delete(`/posts/${id}`),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['posts'] });
      navigate('/posts');
    }
  });
  
  // 添加评论
  const addCommentMutation = useMutation({
    mutationFn: (comment) => api.post(`/posts/${id}/comments`, comment),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['comments', id] });
    }
  });
  
  const handleAddComment = async (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const content = formData.get('content');
    
    if (!content.trim()) return;
    
    await addCommentMutation.mutate({
      content,
      authorId: user.id,
      createdAt: new Date().toISOString()
    });
    
    e.target.reset();
  };
  
  if (isLoading) return <Loading />;
  if (isError) return <Error message={error.message} />;
  if (!post) return <div>文章不存在</div>;
  
  const canEdit = user && (user.role === 'admin' || post.authorId === user.id);
  
  return (
    <div className="post-detail">
      <article>
        <h1>{post.title}</h1>
        
        <div className="post-meta">
          <span>作者: {post.author?.name}</span>
          <span>发布日期: {new Date(post.createdAt).toLocaleDateString()}</span>
          <span>阅读量: {post.views}</span>
        </div>
        
        <div className="post-content">
          {post.content}
        </div>
        
        {/* 操作按钮 */}
        <div className="post-actions">
          {canEdit && (
            <>
              <button 
                className="btn btn-primary"
                onClick={() => navigate(`/posts/${id}/edit`)}
              >
                编辑
              </button>
              <button 
                className="btn btn-danger"
                onClick={() => {
                  if (window.confirm('确定要删除这篇文章吗?')) {
                    deleteMutation.mutate();
                  }
                }}
                disabled={deleteMutation.isLoading}
              >
                {deleteMutation.isLoading ? '删除中...' : '删除'}
              </button>
            </>
          )}
          <button 
            className="btn btn-secondary"
            onClick={() => navigate(-1)}
          >
            返回
          </button>
        </div>
      </article>
      
      {/* 评论区域 */}
      <section className="comments">
        <h3>评论 ({comments?.length || 0})</h3>
        
        {/* 添加评论表单 */}
        {user && (
          <form onSubmit={handleAddComment} className="add-comment-form">
            <textarea
              name="content"
              placeholder="写下你的评论..."
              rows="3"
              required
            />
            <button 
              type="submit" 
              disabled={addCommentMutation.isLoading}
            >
              {addCommentMutation.isLoading ? '提交中...' : '提交评论'}
            </button>
          </form>
        )}
        
        {/* 评论列表 */}
        <div className="comments-list">
          {comments?.map(comment => (
            <div key={comment.id} className="comment">
              <div className="comment-header">
                <strong>{comment.author?.name}</strong>
                <span>{new Date(comment.createdAt).toLocaleString()}</span>
              </div>
              <div className="comment-content">
                {comment.content}
              </div>
            </div>
          ))}
        </div>
      </section>
    </div>
  );
}

四、API和路由最佳实践

1. 项目结构

javascript 复制代码
src/
├── services/
│   ├── api.js          # axios实例和配置
│   ├── auth.js         # 认证相关API
│   ├── posts.js        # 文章相关API
│   └── users.js        # 用户相关API
├── hooks/
│   ├── useApi.js       # API自定义Hook
│   ├── useAuth.js      # 认证Hook
│   └── useQuery.js     # 查询Hook
├── contexts/
│   └── AuthContext.js  # 认证Context
├── pages/
│   ├── Home.jsx
│   ├── Posts.jsx
│   └── Profile.jsx
├── components/
│   ├── Layout/
│   ├── Loading/
│   └── Error/
├── routes/
│   └── index.js       # 路由配置
└── utils/
    └── constants.js

2. API服务层示例

javascript 复制代码
// services/api.js
import axios from 'axios';

// 创建axios实例
const api = axios.create({
  baseURL: process.env.REACT_APP_API_URL || 'http://localhost:3000/api',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// 请求拦截器
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 响应拦截器
api.interceptors.response.use(
  (response) => response.data,
  (error) => {
    if (error.response?.status === 401) {
      // token过期,清除本地存储
      localStorage.removeItem('token');
      localStorage.removeItem('user');
      // 跳转到登录页
      window.location.href = '/login';
    }
    return Promise.reject(error.response?.data || error.message);
  }
);

// 封装常用方法
export const get = (url, params) => api.get(url, { params });
export const post = (url, data) => api.post(url, data);
export const put = (url, data) => api.put(url, data);
export const del = (url) => api.delete(url);
export const patch = (url, data) => api.patch(url, data);

export default api;

五、常见问题解决

1. 跨域问题

javascript 复制代码
// 开发环境代理配置 (vite.config.js)
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}

2. 请求取消

javascript 复制代码
// 在组件卸载时取消请求
useEffect(() => {
  const controller = new AbortController();
  
  const fetchData = async () => {
    try {
      const response = await api.get('/data', {
        signal: controller.signal
      });
      // 处理响应
    } catch (err) {
      if (err.name === 'AbortError') {
        console.log('请求被取消');
      } else {
        // 处理其他错误
      }
    }
  };
  
  fetchData();
  
  return () => {
    controller.abort();
  };
}, []);

3. 路由权限控制

javascript 复制代码
// 基于角色的权限控制
export const checkPermission = (userRole, allowedRoles) => {
  if (!allowedRoles || allowedRoles.length === 0) return true;
  if (!userRole) return false;
  return allowedRoles.includes(userRole);
};

关键要点

  1. API集成

    • 使用axios处理HTTP请求

    • 使用React Query管理服务器状态

    • 封装自定义Hook复用逻辑

    • 统一错误处理和拦截器

  2. 路由管理

    • 使用React Router v6

    • 实现路由守卫保护页面

    • 使用懒加载优化性能

    • 合理组织路由结构

  3. 状态同步

    • 服务器状态用React Query

    • 全局客户端状态用Context或Zustand

    • 局部状态用useState/useReducer

  4. 错误处理

    • 统一的错误边界

    • 友好的错误提示

    • 请求重试机制

  5. 性能优化

    • 组件懒加载

    • 请求防抖/节流

    • 缓存策略优化

相关推荐
爱上妖精的尾巴2 小时前
8-1 WPS JS宏 String.raw等关于字符串的3种引用方式
前端·javascript·vue.js·wps·js宏·jsa
hvang19882 小时前
某花顺隐藏了重仓涨幅,通过chrome插件计算基金的重仓涨幅
前端·javascript·chrome
Async Cipher2 小时前
TypeScript 的用法
前端·typescript
web打印社区2 小时前
vue页面打印:printjs实现与进阶方案推荐
前端·javascript·vue.js·electron·html
We་ct3 小时前
LeetCode 30. 串联所有单词的子串:从暴力到高效,滑动窗口优化详解
前端·算法·leetcode·typescript
木卫二号Coding3 小时前
Docker-构建自己的Web-Linux系统-Ubuntu:22.04
linux·前端·docker
CHU7290353 小时前
一番赏盲盒抽卡机小程序:解锁惊喜体验与社交乐趣的多元功能设计
前端·小程序·php
RFCEO3 小时前
前端编程 课程十二、:CSS 基础应用 Flex 布局
前端·css·flex 布局·css3原生自带的布局模块·flexible box·弹性盒布局·垂直居中困难
天若有情6734 小时前
XiangJsonCraft v1.2.0重大更新解读:本地配置优先+全量容错,JSON解耦开发体验再升级
前端·javascript·npm·json·xiangjsoncraft