首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >2026年React数据获取的第三层:建立可靠的API层——从零散代码到生产级架构

2026年React数据获取的第三层:建立可靠的API层——从零散代码到生产级架构

作者头像
前端达人
发布2026-03-12 14:47:23
发布2026-03-12 14:47:23
150
举报
文章被收录于专栏:前端达人前端达人
image
image

前置阅读: 如果你还没看过本系列的前两篇,强烈建议先读:

这一篇建立在这些基础之上。

我最近接触了一个3年的React项目,代码库里有47个不同的fetch实现

47个。

每个都是这样的:

代码语言:javascript
复制
// components/UserProfile.js
const [user, setUser] = useState(null);
useEffect(() => {
  fetch('/api/users/' + userId)
    .then(r => r.json())
    .then(data => setUser(data))
    .catch(err =>console.error(err));
}, [userId]);

// components/PostList.js
const [posts, setPosts] = useState([]);
useEffect(() => {
  fetch('/api/posts?userId=' + userId)
    .then(r => r.json())
    .then(data => setPosts(data))
    .catch(err =>console.error(err));
}, [userId]);

// components/CommentForm.js
const handleSubmit = async () => {
const response = await fetch('/api/comments', {
    method: 'POST',
    body: JSON.stringify(data)
  });
const result = await response.json();
// ... 没有error handling
};

// ... 还有44个这样的

看起来很正常吗?这就是问题所在。每个实现都有细微的差异,每个都可能埋着bug

当他们需要添加认证token时,必须手动改这47个地方。当某个team发现了一个bug(比如忘记检查response.ok),修复代价是巨大的。

这一篇,我们要解决的问题是:如何建立一个可信赖的、可复用的API层,让你再也不用重复这47次工作。

第一部分:为什么你需要一个"API层"而不是零散的fetch

问题对比:没有 vs 有 API层

代码语言:javascript
复制
❌ 没有API层的代价
├─ 47个fetch的不同实现
├─ 错误处理逻辑散落在各个组件
├─ 认证token的管理混乱
├─ 某个team改了某个fetch的方式,其他team不知道
├─ 添加新功能(重试、超时、日志)需要改47个地方
├─ 新人加入不知道"规范"是什么
└─ 坑:某个组件因为一个月前的"优化",开始发送了100倍的请求

✅ 有API层的优势
├─ 所有请求通过一个中心入口
├─ 认证、错误处理、重试在一个地方
├─ 新增功能一次搞定,全应用生效
├─ 容易审计和监控(哪个endpoint被调用了)
├─ 新人快速上手(只需学一套API)
├─ 性能优化在一个地方(比如添加请求去重)
└─ 坑:如果API层有问题,会影响全应用(但更容易发现和修复)

成本对比:

有人做过计算,一个中等规模应用(50个API调用点):

  • 💰 没有API层:维护成本 = 50 × N(每次修改需要改多个地方)
  • 💰 有API层:维护成本 = 1 × N(只改一个地方)

换句话说,有API层能将维护成本降低50倍

第二部分:从零开始构建一个生产级的API层

第一步:基础的apiRequest函数

代码语言:javascript
复制
// api/client.js
const BASE_URL = process.env.REACT_APP_API_URL || 'https://api.example.com';

/**
 * 核心API请求函数
 * @param {string} endpoint - 相对路径,如 '/users/123'
 * @param {object} options - fetch选项
 * @returns {Promise} 响应数据
 */
exportasyncfunction apiRequest(endpoint, options = {}) {
const url = `${BASE_URL}${endpoint}`;

// 构建请求配置
const config = {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...options.headers,
    },
  };

try {
    const response = await fetch(url, config);

    // 检查HTTP状态码
    if (!response.ok) {
      // 尝试从响应体获取错误信息
      const errorData = await response.json().catch(() => ({}));
      
      const error = newError(
        errorData.message || `HTTP ${response.status}: ${response.statusText}`
      );
      error.status = response.status;
      error.data = errorData;
      
      throw error;
    }

    // 处理204 No Content(删除成功但无返回内容)
    if (response.status === 204) {
      returnnull;
    }

    // 解析响应体
    const data = await response.json();
    return data;
  } catch (error) {
    console.error(`API Error [${endpoint}]:`, error);
    throw error;
  }
}

