封装ajax

引言

本文旨在探讨如何将传统的 Ajax 请求封装为 Promise 形式。我们将从 XMLHttpRequest 基础知识入手,逐步实现一个功能完善的 Promise 化 Ajax 请求库。

本文会介绍 XMLHttpRequest,但它实际上是过时 Web 规范的产物,应该只在旧版本浏览器中使用。

1. XMLHttpRequest 基础

所有现代浏览器都通过 XMLHttpRequest 构造函数原生支持 XHR 对象:

let xhr = new XMLHttpRequest();

1.1 XMLHttpRequest 概述

1.1.1 使用 XHR

使用 XHR 对象首先要调用 open 方法,open 方法接受三个参数:请求的类型、请求的 URL、是否异步发送请求,然后必须要调用 send()方法发生定义好的请求,send 方法接收一个参数,是作为请求体,如果不需要请求体,则传 null,这个参数再某些浏览器是必须的,下面是一个例子:

js 复制代码
let xhr = new XMLHttpRequest();
xhr.open("GET", "http://example.com", true);
xhr.send(null);

因为这个请求时同步的,所以 javascript 代码会等待服务器响应后再继续执行,收到响应后,XHR 的对象的以下属性会被填充上数据:

  • responseText:作为响应体返回的文本
  • responseXML:如果响应的内容类型是 text/xml 或 application/xml,则保存响应的 XML 文档
  • status:响应的 HTTP 状态码
  • statusText:HTTP 状态的说明

收到响应后,首先要检查 status 属性以确保响应成功返回。一般来说 HTTP 状态码为 2xx 或 304 时,响应才算成功。 XHR 对象上有一个 readyState 属性,该属性表示当前 XMLHttpRequest 的状态。这个属性有如下可能的值:

  • 0: 请求未初始化
  • 1: 服务器连接已建立
  • 2: 请求已接收
  • 3: 请求处理中
  • 4: 响应已完成

readyState 的值会改变,当请求完成时,会触发 onreadystatechange 事件。一般来说,我们唯一要关心的状态是 readyState 为 4 时。来看下面的例子:

javascript 复制代码
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4) {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
      console.log(xhr.responseText);
    } else {
      console.log("Error: " + xhr.status);
    }
  }
};

收到响应之前如果想取消异步请求,可以调用 abort()方法。

javascript 复制代码
xhr.abort();

1.1.2 HTTP 头部

每个 HTTP 请求和响应都会携带一些头部字段,这些字段可能对开发者有用。XHR 对象会通过一些方法暴露与请求和响应相关的头部字段。

设置请求头部 :使用 setRequestHeader() 方法可以设置请求头部信息。这个方法接收两个参数:头部字段的名称和值。必须在 open() 方法之后、send() 方法之前调用 setRequestHeader()

javascript 复制代码
let xhr = new XMLHttpRequest();
xhr.open("GET", "http://example.com", true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.setRequestHeader("X-Custom-Header", "CustomValue");
xhr.send(null);

获取响应头部 :可以使用 getResponseHeader() 方法和 getAllResponseHeaders() 方法获取响应头部信息。

javascript 复制代码
// 获取特定的响应头部
let contentType = xhr.getResponseHeader("Content-Type");

// 获取所有的响应头部
let allHeaders = xhr.getAllResponseHeaders();

1.1.3 GET 请求

GET 请求是最常见的 HTTP 请求类型,通常用于从服务器获取数据。在 GET 请求中,查询参数会附加到 URL 的末尾。

javascript 复制代码
let xhr = new XMLHttpRequest();

// 构建带查询参数的URL
let url = "http://example.com/api/data?name=John&age=30";

// 或者使用URLSearchParams构建查询字符串
let params = new URLSearchParams();
params.append("name", "John");
params.append("age", 30);
let url = `http://example.com/api/data?${params.toString()}`;

xhr.open("GET", url, true);
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText);
  }
};
xhr.send(null);

注意事项

  • GET 请求不应该用于发送敏感数据,因为查询参数会显示在 URL 中
  • 浏览器对 URL 长度有限制,所以 GET 请求的数据量应该较小
  • GET 请求可以被缓存、收藏和保存在浏览器历史记录中

