JavaScript进阶---一文认识网络请求

108 阅读17分钟

一、前言

网络请求是通过JavaScript代码发送HTTP请求到远程服务器以获取数据或与服务器进行交互的过程,这玩意儿对来说头痛,只好边学边写了...

二、XMLHttpRequest对象

说明: XMLHttpRequest对象简称XHR对象,它为发送服务器请求和获取响应提供了合理的接口,这个接口可以实现异步从服务器获取额外数据,意味着用户点击不用页面刷新也可以获取数据,所有浏览器都可以使用XMLHttpRequest构造函数创建XHR对象

let xhr = new XMLHttpRequest();

1.使用XHR

说明: 首先需要调用open("请求方式","请求地址","是否异步")方法准备一个请求,这里使用的地址可以是相对页面的地址,也可以是http开头的绝对地址,地址有一个要求就是:请求的URL与发送请求的页面必须是同源的,发送请求需要使用send(作为请求体发送的数据,不需要就写null),其次,终端请求可以使用abort()方法,终止请求后,应该释放xhr对象,

同源: 域名相同、端口相同、协议相同

// 在百度的界面下用控制台发送
let xhr = new XMLHttpRequest();

xhr.open("get", "https://www.baidu.com/", false);
xhr.send(null);
console.log(xhr)

image.png

_3M7.png

status:如果这个属性的值是200多,表示请求成功,此时responseText中是有内容的,如果是304,表示结果是从浏览器缓存中获取的,也意味着成功响应

readyState:值为数字,表示当前请求处于哪一个阶段,当这个属性值改变的时候,都会触发readystatechange事件,如果需要使用这个事件的事件处理程序赋值最好在open()方法之前,其次事件处理程序中不能使用event对象并且不能使用this关键字

  • 0:尚未调用 open()方法。
  • 1:已调用 open()方法,尚未调用 send()方法。
  • 2:已调用 send()方法,尚未收到响应。
  • 3:已经收到部分响应。
  • 4:已经收到所有响应,可以使用了。

2.HTTP头部

说明: http请求都会在请求的头部和响应的头部存储一些信息,便于开发者使用,使用xhr发送请求其请求头会默认发送下面带标注的信息,如果需要额外的请求头可以使用setRequestHeader("头部字段名称","值")来设置,为了确保请求头的发送,应该在open()之后,send()之前设置,如果需要获取指定的响应头部信息,可以使用getResponseHeader("指定的响应头部字段")获取,如果要获取所有的响应头,直接getAllResponseHeaders()即可

image.png

let xhr = new XMLHttpRequest();

xhr.open("get", "https://www.baidu.com/", false);
xhr.setRequestHeader("MyHeader", "MyValue"); 
xhr.send(null);

3IYK.png

// 下面所有的响应头以一个字符串全部返回,导致截图很长很小
let xhr = new XMLHttpRequest();

xhr.open("get", "https://www.baidu.com/", false);
xhr.setRequestHeader("MyHeader", "MyValue"); 
xhr.send(null);

let myHeader = xhr.getResponseHeader("MyHeader"); 
let allHeaders = xhr.getAllResponseHeaders(); 
console.log(myHeader,allHeaders)

2A@WK0VN2SURXF.png

3.GET请求

说明: 这是最常用的请求,用于向服务器查询某些信息,如果有需要,可以添加字符串参数进行查找,注意传参格式,请求地址与参数之间用?分隔,参数之间用&连接

xhr.open("get", "example.php?name1=value1&name2=value2", true);

4.POST请求

说明: 这也是一个常用的请求,用于向服务器发送应该保存的数据,其数据应该放在请求体内,对于服务器而言,服务器逻辑需要读取原始 POST 数据才能取得浏览器发送的数据,其实可以使用xhr模拟表单的提交(举例),其步骤如下,其次POST请求相比 GET请求要占用更多资源

  • content-Type头部设置为"application/x-www-formurlencoded",这是提交表单时使用的内容类型。
  • 创建对应格式的字符串。POST 数据此时使用与查询字符串相同的格式
// 来自 ID 为"user-info"的表单中的数据被序列化之后
// 发送给了服务器。PHP 文件 postexample.php 随后
// 可以通过$_POST 取得 POST 的数据,如果没有设置
// Content-Type,则 POST 数据需要通过$HTTP_RAW_POST_DATA
// 获取
function submitData() {
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
      if (xhr.readyState == 4) {
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
          alert(xhr.responseText);
        } else {
          alert("Request was unsuccessful: " + xhr.status);
        }
      }
    };
    xhr.open("post", "postexample.php", true);
    xhr.setRequestHeader(
      "Content-Type",
      "application/x-www-form-urlencoded"
    );
    let form = document.getElementById("user-info");
    xhr.send(serialize(form));
}