第二步:便利方法(GET/POST/PATCH等)

代码语言:javascript
复制
// api/client.js(续)

/**
 * API方法集合
 * 提供简洁的调用方式
 */
exportconst api = {
/**
   * GET请求:获取数据
   * @example
   * const users = await api.get('/users');
   * const user = await api.get('/users/123');
   */
get: (endpoint, options) =>
    apiRequest(endpoint, { ...options, method: 'GET' }),

/**
   * POST请求:创建资源
   * @example
   * const newUser = await api.post('/users', { name: 'Alice', email: '...' });
   */
post: (endpoint, data, options) =>
    apiRequest(endpoint, {
      ...options,
      method: 'POST',
      body: JSON.stringify(data),
    }),

/**
   * PUT请求:完整替换资源
   * @example
   * const updated = await api.put('/users/123', { name: 'Bob', ... });
   */
put: (endpoint, data, options) =>
    apiRequest(endpoint, {
      ...options,
      method: 'PUT',
      body: JSON.stringify(data),
    }),

/**
   * PATCH请求:部分更新资源
   * @example
   * const updated = await api.patch('/users/123', { email: 'newemail@...' });
   */
patch: (endpoint, data, options) =>
    apiRequest(endpoint, {
      ...options,
      method: 'PATCH',
      body: JSON.stringify(data),
    }),

/**
   * DELETE请求:删除资源
   * @example
   * await api.delete('/users/123');
   */
delete: (endpoint, options) =>
    apiRequest(endpoint, { ...options, method: 'DELETE' }),
};

第三步:在组件中使用

代码语言:javascript
复制
// components/UserProfile.js
import { api } from'../api/client';
import { useState, useEffect } from'react';

function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;

    const loadUser = async () => {
      try {
        setLoading(true);
        setError(null);
        
        // ✅ 现在调用API非常简洁
        const data = await api.get(`/users/${userId}`);
        
        if (isMounted) {
          setUser(data);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };

    loadUser();

    return() => {
      isMounted = false;
    };
  }, [userId]);

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

return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

对比:改写前后

代码语言:javascript
复制
改写前:每个组件都要处理
├─ fetch调用
├─ .then(r => r.json())
├─ .catch()
└─ 错误处理

改写后:一行代码
└─ const data = await api.get('/users/123');

第三部分:生产级错误处理——5种错误的正确应对

问题:初级开发者的错误处理往往太粗糙

代码语言:javascript
复制
// ❌ 这样的错误处理基本无用
try {
  const data = await api.get('/users');
  setUsers(data);
} catch (err) {
  alert('出错了');  // 用户看到"出错了",但不知道是什么问题
}

为什么不够好?

  • 404(资源不存在)应该显示"没有找到该用户"
  • 401(未授权)应该跳转到登录页
  • 500(服务器错误)应该鼓励"稍后重试"
  • 网络错误应该显示"检查网络连接"

每种错误需要不同的处理逻辑,但初级代码把它们全部当成了"出错了"。

完整的错误处理系统

代码语言:javascript
复制
// api/errors.js

/**
 * 自定义API错误类
 * 用于区分不同类型的错误
 */
exportclass APIError extends Error {
constructor(message, status, data = {}) {
    super(message);
    this.status = status;
    this.data = data;
    this.name = 'APIError';
  }

// 便利属性:快速判断错误类型
get isNotFound() {
    returnthis.status === 404;
  }

get isUnauthorized() {
    returnthis.status === 401;
  }

get isForbidden() {
    returnthis.status === 403;
  }

get isValidationError() {
    returnthis.status === 400;
  }

get isServerError() {
    returnthis.status >= 500;
  }

get isNetworkError() {
    returnthis.message.includes('Network');
  }
}

