JavaScript脚本中Fetch API与async/await异步请求实现

作者:拾光时间:2026-10-30 19:03:27浏览:104

深入讲解JavaScript中使用Fetch API结合async/await进行网络请求的完整实现方案,涵盖GET/POST请求、响应解析、状态码判断、错误捕获与请求超时取消等关键环节。

现代前端与Node.js开发中,异步数据交互是脚本的核心能力。Fetch API作为浏览器原生提供的现代网络接口,替代了早期的XMLHttpRequest;而async/await语法则是基于Promise的语法糖,使开发者能够用接近同步代码的结构书写异步逻辑。结合使用Fetch API与async/await,可以构建出结构清晰、错误边界明确且易于维护的网络请求模块。

一、Fetch API与async/await的协同机制

Fetch API调用后会立即发起网络I/O并返回一个处于pending状态的Promise对象。该Promise在服务器返回响应头后resolve为一个Response对象,开发者通过调用其内置的数据解析方法(如response.json())获取实际响应内容。

若使用传统Promise链式调用(.then().catch()),在处理多步依赖请求或统一异常捕获时容易形成代码嵌套。使用async/await修饰函数后,await关键字会暂停当前异步函数的执行,等待底层Promise敲定(settled),并将resolve的结果直接解包返回给左侧变量。这种机制消除了回调嵌套,让请求发起、状态校验、数据反序列化与错误处理按线性的顺序清晰呈现。

JavaScript脚本中Fetch API与async/await异步请求实现
图 1:Fetch API 结合 async/await 的完整调用链路与时序阶段

二、基础 GET 与 POST 请求的标准实现

执行网络请求时,最核心的两个操作是获取资源(GET)和提交表单/数据(POST)。以下提供生产环境可直接参考的完整实现范例。

1. GET 请求与查询参数构建

GET请求通常不包含请求体(Body),参数需通过URL查询字符串传递。为了规避中文或特殊字符转义错误,推荐使用URLSearchParams或URL构造器构建完整请求地址。

async function fetchUserData(userId) {
  const baseUrl = 'https://api.example.com/v1/users';
  const params = new URLSearchParams({ id: userId, format: 'detail' });
  const targetUrl = `${baseUrl}?${params.toString()}`;

  try {
    const response = await fetch(targetUrl, {
      method: 'GET',
      headers: {
        'Accept': 'application/json'
      }
    });

    if (!response.ok) {
      throw new Error(`HTTP 异常状态码: ${response.status}`);
    }

    const userData = await response.json();
    return userData;
  } catch (error) {
    console.error('获取用户数据失败:', error.message);
    throw error;
  }
}

2. POST 请求与 Payload 提交

提交JSON数据时,必须显式在headers中声明Content-Type: application/json,并通过JSON.stringify()将JavaScript对象序列化为JSON字符串传入body。

async function createNewPost(postPayload) {
  const targetUrl = 'https://api.example.com/v1/posts';

  try {
    const response = await fetch(targetUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      },
      body: JSON.stringify(postPayload)
    });

    if (!response.ok) {
      const errorDetail = await response.text();
      throw new Error(`创建失败 [${response.status}]: ${errorDetail}`);
    }

    const createdRecord = await response.json();
    return createdRecord;
  } catch (error) {
    console.error('提交文章记录失败:', error.message);
    throw error;
  }
}
JavaScript脚本中Fetch API与async/await异步请求实现
图 2:Promise 链式回调与 async/await 线性控制结构对照

三、严谨的错误处理机制:区分网络错误与 HTTP 状态

核心注意事项:Fetch API 仅在遇到真正的“网络层故障”(如断网、DNS解析失败、跨域CORS被拦截、连接被强行关闭)时,底层 Promise 才会进入 reject 状态。如果服务端返回 404 Not Found、403 Forbidden 或 500 Internal Server Error,在 Fetch 规范中依然视为一次成功的通信交换,Promise 会正常 resolve。

因此,直接依赖 try...catch 无法自动捕获 4xx/5xx 的 HTTP 错误响应。开发者必须手动读取 response.ok 属性(当 HTTP 状态码位于 200 至 299 之间时为 true,其余均为 false)。