5.XMLHttpRequest 2.0

(1)FormData类型

说明: 这个类型便于表单的序列化,也便于创建与表单类似格式的数据然后通过xhr发送,创建一个这样的类型需要使用FormData构造函数,然后为这个类型添加数据需要使用append("表单字段名","字段值")方法,也可以直接给构造函数传入一个表单元素,这个类型的好处在于不需要给xhr对象显示设置请求头了,因为它xhr对象能识别这种类型并自动配置响应的头部

let xhr = new XMLHttpRequest();

xhr.onreadystatechange = function () {
    if (xhr.readyState == 4) {
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
        alert(xhr.responseText);
      } else {
        alert("Request was unsuccessful: " + xhr.status);
      }
    }
};

xhr.open("post", "postexample.php", true);
let form = document.getElementById("user-info");
xhr.send(new FormData(form));

(2)超时

说明: xhr对象存在一个timeout属性,表示请求发生后等待多少毫秒,如果响应不成功就中断请求,在事件过后还没收到任何响应,xhr就会除非timeout事件,也就会触发其事件处理程序,不过readyState的值会变成4,因此它也会调用onreadystatechange事件处理程序,注意,超时后去访问status属性会报错

let xhr = new XMLHttpRequest();

xhr.onreadystatechange = function () {
    if (xhr.readyState == 4) {
      try {
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
          alert(xhr.responseText);
        } else {
          alert("Request was unsuccessful: " + xhr.status);
        }
      } catch (ex) {
        // 假设由 ontimeout 处理
      }
    }
};

xhr.open("get", "timeout.php", true);
// 设置 1 秒超时
xhr.timeout = 1000; 
xhr.ontimeout = function () {
    alert("Request did not return in a second.");
};
xhr.send(null);

(3)overrideMimeType()

说明: 这个方法可以重写xhr响应的MIME类型,因为这个类型决定了xhr对象如何处理响应,如果需要使用,应该在send()之前使用

// 假设服务器实际发送了 XML 数据,但响应头设置的 
// MIME 类型是 text/plain。结果就会导致虽 然数据
// 是 XML,但 responseXML 属性值是 null。此时调用
// overrideMimeType()可以保证将响应 当成 XML 而不
// 是纯文本来处理:

let xhr = new XMLHttpRequest();
      
xhr.open("get", "text.php", true);
xhr.overrideMimeType("text/xml");
xhr.send(null);

三、进度事件

  • loadstart:在接收到响应的第一个字节时触发。
  • progress:在接收响应期间反复触发。
  • error:在请求出错时触发。
  • abort:在调用 abort()终止连接时触发。
  • load:在成功接收完响应时触发。
  • loadend:在通信完成时,且在 error、abort 或 load 之后触发。

1.load 事件

说明: 这个事件在接收完成后会立即触发,以此来替代readystatechange事件和readyState属性,load的事件处理程序会收到一个event对象,其target属性是xhr对象,由此可以访问xhr对象所有的属性和方法,不过只要从服务器收到响应,状态码是啥,load事件都会触发,因此还需要判断state属性的值才能确定数据是否有效

let xhr = new XMLHttpRequest();

xhr.onload = function () {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
      alert(xhr.responseText);
    } else {
      alert("Request was unsuccessful: " + xhr.status);
    }
};
xhr.open("get", "altevents.php", true);
xhr.send(null);

2.progress 事件

说明: 这个事件在浏览器接受数据会反复触发,是事件处理程序会收到event对象,target属性是xhr对象,并额外存在三个属性,为了确保事件处理程序执行,应该在open()之前使用

  • lengthComputable:是一个布尔值,表示进度信息是否可用;
  • position:是接收到的字节数;
  • totalSize:是响应的 ContentLength 头部定义的总字节数
let xhr = new XMLHttpRequest();

xhr.onload = function (event) {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
      alert(xhr.responseText);
    } else {
      alert("Request was unsuccessful: " + xhr.status);
    }
};

xhr.onprogress = function (event) {
    let divStatus = document.getElementById("status");
    if (event.lengthComputable) {
      divStatus.innerHTML =
        "Received " + event.position + " of " + event.totalSize + " bytes";
    }
};

xhr.open("get", "altevents.php", true);
xhr.send(null);

四、跨域资源共享