// api/client.js(增强版本)

exportasyncfunction apiRequest(endpoint, options = {}) {
const url = `${BASE_URL}${endpoint}`;
const controller = new AbortController();
const timeoutId = setTimeout(() => {
    controller.abort();
  }, options.timeout || 10000);

try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal,
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
    });

    // 清理超时定时器
    clearTimeout(timeoutId);

    // 尝试解析响应
    let data;
    const contentType = response.headers.get('content-type');
    
    if (contentType?.includes('application/json')) {
      data = await response.json();
    } else {
      data = await response.text();
    }

    // HTTP错误处理
    if (!response.ok) {
      thrownew APIError(
        data.message || `HTTP ${response.status}`,
        response.status,
        data
      );
    }

    // 204 No Content
    if (response.status === 204) {
      returnnull;
    }

    return data;
  } catch (error) {
    clearTimeout(timeoutId);

    // 区分错误类型
    if (error.name === 'AbortError') {
      thrownew APIError('请求超时,请检查网络并重试', 'TIMEOUT');
    }

    if (error instanceofTypeError) {
      thrownew APIError(
        '网络连接失败,请检查网络配置',
        'NETWORK_ERROR'
      );
    }

    // 如果已经是APIError,直接抛出
    if (error instanceof APIError) {
      throw error;
    }

    // 未知错误
    thrownew APIError(`请求失败: ${error.message}`, 'UNKNOWN');
  }
}

在组件中正确处理每种错误

代码语言:javascript
复制
// components/UserList.js

import { useState, useEffect } from'react';
import { api } from'../api/client';
import { APIError } from'../api/errors';