1.1.4 POST 请求

POST 请求通常用于向服务器发送数据,特别是当数据量较大或包含敏感信息时。在 POST 请求中,数据会包含在请求体中,而不是 URL 中。

javascript 复制代码
let xhr = new XMLHttpRequest();
xhr.open("POST", "http://example.com/api/submit", true);

// 设置Content-Type头部
xhr.setRequestHeader("Content-Type", "application/json");

// 准备要发送的数据
let data = JSON.stringify({
  name: "xl",
  age: 30,
});

xhr.onreadystatechange = function () {
  if (xhr.readyState === 4) {
    if (xhr.status === 200 || xhr.status === 201) {
      console.log("Success:", xhr.responseText);
    } else {
      console.log("Error:", xhr.status);
    }
  }
};

// 发送数据
xhr.send(data);

常见的 Content-Type 类型

  • application/x-www-form-urlencoded:表单提交的默认编码方式
  • application/json:JSON 格式的数据
  • multipart/form-data:用于上传文件
  • text/plain:纯文本

POST 请求相比 GET 请求要占用更多资源。从性能方面说,发送相同数量的数据, GET 请求比 POST 请求要快两倍。

1.1.5 XMLHttpRequest Level 2

XMLHttpRequest Level 2 是对原始 XHR 对象的扩展,添加了许多新功能,使得 Ajax 请求更加强大和灵活。

1. FormData 类型

FormData 对象用于模拟表单数据,可以更方便地发送表单数据或文件上传。

javascript 复制代码
// 创建空的FormData对象
let formData = new FormData();

// 添加字段
formData.append("username", "John");
formData.append("email", "john@example.com");

// 添加文件
let fileInput = document.getElementById("fileInput");
formData.append("file", fileInput.files[0]);

// 发送FormData
let xhr = new XMLHttpRequest();
xhr.open("POST", "http://example.com/upload", true);
// 使用FormData时不需要设置Content-Type,浏览器会自动设置
xhr.send(formData);
2. 超时

XHR Level 2 添加了超时功能,可以设置请求的超时时间,避免请求长时间挂起。

javascript 复制代码
let xhr = new XMLHttpRequest();
xhr.open("GET", "http://example.com/api/data", true);

// 设置超时时间(毫秒)
xhr.timeout = 5000; // 5秒超时

// 超时处理函数
xhr.ontimeout = function () {
  console.log("请求超时");
};

xhr.send(null);
3. overrideMimeType()方法

overrideMimeType() 方法用于重写服务器返回的 MIME 类型,这在服务器返回的 MIME 类型不正确时非常有用。

javascript 复制代码
let xhr = new XMLHttpRequest();
xhr.open("GET", "http://example.com/data.json", true);

// 强制将响应解析为JSON
xhr.overrideMimeType("application/json");

xhr.onreadystatechange = function () {
  if (xhr.readyState === 4 && xhr.status === 200) {
    let jsonResponse = JSON.parse(xhr.responseText);
    console.log(jsonResponse);
  }
};

xhr.send(null);
4. 进度事件

XHR Level 2 添加了进度事件,可以监控请求的进度。

javascript 复制代码
let xhr = new XMLHttpRequest();
xhr.open("GET", "http://example.com/large-file", true);

// 上传进度
xhr.upload.onprogress = function (event) {
  if (event.lengthComputable) {
    let percentComplete = (event.loaded / event.total) * 100;
    console.log(`上传进度: ${percentComplete.toFixed(2)}%`);
  }
};

// 下载进度
xhr.onprogress = function (event) {
  if (event.lengthComputable) {
    let percentComplete = (event.loaded / event.total) * 100;
    console.log(`下载进度: ${percentComplete.toFixed(2)}%`);
  }
};

xhr.send(null);

1.2 传统 Ajax 的局限性

尽管 XMLHttpRequest 为 Web 应用程序带来了革命性的变化,但它也存在一些局限性:

1.2.1 回调地狱

传统的 Ajax 使用回调函数处理异步操作,当需要执行多个连续的异步操作时,会导致回调嵌套,形成所谓的"回调地狱"(Callback Hell)。

