😁深入JS(七): 了解 Xhr 与 用Promise封装一个XMLHttpRequest

1、简单了解 Xhr

XMLHttpRequest 是一个内建的浏览器对象,它允许使用 JavaScript 发送 HTTP 请求。

现如今,我们有一个更为现代的方法叫做 fetch,它的出现使得 XMLHttpRequest 在某种程度上被弃用。

目前只有一种情况下将使用 Xhr

做一些 fetch 目前无法做到的事情,如跟踪上传进度

1.1、发送请求

要发送请求,需要 3 个步骤:

  1. 创建 XMLHttpRequest

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

    此构造器没有参数。

  2. 初始化它,通常就在 new XMLHttpRequest 之后:

    js 复制代码
    xhr.open(method, URL)

    此方法指定请求的主要参数:

    • method ------ HTTP 方法。通常是 "GET""POST"
    • URL ------ 要请求的 URL 请注意,open 调用与其名称相反,不会建立连接。它仅配置请求,而网络活动仅以 send 调用开启。
  3. 发送请求。

    js 复制代码
    xhr.send([body])

    这个方法会建立连接,并将请求发送到服务器。可选参数 body 包含了 request body。 一些请求方法,像 GET 没有 request body。还有一些请求方法,像 POST 使用 body 将数据发送到服务器。我们稍后会看到相应示例。

1.2、XMLHttpRequest 的状态

readyState 的值及其对应的状态:

状态 描述
0 UNSENT 代理被创建,但尚未调用 open 方法
1 OPENED open 方法已经被调用
2 HEADERS_RECEIVED send 方法已经被调用,并且头部和状态已经可获得
3 LOADING 下载中
4 DONE 下载操作已完成

一般而言,我们只关心 xhr 完成请求体下载的状态(即 readyState 为 4)

1.3、获取请求结果

js 复制代码
const xhr = new XMLHttpRequest();

// 初始化一个新创建的请求
xhr.open("GET", "https://xxx/car");
// 设置响应处理
xhr.onreadystatechange = function () {
// 请求完成
if (xhr.readyState === 4) {
  // HTTP状态码为200,请求成功
  if (xhr.status === 200) {
    // 获取数据
    doSomething(xhr.response);
  } else {
    console.error("GET 请求失败:", xhr.status, xhr.statusText);
  }
}
};
// 发送请求
xhr.send();

1.4、请求头设置

setRequestHeader(name, value)

使用给定的 namevalue 设置 request header。

js 复制代码
xhr.setRequestHeader('Content-Type', 'application/json');

1.5、上传进度

实战案例

js 复制代码
<input type="file" onchange="upload(this.files[0])">

<script>
function upload(file) {
  let xhr = new XMLHttpRequest();

  // 跟踪上传进度
  xhr.upload.onprogress = function(event) {
    console.log(`Uploaded ${event.loaded} of ${event.total}`);
  };

  xhr.open("POST", "/article/xmlhttprequest/post/upload");
  xhr.send(file);
}
</script>

1.6、跨域设置

默认情况下不会将 HTTP 授权发送到其他域。要启用它们,可以将 xhr.withCredentials 设置为 true
注意 :服务器端必须在响应头中设置 Access-Control-Allow-Credentials: true,且 Access-Control-Allow-Origin 不能为通配符 *,必须指定明确的源(如 https://example.com)。

js 复制代码
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;

xhr.open('POST', 'http://anywhere.com/request');
.....

1.7、超时机制

js 复制代码
var xhr = new XMLHttpRequest();
xhr.open("GET", "/server");

// 超时时间,单位是毫秒
xhr.timeout = 2000;

xhr.ontimeout = function (e) {
  // XMLHttpRequest 超时。在此做某事。
};
xhr.send();

2、Promise封装Xhr

封装目标

  1. 使用 Promise 封装 XMLHttpRequest

  2. 处理 HTTP 状态码

    1. 在 [200, 300) 区间,以及 304(缓存命中) 时,视为请求成功,Promise 兑现
    2. 其余状态码视为请求失败,Promise 拒绝
  3. 处理请求体数据,使用 data 配置项。

  4. 支持设置请求头

2.1、简单使用

  • 通过 XMLHttpRequest创建实例
  • 调用实例 open 方法,传入请求方法URL
  • 监听实例状态改变
  • 发送请求 send
js 复制代码
const xhr = new XMLHttpRequest();

// 初始化一个新创建的请求
xhr.open("GET", "https://xxx/car");
// 设置响应处理
xhr.onreadystatechange = function () {
// 请求完成
if (xhr.readyState === 4) {
  // HTTP状态码为200,请求成功
  if (xhr.status === 200) {
    // 获取数据
    doSomething(xhr.response);
  } else {
    console.error("GET 请求失败:", xhr.status, xhr.statusText);
  }
}
};
// 发送请求
xhr.send();

2.2、Promise 化

js 复制代码
function ajax() {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", "https://xxx/car");
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
          // 异步操作完成时,将promise状态改为fulfilled,兑现值为响应体数据
          resolve(xhr.response);
        } else {
          // 拒绝原因为HTTP状态码,具体可根据业务调整
          reject(xhr.status);
        }
      }
    };

    xhr.send();
  });
}

2.3、设置 Http 方法,地址,数据类型

  • 设置请求方法
  • 设置请求地址(URL)
  • 设置数据格式