function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [retryCount, setRetryCount] = useState(0);

  useEffect(() => {
    let isMounted = true;

    const loadUsers = async () => {
      try {
        setLoading(true);
        setError(null);

        const data = await api.get('/users');

        if (isMounted) {
          setUsers(data);
        }
      } catch (err) {
        if (!isMounted) return;

        // ✅ 根据错误类型显示不同的信息和UI

        if (err instanceof APIError) {
          if (err.isNotFound) {
            // 404:资源不存在
            setError('没有找到用户列表,可能服务器配置有误');
          } elseif (err.isUnauthorized) {
            // 401:未授权,需要重新登录
            setError('你的登录已过期,请重新登录');
            // 实际应用中应该跳转到登录页
            window.location.href = '/login';
          } elseif (err.isForbidden) {
            // 403:禁止访问
            setError('你没有权限查看用户列表');
          } elseif (err.isValidationError) {
            // 400:请求格式错误
            setError(`请求参数有误: ${err.data.details || err.message}`);
          } elseif (err.isServerError) {
            // 5xx:服务器错误
            setError('服务器暂时出错,请稍后重试');
          } elseif (err.isNetworkError) {
            // 网络错误
            setError('网络连接失败,请检查你的网络');
          } else {
            // 其他API错误
            setError(err.message);
          }
        } else {
          // 未知错误
          setError('发生未知错误,请稍后重试');
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };

    loadUsers();

    return() => {
      isMounted = false;
    };
  }, [retryCount]);

if (loading) return<div>加载中...</div>;

if (error) {
    return (
      <div style={{ color: 'red' }}>
        <p>❌ {error}</p>
        <button onClick={() => setRetryCount(c => c + 1)}>
          重新加载
        </button>
      </div>
    );
  }

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

exportdefault UserList;

错误处理流程图:

代码语言:javascript
复制
请求开始
  ↓
发送fetch
  ├─ ❌ 网络错误(无网络/DNS失败)
  │   └─ → 显示"检查网络连接"
  │
  ├─ ✅ 获得响应
  │   ↓
  │   尝试解析JSON
  │   ├─ ❌ 解析失败(不是有效JSON)
  │   │   └─ → 显示"服务器响应格式错误"
  │   │
  │   └─ ✅ 解析成功
  │       ↓
  │       检查HTTP状态码
  │       ├─ ✅ 2xx 成功
  │       │   └─ → 返回数据
  │       │
  │       ├─ ❌ 400 请求格式错误
  │       │   └─ → 显示"你的请求有问题"
  │       │
  │       ├─ ❌ 401 未授权
  │       │   └─ → 显示"请重新登录"
  │       │
  │       ├─ ❌ 403 禁止访问
  │       │   └─ → 显示"你没有权限"
  │       │
  │       ├─ ❌ 404 资源不存在
  │       │   └─ → 显示"资源不存在"
  │       │
  │       ├─ ❌ 429 请求过频
  │       │   └─ → 显示"请求过于频繁,请稍候"
  │       │
  │       └─ ❌ 5xx 服务器错误
  │           └─ → 显示"服务器出错,请稍后重试"

第四部分:认证与授权——安全地处理用户身份

问题:localStorage的陷阱

很多教程会这样教token管理:

代码语言:javascript
复制
// ❌ 常见的"教科书"做法,但有安全问题
localStorage.setItem('authToken', token);

// 然后在API请求中:
const token = localStorage.getItem('authToken');
headers['Authorization'] = `Bearer ${token}`;

问题是什么?

  1. XSS攻击 — 如果网站被注入恶意JavaScript,就能读取localStorage
  2. CSRF攻击 — localStorage的token容易被跨域请求使用
  3. token泄露 — localStorage在DevTools中可见

更安全的做法:用httpOnly cookie(由服务器设置)。

但很多前端开发者控制不了后端,所以需要在前端做额外防护。

完整的认证系统

代码语言:javascript
复制
// api/auth.js

/**
 * 认证管理
 * 处理登录、登出、token刷新等
 */

let currentToken = null;
let refreshToken = null;
let isRefreshing = false;
let refreshSubscribers = [];

/**
 * 初始化认证(应用启动时调用)
 */
exportasyncfunction initAuth() {
// 从localStorage恢复token(仅在需要时)
// 注:这里有安全权衡 - 更安全的做法是不存储token
  currentToken = localStorage.getItem('authToken');

if (currentToken) {
    // 验证token是否还有效
    try {
      await api.get('/auth/verify');
      returntrue;
    } catch (err) {
      // Token无效,尝试刷新
      return refreshAccessToken();
    }
  }

returnfalse;
}

/**
 * 用户登录
 */
exportasyncfunction login(email, password) {
try {
    const response = await fetch(`${BASE_URL}/auth/login`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });

    if (!response.ok) {
      thrownewError('登录失败');
    }

    const data = await response.json();
    
    // 存储token
    currentToken = data.accessToken;
    refreshToken = data.refreshToken;
    
    // 仅存储必要的信息,避免存储完整token在localStorage中
    localStorage.setItem('authToken', currentToken);
    
    // 通知所有等待刷新的请求
    refreshSubscribers.forEach(callback => callback(currentToken));
    refreshSubscribers = [];
    
    return data;
  } catch (error) {
    thrownewError(`登录失败: ${error.message}`);
  }
}

/**
 * 用户登出
 */
exportfunction logout() {
  currentToken = null;
  refreshToken = null;
  localStorage.removeItem('authToken');
  localStorage.removeItem('refreshToken');

// 跳转到登录页
window.location.href = '/login';
}

/**
 * 获取当前token
 */
exportfunction getAuthToken() {
return currentToken;
}

/**
 * 刷新访问token
 * 当token过期时调用
 */
exportasyncfunction refreshAccessToken() {
// 防止多个请求同时刷新token
if (isRefreshing) {
    returnnewPromise(resolve => {
      refreshSubscribers.push(token => {
        resolve(token);
      });
    });
  }

  isRefreshing = true;

try {
    const response = await fetch(`${BASE_URL}/auth/refresh`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken }),
    });

    if (!response.ok) {
      // 刷新失败,需要重新登录
      logout();
      returnfalse;
    }

    const data = await response.json();
    currentToken = data.accessToken;
    
    // 更新localStorage
    localStorage.setItem('authToken', currentToken);
    
    // 通知所有等待的请求
    refreshSubscribers.forEach(callback => callback(currentToken));
    refreshSubscribers = [];
    
    isRefreshing = false;
    returntrue;
  } catch (error) {
    logout();
    returnfalse;
  }
}