javascript 复制代码
// 回调地狱示例
xhr1.onreadystatechange = function () {
  if (xhr1.readyState === 4 && xhr1.status === 200) {
    let data1 = JSON.parse(xhr1.responseText);

    xhr2.onreadystatechange = function () {
      if (xhr2.readyState === 4 && xhr2.status === 200) {
        let data2 = JSON.parse(xhr2.responseText);

        xhr3.onreadystatechange = function () {
          if (xhr3.readyState === 4 && xhr3.status === 200) {
            let data3 = JSON.parse(xhr3.responseText);
            // 处理最终结果
          }
        };
        xhr3.open("GET", `http://example.com/api/data3?id=${data2.id}`, true);
        xhr3.send(null);
      }
    };
    xhr2.open("GET", `http://example.com/api/data2?id=${data1.id}`, true);
    xhr2.send(null);
  }
};
xhr1.open("GET", "http://example.com/api/data1", true);
xhr1.send(null);

1.2.2 错误处理复杂

在传统 Ajax 中,错误处理分散在多个回调函数中,使得统一处理错误变得困难。

1.2.3 缺乏标准的取消机制

虽然可以使用 abort() 方法取消请求,但缺乏一种标准的、优雅的方式来管理和取消多个请求。

1.2.4 不支持请求/响应拦截

传统 Ajax 不提供内置的请求和响应拦截机制,这使得统一处理请求头、认证令牌、响应转换等变得困难。

1.2.5 代码冗余

每次发起 Ajax 请求都需要编写类似的样板代码,导致代码冗余和维护困难。

这些局限性促使开发者寻求更好的解决方案,如 Fetch API 和 axios 等库。

2. Promise 封装实现

2.1 设计思路

2.1.1 核心目标

  • Promise 化:将基于回调的 XMLHttpRequest 转换为基于 Promise 的 API
  • 易用性:提供简洁的接口,减少样板代码
  • 可配置性:支持常见的请求配置选项
  • 错误处理:统一处理各类网络错误和 HTTP 错误

2.1.2 功能需求

  • 返回 Promise 实例
  • 创建 XMLHttpRequest 对象
  • 处理请求的各种状态
  • 支持 GET、POST 等 HTTP 方法

2.1.3 实现思路

  1. 创建一个函数,接收 URL、HTTP 方法、数据和配置选项作为参数
  2. 在函数内部创建并返回一个 Promise 实例
  3. 在 Promise 的执行器函数中处理 XMLHttpRequest 的生命周期
  4. 根据请求结果调用 resolve 或 reject
  5. 添加超时处理和错误处理机制

2.2 核心实现

2.2.1 完整实现代码

javascript 复制代码
/**
 * 将传统的Ajax请求封装为Promise形式
 *
 * @param {string} url - 请求URL
 * @param {string} method - 请求方法,默认为GET
 * @param {Object|null} data - 请求数据,对象会被自动序列化为JSON
 * @param {Object} options - 配置选项
 * @param {number} [options.timeout=10000] - 请求超时时间(毫秒)
 * @param {string} [options.responseType=''] - 响应类型
 * @returns {Promise<any>} 返回Promise,成功时解析为响应数据(自动反序列化),失败时拒绝并提供错误信息
 */