JavaScript脚本中Fetch API与async/await异步请求实现
图 3:Fetch API 中底层通信异常与 HTTP 业务状态码的排查分流模型

四、进阶控制:请求超时与主动取消(AbortController)

原生的 fetch() 配置项中并不支持直接传入类似于 timeout: 5000 的配置。如果服务端挂起或遭遇黑洞路由,请求可能会长时间悬挂,耗尽浏览器的并发通道。

解决该问题的标准机制是使用原生的 AbortController 对象。通过将 controller 的 signal 属性绑定到 fetch 选项中,在超时到达或用户切换页面时调用 controller.abort(),即可主动掐断请求。

async function fetchWithTimeout(resource, options = {}) {
  const { timeout = 8000, ...fetchOptions } = options;
  
  // 现代环境推荐: AbortSignal.timeout(timeout)
  // 通用兼容方案: 显式实例化 AbortController
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(resource, {
      ...fetchOptions,
      signal: controller.signal
    });

    if (!response.ok) {
      throw new Error(`HTTP 错误: ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error(`请求超时,超过限定时长 ${timeout}ms`);
    }
    throw error;
  } finally {
    clearTimeout(id); // 请求完成后清除定时器,避免内存泄露
  }
}

五、生产级通用请求客户端封装

在大型应用中,零散编写 fetch 会导致基础 URL、通用 Headers(例如 Authorization 凭据)、统一日志监控与异常拦截代码冗余。一个具备完备防御性的封装应当统一处理这些横切关注点。

class HttpClient {
  constructor(baseURL = '', defaultHeaders = {}) {
    this.baseURL = baseURL;
    this.defaultHeaders = defaultHeaders;
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const headers = {
      'Content-Type': 'application/json',
      ...this.defaultHeaders,
      ...options.headers
    };

    const config = {
      ...options,
      headers
    };

    // 自动序列化普通对象的 body
    if (config.body && typeof config.body === 'object' && !(config.body instanceof FormData)) {
      config.body = JSON.stringify(config.body);
    }

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

      if (!response.ok) {
        // 尝试解析服务端返回的结构化错误信息
        let errorPayload;
        try {
          errorPayload = await response.json();
        } catch {
          errorPayload = await response.text();
        }

        const httpError = new Error(`请求失败: ${response.status} ${response.statusText}`);
        httpError.status = response.status;
        httpError.response = errorPayload;
        throw httpError;
      }

      // 如果响应状态码是 204 No Content,则不解析 JSON
      if (response.status === 204) {
        return null;
      }

      return await response.json();
    } catch (err) {
      // 统一监控或上报
      console.warn(`[API] 路径: ${endpoint} 错误: ${err.message}`);
      throw err;
    }
  }

  get(endpoint, options = {}) {
    return this.request(endpoint, { ...options, method: 'GET' });
  }

  post(endpoint, body, options = {}) {
    return this.request(endpoint, { ...options, method: 'POST', body });
  }
}
JavaScript脚本中Fetch API与async/await异步请求实现
图 4:Fetch API 与 async/await 生产级请求实现要素架构总结

六、常见避坑要点

陷阱场景 典型错误表现 正确防范措施
Response Body 重复读取 报错 TypeError: Failed to execute 'json' on 'Response': body stream already read Response 的 body 是一个单向读取流(ReadableStream),res.json() 或 res.text() 只能消费一次。若需多次读取,须提前调用 res.clone() 复制实例。
忽略 Content-Type 请求头 服务端无法正确解析 POST 请求体,接收到空对象或报 400 格式错误 在 headers 中显式声明 'Content-Type': 'application/json',并确保 body 经过 JSON.stringify() 转化。
误判 404/500 为进入 catch 页面未能捕获接口不存在或服务器报错,逻辑继续执行导致空指针崩溃 在获取 response 后必须第一时间检查 if (!response.ok),根据需要主动 throw new Error()。
未清除超时定时器 请求虽在时限内成功完成,但后台定时器仍在运行,甚至提前中断后续复用操作 在 finally 块中严格执行 clearTimeout(timerId),保证资源被及时销毁。

通过遵循上述结构与工程实践,JavaScript中的异步网络请求可以做到逻辑精简、职责专一,并在高并发或弱网环境下保持高度稳定。

作者最新文章
相关文章
更多