// 订阅token刷新事件
exportfunction onTokenRefresh(callback) {
  refreshSubscribers.push(callback);
}

更新API客户端以支持认证

代码语言:javascript
复制
// api/client.js(增强版本)

import { getAuthToken, refreshAccessToken, logout } from'./auth';

exportasyncfunction apiRequest(endpoint, options = {}) {
const url = `${BASE_URL}${endpoint}`;
const token = getAuthToken();

const config = {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      // 如果有token,添加到请求头
      ...(token && { 'Authorization': `Bearer ${token}` }),
      ...options.headers,
    },
  };

try {
    let response = await fetch(url, config);

    // 处理401(token过期)
    if (response.status === 401) {
      // 尝试刷新token
      const refreshed = await refreshAccessToken();
      
      if (refreshed) {
        // 用新token重试请求
        const newToken = getAuthToken();
        config.headers['Authorization'] = `Bearer ${newToken}`;
        response = await fetch(url, config);
      } else {
        // 刷新失败,跳转到登录页
        logout();
        thrownew APIError('登录已过期,请重新登录', 401);
      }
    }

    // ... 其他错误处理保持不变
    
    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}));
      thrownew APIError(
        errorData.message || `HTTP ${response.status}`,
        response.status,
        errorData
      );
    }

    if (response.status === 204) {
      returnnull;
    }

    returnawait response.json();
  } catch (error) {
    // ... 错误处理
    throw error;
  }
}

登录表单示例

代码语言:javascript
复制
// components/LoginForm.js

import { useState } from'react';
import { useNavigate } from'react-router-dom';
import { login } from'../api/auth';

function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const navigate = useNavigate();

const handleSubmit = async (e) => {
    e.preventDefault();
    
    try {
      setLoading(true);
      setError(null);

      await login(email, password);
      
      // 登录成功,跳转到仪表板
      navigate('/dashboard');
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="邮箱"
        required
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="密码"
        required
      />
      {error && <div style={{ color: 'red' }}>{error}</div>}
      <button type="submit" disabled={loading}>
        {loading ? '登录中...' : '登录'}
      </button>
    </form>
  );
}

exportdefault LoginForm;

第五部分:受保护路由——控制访问权限

实现Protected Route组件

代码语言:javascript
复制
// components/ProtectedRoute.js

import { Navigate } from'react-router-dom';
import { getAuthToken } from'../api/auth';

/**
 * 受保护的路由
 * 仅当用户已认证时才显示组件
 * 
 * 使用方式:
 * <Route 
 *   path="/dashboard" 
 *   element={
 *     <ProtectedRoute>
 *       <Dashboard />
 *     </ProtectedRoute>
 *   } 
 * />
 */
function ProtectedRoute({ children, requiredRole = null }) {
const token = getAuthToken();

// 如果没有token,重定向到登录页
if (!token) {
    return<Navigate to="/login" replace />;
  }

// 如果需要特定角色,检查token中的角色信息
if (requiredRole) {
    // 注:实际应用中,你需要从token中解析用户信息
    const userRole = getUserRoleFromToken(token);
    
    if (userRole !== requiredRole) {
      return<Navigate to="/unauthorized" replace />;
    }
  }

// 用户已认证,显示组件
return children;
}

/**
 * 从JWT token中提取用户信息
 * JWT格式:header.payload.signature
 * payload是base64编码的JSON,包含用户信息
 */
function getUserRoleFromToken(token) {
try {
    const parts = token.split('.');
    if (parts.length !== 3) returnnull;

    const payload = JSON.parse(atob(parts[1]));
    return payload.role;
  } catch (error) {
    returnnull;
  }
}