function ajaxPromise(url, method = "GET", data = null, options = {}) {
  // 存储xhr实例,用于手动中断请求
  let xhr;

  // 创建主请求Promise
  const fetchPromise = new Promise((resolve, reject) => {
    const defaultOptions = {
      timeout: 10000,
      responseType: "",
    };
    const mergedOptions = { ...defaultOptions, ...options };

    xhr = new XMLHttpRequest();
    xhr.timeout = mergedOptions.timeout;
    xhr.open(method, url, true);
    if (mergedOptions.responseType) {
      xhr.responseType = mergedOptions.responseType;
    }

    // 处理请求数据
    let processedData = data;
    if (
      data &&
      typeof data === "object" &&
      !(data instanceof FormData) &&
      !(data instanceof Blob) &&
      !(data instanceof ArrayBuffer)
    ) {
      processedData = JSON.stringify(data);
    }

    // 处理加载完成事件
    xhr.onload = function () {
      if (xhr.readyState === 4) {
        // 确保请求完成
        if (xhr.status >= 200 && xhr.status < 300) {
          let response = xhr.response;
          // 如果响应类型未设置,尝试解析JSON
          if (xhr.responseType === "" || xhr.responseType === "text") {
            if (response) {
              try {
                response = JSON.parse(response);
              } catch (e) {
                console.warn("JSON解析失败,返回原始响应:", e);
              }
            }
          }

          resolve(response);
        } else {
          reject({
            status: xhr.status,
            statusText: xhr.statusText,
            response: xhr.response,
          });
        }
      }
    };

    // 处理网络错误
    xhr.onerror = function () {
      reject({
        status: 0,
        statusText: "网络错误",
        error: new Error("网络请求失败"),
      });
    };

    // 处理超时
    xhr.ontimeout = function () {
      reject({
        status: 0,
        statusText: "请求超时",
        error: new Error("请求超时"),
      });
    };

    // 发送请求
    xhr.send(processedData);
  });

  // 创建超时Promise
  const timeoutPromise = new Promise((_, reject) => {
    const { timeout, timeoutMessage } = { ...options };
    const timer = setTimeout(() => {
      reject({
        status: 0,
        statusText: "请求超时",
        error: new Error("请求超时"),
        isManualTimeout: true,
      });
    }, timeout || 10000);
  });

  // 使用Promise.race竞争,谁先完成就返回谁的结果
  return Promise.race([fetchPromise, timeoutPromise]).catch((error) => {
    console.log(error);
    // 如果是手动超时,尝试中断XHR请求
    if (error && error.isManualTimeout && xhr) {
      try {
        xhr.abort();
      } catch (e) {
        console.warn("中断请求失败:", e);
      }
    }

    // 继续抛出错误
    return Promise.reject(error);
  });
}

2.2.2 使用示例

js 复制代码
// GET请求
async function testGet() {
  try {
    // 设置1毫秒的超时时间,确保会触发超时
    const result = await ajaxPromise(
      "https://example.com/todos/1",
      "GET",
      null,
      { timeout: 10000 }
    );
    console.log("get-result", result);
  } catch (error) {
    console.log("get-result", error);
  }
}

// POST请求
async function testPost() {
  try {
    const data = {
      title: "测试标题",
      body: "测试内容",
      userId: 1,
    };
    const result = await ajaxPromise("https://example.com/posts", "POST", data);
    console.log("post-result", result);
  } catch (error) {
    console.log("post-result", error);
  }
}

总结

随着前端技术的发展,Fetch API 和 axios 等工具已经成为主流的网络请求方案,但理解 XMLHttpRequest 的 Promise 封装过程,有助于我们更深入地理解 JavaScript 的异步编程模型。

相关推荐
paopaokaka_luck1 小时前
基于SpringBoot+Uniapp的健身饮食小程序(协同过滤算法、地图组件)
前端·javascript·vue.js·spring boot·后端·小程序·uni-app
患得患失9492 小时前
【前端】【vscode】【.vscode/settings.json】为单个项目配置自动格式化和开发环境
前端·vscode·json
飛_2 小时前
解决VSCode无法加载Json架构问题
java·服务器·前端
YGY Webgis糕手之路5 小时前
OpenLayers 综合案例-轨迹回放
前端·经验分享·笔记·vue·web
90后的晨仔5 小时前
🚨XSS 攻击全解:什么是跨站脚本攻击?前端如何防御?
前端·vue.js
Ares-Wang5 小时前
JavaScript》》JS》 Var、Let、Const 大总结
开发语言·前端·javascript
90后的晨仔5 小时前
Vue 模板语法完全指南:从插值表达式到动态指令,彻底搞懂 Vue 模板语言
前端·vue.js
德育处主任5 小时前
p5.js 正方形square的基础用法
前端·数据可视化·canvas
烛阴5 小时前
Mix - Bilinear Interpolation
前端·webgl
90后的晨仔5 小时前
Vue 3 应用实例详解:从 createApp 到 mount,你真正掌握了吗?
前端·vue.js