说明: 使用xhr对象进行ajax通信的一个限制在于跨域安全策略,默认情况下,只能访问和发起请求的页面在同一个域的资源,这个安全策略能够防止一些恶意行为,跨域的基本思路就是使用自定义的HTTP头部允许浏览器和服务器之间相互了解,以确实请求应该响应成功或者失败,假设有一个get或者是post请求,没有自定义头部,且请求体是text/plain类型,这样的请求在发送的时候存在一个Origin的头部,它包含请求页面的源,以便服务器使用,如果服务器需要响应这个请求,那么它的响应头应该包含Access-Control-Allow-Origin,值需要与Origin的值一样,如果没有这样操作,服务器是不会响应的,如果要向不同域的源发送请求,可以给open()传一个绝对的路径

跨域的xhr对象的限制:

  • 允许访问 status 和 statusText 属性
  • 不能使用 setRequestHeader()设置自定义头部。
  • 不能发送和接收 cookie。
  • getAllResponseHeaders()方法始终返回空字符串。

1.预检请求

说明: 这是一种服务器的验证机制,允许使用自定义头部、除get和post之外的方法以及不同请求体内容类型,请求会通过使用OPTIONS进行发送,在信息确认完毕后服务器会响应该请求,预检请求返回后,结果会按响应指定的时间缓存一段时间。换句话说,只有第一次发送这种类型的 请求时才会多发送一次额外的 HTTP 请求。

OPTIONS方法包含的头部信息:

  • Origin:与简单请求相同。
  • Access-Control-Request-Method:请求希望使用的方法。
  • Access-Control-Request-Headers:(可选)要使用的逗号分隔的自定义头部列表。

服务器与浏览器沟通的信息:

  • Access-Control-Allow-Origin:与简单请求相同。
  • Access-Control-Allow-Methods:允许的方法(逗号分隔的列表)。
  • Access-Control-Allow-Headers:服务器允许的头部(逗号分隔的列表)。
  • Access-Control-Max-Age:缓存预检请求的秒数。

2.凭据请求

说明: 默认情况下,跨域请求不提供凭据,如果需要可以将withCredentials属性设置为true表面请求会鞋携带凭据,如果服务器允许,可以设置以下头部,如果请求中存在凭据但是返回的响应没有这个头部,则会出现responseText 是空字符串,status 是 0,onerror()被调用,当然,这个头部也可以在预检请求中返回

Access-Control-Allow-Credentials: true

3.替代性跨源技术

(1)图片探测

说明: 任何页面都可以跨域加载图片而不受限制,图片探测是与服务器之间简单、跨域、 单向的通信。数据通过查询字符串发送,响应可以随意设置,不过一般是位图图片或值为 204 的状态码。 浏览器通过图片探测拿不到任何数据,但可以通过监听 onload 和 onerror 事件知道什么时候能接收到响应,一般设置好src属性请求就开始了,其作用在于跟踪用户在页面上的点击操作或动态显示广告,缺点在于是只能发 送 GET 请求和无法获取服务器响应的内容

(2)JSONP

说明: JSONP 调用是通过动态创建<script>元素并为 src 属性指定跨域 URL 实现的。此时的<script>与<img>元素类似,能够不受限制地从其他域加载资源,这个方法主要由回调和数据组成,回调是在页面接收到响应之后应该调用的函数,通常回调 函数的名称是通过请求来动态指定的。而数据就是作为参数传给回调函数的 JSON 数据,其优点是简单易用,缺点有两个,如下:

  • JSONP 是从不同的域拉取可执行代码。如果这个域并不可信,则可能在响应中加入恶意内容。 此时除了完全删除 JSONP 没有其他办法。在使用不受控的 Web 服务时,一定要保证是可以信任的。
  • 不好确定 JSONP 请求是否失败
<script src="http://freegeoip.net/json/?callback=handleResponse ">
    // 服务器响应后会触发src中定义的函数
    function handleResponse(value) {
        // value就是服务器返回的数据
    }
</script>

五、Fetch API

说明: 它能够执行xhr对象中的所有事情,也是一个请求资源的优秀工具,不过它发出的请求是异步

1.基本使用

说明: fetch(请求的url)暴露在全局作用域中,调用这个方法,它就会向指定的URL发送请求,如果只添加请求的url,那么fetch会发送get请求,它的请求头是低配版的,如果需要高配版的,需要第二个参数协助,这是一个配置对象(可选),对象的取值可以点击此处

(1)请求

说明: fetch()这个方法接收一个参数,多数情况是获取资源的url,可以是相对的url,也可以是绝对的url,它的返回值是一个promise,请求成功之后可以调用then方法获取返回的数据

fetch("https://www.baidu.com/").then((response) => {
    console.log(response)
})

image.png

(2)文本响应

说明: 如果需要响应的内容为纯文本格式的,需要使用text()方法,其返回值是一个promise