exportdefault ProtectedRoute;

在路由配置中使用

代码语言:javascript
复制
// routes/index.js

import { BrowserRouter, Routes, Route, Navigate } from'react-router-dom';
import LoginForm from'../components/LoginForm';
import Dashboard from'../pages/Dashboard';
import UserList from'../components/UserList';
import ProtectedRoute from'../components/ProtectedRoute';

exportfunction AppRoutes() {
return (
    <BrowserRouter>
      <Routes>
        {/* 公开路由 */}
        <Route path="/login" element={<LoginForm />} />
        
        {/* 受保护路由 */}
        <Route
          path="/dashboard"
          element={
            <ProtectedRoute>
              <Dashboard />
            </ProtectedRoute>
          }
        />
        
        {/* 仅管理员可访问 */}
        <Route
          path="/users"
          element={
            <ProtectedRoute requiredRole="admin">
              <UserList />
            </ProtectedRoute>
          }
        />
        
        {/* 404处理 */}
        <Route path="*" element={<Navigate to="/dashboard" replace />} />
      </Routes>
    </BrowserRouter>
  );
}

应用启动时初始化认证

代码语言:javascript
复制
// index.js 或 App.js

import React, { useEffect, useState } from'react';
import { initAuth } from'./api/auth';
import { AppRoutes } from'./routes';

function App() {
const [isInitialized, setIsInitialized] = useState(false);

  useEffect(() => {
    const init = async () => {
      try {
        // 在应用启动时初始化认证
        await initAuth();
      } catch (error) {
        console.error('认证初始化失败:', error);
      } finally {
        setIsInitialized(true);
      }
    };

    init();
  }, []);

if (!isInitialized) {
    return<div>初始化中...</div>;
  }

return<AppRoutes />;
}

exportdefault App;

第六部分:完整的API架构总结

文件结构

代码语言:javascript
复制
project/
├── api/
│   ├── client.js          // 核心apiRequest和方便方法
│   ├── auth.js            // 认证和token管理
│   ├── errors.js          // 自定义错误类
│   └── endpoints.js       // 业务相关的API调用(可选)
│
├── components/
│   ├── ProtectedRoute.js  // 受保护路由
│   ├── LoginForm.js       // 登录表单
│   └── UserProfile.js     // 使用API的组件示例
│
├── pages/
│   ├── Dashboard.js
│   └── NotFound.js
│
└── routes/
    └── index.js           // 路由配置

可选:业务相关的API调用

为了进一步提高可维护性,可以为不同的资源创建专门的API调用函数:

代码语言:javascript
复制
// api/endpoints.js

import { api } from'./client';

/**
 * 用户相关的API调用
 */
exportconst userAPI = {
list: (filters) => api.get('/users', { params: filters }),
get: (id) => api.get(`/users/${id}`),
create: (data) => api.post('/users', data),
update: (id, data) => api.patch(`/users/${id}`, data),
delete: (id) => api.delete(`/users/${id}`),
};

/**
 * 文章相关的API调用
 */
exportconst postAPI = {
list: () => api.get('/posts'),
get: (id) => api.get(`/posts/${id}`),
create: (data) => api.post('/posts', data),
update: (id, data) => api.patch(`/posts/${id}`, data),
delete: (id) => api.delete(`/posts/${id}`),
};

/**
 * 评论相关的API调用
 */
exportconst commentAPI = {
list: (postId) => api.get(`/posts/${postId}/comments`),
create: (postId, data) => api.post(`/posts/${postId}/comments`, data),
delete: (id) => api.delete(`/comments/${id}`),
};

现在在组件中调用就更简洁了:

代码语言:javascript
复制
// components/UserList.js

import { userAPI } from'../api/endpoints';