js 复制代码
function ajax({
  method = "GET",
  url,
  // 默认情况下,responseType是JSON格式数据
  responseType = "json",
}) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url);
    xhr.responseType = responseType;
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
          // 经过正确处理的响应体数据
          resolve(xhr.response);
        } else {
          reject(xhr.status);
        }
      }
    };
    xhr.send();
  });
}
ajax({
  url: "https://xxx/car",
})
  .then((res) => {
    console.log(res); 
  })
  .catch((err) => {
    // do something
  });

2.4、处理请求头

上文知晓 setRequestHeader(name, value)

js 复制代码
function ajax({
  method = "GET",
  url,
  // 默认情况下,responseType是JSON格式数据
  responseType = "json",
  headers = {}
}) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();

    xhr.open(method, url);

    xhr.responseType = responseType;
    // 设置请求头
    Object.entries(headers).forEach(([key, value]) => {
      xhr.setRequestHeader(key, value);
    });
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
          // 经过正确处理的响应体数据
          resolve(xhr.response);
        } else {
          reject(xhr.status);
        }
      }
    };

    xhr.send();
  });
}

ajax({
  url: "https://xxx/car",
   headers: {
    header1: "header1",
  },
})
  .then((res) => {
    console.log(res); 
  })
  .catch((err) => {
  });

2.5、处理请求体

以 POST 请求举例,根据请求体数据的类型不同,一般可以分为以下四种媒体类型(MIME type)进行传输:

  • application/x-www-form-urlencoded:数据以键值对形式发送,键和值都进行了 URL 编码(name=Jack&age=18)
  • multipart/form-data:用于文件上传或需要同时发送文件和数据时。
  • text/xml:用于发送 XML 数据。
  • application/json:用于发送 JSON 格式的数据。

请求体和用户设置的 content-type 约定如下:

  1. 用户设置的 content-type 拥有最高优先级

  2. 用户不设置 content-type 时:

    1. 如 data 是特殊类型:

      1. data 是 FormData:直接添加到请求体中,因content-type 涉及 boundary,不额外处理,由浏览器自动设置。
      2. data 是 URLSearchParams:调用 toString 方法,转化为 url 编码字符串,设置 content-type:application/x-www-form-urlencoded
    2. data 是普通对象:转为 JSON 类型,设置 content-type: application/json

    3. data 为 text 或其他: 直接添加到请求体中,content-type:text/xml

js 复制代码
function ajax({
  method = "GET",
  url,
  // 默认情况下,responseType是JSON格式数据
  responseType = "json",
  headers = {}
}) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();

    xhr.open(method, url);

    xhr.responseType = responseType;
    // 设置请求头
    Object.entries(headers).forEach(([key, value]) => {
      xhr.setRequestHeader(key, value);
    });
        // 处理请求体
    let requestData = null;
    if (data) {
      if (data instanceof FormData) {
        // data是FormData类型
        requestData = data;
      } else if (data instanceof URLSearchParams) {
        // data是URLSearchParams类型

        requestData = data.toString();
        // 如用户设置了content-type请求头,则不作修改
        if (!headers["content-type"]) {
          xhr.setRequestHeader(
            "content-type",
            "application/x-www-form-urlencoded"
          );
        }
      } else if (typeof data === "object") {
        // data为普通对象

        // 只转化为JSON或url编码字符串
        if (
          ["application/x-www-form-urlencoded", "application/json"].includes(
            headers["content-type"]
          )
        ) {
          if (headers["content-type"] === "application/x-www-form-urlencoded") {
            const params = new URLSearchParams(data);
            requestData = params.toString();
          } else {
            requestData = JSON.stringify(data);
          }
        } else {
          // 默认情况下转为JSON格式
          requestData = JSON.stringify(data);
          if (!headers["content-type"]) {
            xhr.setRequestHeader("content-type", "application/json");
          }
        }
      } else {
        requestData = data;
        if (!headers["content-type"]) {
          xhr.setRequestHeader("content-type", "text/xml");
        }
      }
    }

    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
          // 经过正确处理的响应体数据
          resolve(xhr.response);
        } else {
          reject(xhr.status);
        }
      }
    };

    xhr.send(requestData);
  });
}

ajax({
  url: "https://xxx/car",
   headers: {
    header1: "header1",
  },
   data: {
    name: "小明",
  },
})
  .then((res) => {
    console.log(res); 
  })
  .catch((err) => {
  });

参考:juejin.cn/post/752493...

相关推荐
快乐点吧2 分钟前
【前端】异步任务风控验证与轮询机制技术方案(通用笔记版)
前端·笔记
pe7er30 分钟前
nuxtjs+git submodule的微前端有没有搞头
前端·设计模式·前端框架
七月的冰红茶38 分钟前
【threejs】第一人称视角之八叉树碰撞检测
前端·threejs
爱掉发的小李1 小时前
前端开发中的输出问题
开发语言·前端·javascript
祝余呀1 小时前
HTML初学者第四天
前端·html
浮桥3 小时前
vue3实现pdf文件预览 - vue-pdf-embed
前端·vue.js·pdf
七夜zippoe3 小时前
前端开发中的难题及解决方案
前端·问题
Hockor4 小时前
用 Kimi K2 写前端是一种什么体验?还支持 Claude Code 接入?
前端
杨进军4 小时前
React 实现 useMemo
前端·react.js·前端框架
海底火旺4 小时前
浏览器渲染全过程解析
前端·javascript·浏览器