fetch("https://www.baidu.com/")
    .then((response) => response.text())
    .then((res) => console.log(res));

1LQ@E.png

(3)请求失败和状态码

说明: 请求所得到的响应对象中存在status(状态码)statusText(状态文本)这两个属性,用于检查响应的状态,只要服务器有响应的内容,内容会保存在response对象里面,如果响应失败其失败的内容会存在与reject对象中,并且只要状态码是200请求就被认定为成功,其它均为不成功,为了区分,可以使用response的ok属性,最后,请求的完整URL可以使用response的URL属性获取

常见状态:

  • 请求成功:status:200,statusText:ok
  • 资源不存在:status:404,statusText:Not Found
  • 服务器错误:status:500,statusText:Internal Server Error

2.常见请求模式

(1)发送JSON数据

// 创建简单的JSON字符串
let payload = JSON.stringify({
    foo: "bar",
});

// 创建请求头,表示发送的数据是JSON格式的
let jsonHeaders = new Headers({
    "Content-Type": "application/json",
});

fetch("/send-me-json", {
    // 发送请求体时必须使用一种 HTTP 方法
    method: "POST",
    body: payload,
    headers: jsonHeaders,
});

(2)请求体中发射参数

// 发送的 URL 编码参数
let payload = "foo=bar&baz=qux";

// 头部表示请求的内容将使用 URL 编码的形式发送
let paramHeaders = new Headers({
    "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
});

// 发送参数字符串
fetch("/send-me-params", {
    method: "POST", 
    body: payload,
    headers: paramHeaders
})

(3)文件发送

// 创建了一个新的 FormData 对象,用于构建表单数据
let imageFormData = new FormData();

// 类型为 file 的 input 元素,通常用于选择文件上传。
let imageInput = document.querySelector("input[type='file']");

// input 元素中选择的第一个文件添加到 imageFormData 
// 表单数据中,当然,这里可以选择for循环来发送多个文件
imageFormData.append("image", imageInput.files[0]);

// 发送FormData的表单数据
fetch("/img-upload", {
    method: "POST",
    body: imageFormData,
});

(4)加载blob文件

说明: 常见做法是将图片文件加载到内存中,然后将其添加到 HTML图片元素

const imageElement = document.querySelector("img");

fetch("my-image.png")
    .then((response) => response.blob())
    .then((blob) => {
        imageElement.src = URL.createObjectURL(blob);
    });

(5)发送跨源请求

说明: 从不同的源请求资源,响应要包含 CORS 头部才能保证浏览器收到响应。没有这些头部,跨源请求 会失败并抛出错误。如果代码不需要访问响应,也可以发送 no-cors 请求。此时响应的 type 属性值为 opaque,因此无法读取响应内容。这种方式适合发送探测请求或者将响应缓存起来供以后使用。

fetch("//cross-origin.com", { method: "no-cors" }).then((response) =>
    console.log(response.type)
);

(6)请求中断

说明: 调用 AbortController.abort()会中断所有网络传输,特别适合希望停止传输大型负载的情况。中断进行中的 fetch()请求会 导致包含错误的拒绝。

// 创建了一个新的 AbortController 对象,用于控制请求的中断
let abortController = new AbortController();

// AbortController 就能够控制这个请求
fetch("wikipedia.zip", { signal: abortController.signal }).catch(() =>
    console.log("aborted!")
);

// 10 毫秒后中断请求
setTimeout(() => abortController.abort(), 10);

3.Headers 对象

说明: 这个对象可以让你操控HTTP请求的响应头和请求头,同时在使用的时候跟Map结构很像

(1)API

// 基本实例方法
let h = new Headers();

// 设置键
h.set("foo", "bar");
// 这个在头部不存在这个值得时候与上面的方法作用是一致的
h.append('foo', 'bar');
// 检查键
console.log(h.has("foo")); // true
console.log(h.has("qux")); // false
// 获取值
console.log(h.get("foo")); // bar
// 更新值
h.set("foo", "baz");
// 取得更新的值
console.log(h.get("foo")); // baz
// 删除值
h.delete("foo");
// 确定值已经删除
console.log(h.get("foo")); // undefined
// 可以使用一个可迭代对象来初始化
let seed = [["foo", "bar"]];

let h = new Headers(seed);
console.log(h.get("foo")); // bar
// 可以使用键/值对的对象来初始化
let seed = { foo: "bar" };
      
let h = new Headers(seed);
console.log(h.get("foo")); // bar
// 存在keys()、values()和 entries()迭代器接口
let seed = [
    ["foo", "bar"],
    ["baz", "qux"],
];
let h = new Headers(seed);