function UserList() {
const [users, setUsers] = useState([]);

  useEffect(() => {
    userAPI.list()
      .then(setUsers)
      .catch(err =>console.error(err));
  }, []);

return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

第七部分:从"能用"到"生产级"的完整检查清单

你的API层是否真的生产就绪?

  • [ ] 统一入口 — 所有API请求都通过api.js?
  • [ ] 错误分类 — 是否能区分404、401、5xx等不同错误?
  • [ ] 错误处理 — 每种错误是否有对应的用户提示?
  • [ ] 认证管理 — 是否有token刷新机制?
  • [ ] 过期处理 — token过期时是否能自动刷新并重试?
  • [ ] 加载状态 — 用户能看到"加载中..."吗?
  • [ ] 内存泄漏 — 组件卸载时是否清理请求?
  • [ ] 超时控制 — 是否有请求超时设置?
  • [ ] 安全头部 — 是否正确设置了Content-Type、Authorization等?
  • [ ] 访问控制 — 受保护路由是否真的在保护敏感页面?
  • [ ] 环境配置 — 是否用process.env.REACT_APP_API_URL而不是硬编码URL?
  • [ ] 日志记录 — 是否有最小的日志记录用于调试?
  • [ ] 监控 — 是否知道哪些API调用失败最频繁?

如果你的答案是"不确定"或"没有"超过3个,说明还有优化空间。

总结:从47个fetch到1个API层

你学到了什么

  1. 为什么需要API层 — 维护成本降低50倍,代码更可靠
  2. 如何构建API层 — apiRequest + 便利方法 + 错误处理
  3. 完整的错误处理 — 不同错误不同处理
  4. 认证和授权 — token管理、刷新、过期处理
  5. 受保护路由 — 控制谁能看到什么页面

现实中的影响

有API层的团队:

  • ✅ 添加新功能快速(改一个地方)
  • ✅ 缺陷率低(集中管理意味着容易发现问题)
  • ✅ 新人快速上手(只需学一套API)
  • ✅ 问题更容易追踪(所有请求都有日志)

没有API层的团队:

  • ❌ 每次添加功能都要改多个地方
  • ❌ bug可能在某个组件里藏很久才被发现
  • ❌ 新人需要学习每个组件的"做法"
  • ❌ 性能问题难以追踪(哪个组件发了太多请求?)

【关注前端达人,建立你的技术壁垒】

如果这一篇帮你理解了如何建立一个生产级的API层,请关注微信公众号《前端达人》

最后的话

这一篇讲的"API层"看起来很基础,但大多数初创团队的代码质量问题都源于此

没有统一的API层 → 错误处理混乱 → bug分散在各个组件 → 新人来了不敢改 → 技术债爆炸。

反之,一个良好的API层就像一个坚实的地基,让你的应用能健康地长大,而不是在第二年突然崩塌。

点赞、分享、评论支持,我们下一篇再见!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-01-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 第一部分:为什么你需要一个"API层"而不是零散的fetch
    • 问题对比:没有 vs 有 API层
  • 第二部分:从零开始构建一个生产级的API层
    • 第一步:基础的apiRequest函数
    • 第二步:便利方法(GET/POST/PATCH等)
    • 第三步:在组件中使用
  • 第三部分:生产级错误处理——5种错误的正确应对
    • 问题:初级开发者的错误处理往往太粗糙
    • 完整的错误处理系统
    • 在组件中正确处理每种错误
  • 第四部分:认证与授权——安全地处理用户身份
    • 问题:localStorage的陷阱
    • 完整的认证系统
    • 更新API客户端以支持认证
    • 登录表单示例
  • 第五部分:受保护路由——控制访问权限
    • 实现Protected Route组件
    • 在路由配置中使用
    • 应用启动时初始化认证
  • 第六部分:完整的API架构总结
    • 文件结构
    • 可选:业务相关的API调用
  • 第七部分:从"能用"到"生产级"的完整检查清单
    • 你的API层是否真的生产就绪?
  • 总结:从47个fetch到1个API层
    • 你学到了什么
    • 现实中的影响
  • 【关注前端达人,建立你的技术壁垒】
    • 最后的话
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档