console.log(...h.keys()); // foo, baz
console.log(...h.values()); // bar, qux
console.log(...h.entries()); // ['foo', 'bar'], ['baz', 'qux']

4.Request 对象

说明: 它表示获取资源请求的接口。这个接口暴露了请求的相关信息,也暴露了使用请求体的不同方式。

(1)创建

说明: 创建需要通过Request构造函数初始化,它第一个参数是一个url,第二个参数跟fetch的第二个参数是一样的,没有会使用默认值

// 使用默认值创建
new Request('')

9_C8C3F.png

// 使用指定的初始值创建
new Request("www.baidu.com", { method: "POST" });

XKW[%JNJM@0BK1V))]DJML9.png

(2)克隆

说明: 有两种方式创建 Request 对象的副本:使用Request构造函数和使用 clone()方法,第一种是直接将Request实例传给Request构造函数,这样会得到一个副本,第二种调用方法就会得到一个全新的副本

Request构造函数注意:

  • 这种克隆方式并不总能得到一模一样的副本
  • 如果源对象与创建的新对象不同源,则 referrer 属性会被清除
  • 如果源对象的 mode 为 navigate,则会被转换为 same-origin
// 基本使用
let r1 = new Request("https://foo.com");
let r2 = new Request(r1);

console.log(r2.url);

XH.png

// 再传入 init 对象,则 init 对象的值会覆盖源对象中同名的值
let r1 = new Request("https://foo.com");
let r2 = new Request(r1, { method: "POST" });

console.log(r1.method); 
console.log(r2.method);

2R.png

// 但并不总能得到一模一样的副本
let r1 = new Request("https://foo.com", {
    method: "POST",
    body: "foobar",
});
let r2 = new Request(r1);

console.log(r1.bodyUsed);
console.log(r2.bodyUsed);

R66FR36Y1.png

// 使用clone方法克隆副本
let r1 = new Request("https://foo.com", {
    method: "POST",
    body: "foobar",
});
let r2 = r1.clone();

console.log(r1.url);
console.log(r2.url);
console.log(r1.bodyUsed);
console.log(r2.bodyUsed);

8W99Y.png

如果请求对象的bodyUsed属性为true,那么这个对象不能被复制,否则会报错

(3)在 fetch 中使用

说明: 在调用fetch()的时候,可以传入已经创建好的Request实例,同样与 Request构造函数一样,传给fetch()的配置对象会覆盖传入请求对象的值,也不能拿请求体已经用过的Request对象来发送请求,注意,有请求体的Request只能在一次 fetch 中使用,如果要想基于包含请求体的相同 Request 对象多次调用 fetch(),必须在第一次发送 fetch()请求前 调用 clone()

// 举个例子,需要可以按这种格式写
let r = new Request("https://www.baidu.com/", {
    method: "POST",
    body: "foobar",
});

fetch(r.clone());
fetch(r.clone());
fetch(r);

5.Response 对象

说明: 它表示响应资源请求的接口。这个接口暴露了请求的相关信息,也暴露了使用响应体的不同方式。

(1)创建

说明: 通过Response构造函数进行实例化,可以不传参数,这个实例所有属性均为默认值,因为它不代表实际的响应,也可以接收一个可选的 body 参数。这个 body 可以是 null,等同于 fetch()参数 init 中的 body。还可以接收一个可选的 init 对象,这个对象可以包含下表所列的键和值,同时这个对象存在两个静态方法redirect("URL","301、302、303、307、308")error(),前者返回重定向的 Response 对象,后者用于产生表示网络错误的 Response 对象

  • headers: 必须是 Headers 对象实例或包含字符串键/值对的常规对象实例
  • status: 表示 HTTP 响应状态码的整数
  • statusText:表示 HTTP 响应状态的字符串
// 使用默认值初始化
let r = new Response();

console.log(r);

2V.png

// 使用指定值进行初始化
let r = new Response("foobar", {
    status: 418,
    statusText: "I'm a teapot",
});

console.log(r);

T88.png

// 重定向的response
Response.redirect('https://foo.com', 301)

E.png

// 被拒绝的response
Response.error()

J0XW.png

(2)响应信息

image.png

(3)克隆

说明: 这个大致跟Request的克隆是一样的,不过这里主要使用clone()来复制,注意,通过创建带有原始响应体的 Response 实例,可以执行伪克隆操作。关键是这样不会把第一个Response实例标记为已读,而是会在两个响应之间共享

let r1 = new Response("foobar");
let r2 = new Response(r1.body);

console.log(r1.bodyUsed);
console.log(r2.bodyUsed);
r2.text().then(console.log);
r1.text().then(console.log);

U_CY.png