> For the complete documentation index, see [llms.txt](https://lizh.gitbook.io/knowledge/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lizh.gitbook.io/knowledge/javascript-xi-lie/01-ji-chu-pian-05ajax.md).

# 基础篇 05 AJAX

AJAX 是异步 JavaScript 和 XML（Asynchronous JavaScript And XML），指的是通过 JavaScript 的异步通信，从服务器获取 XML 文档并从中提取数据，再更新当前网页的对应部分（不用刷新整个网页）。AJAX 本身不是一种新技术，而是一个在 2005 年被 Jesse James Garrett 提出的新术语，用来描述一种使用现有技术集合的新方法，包括：HTML 或 XHTML、CSS、JavaScript、DOM、XML、XSLT 以及最重要的 XMLHttpRequest。

当使用结合了这些技术的 AJAX 模型以后， 网页应用能够快速地将增量更新呈现在用户界面上，而不需要重载（刷新）整个页面。这使得程序能够更快地回应用户的操作。

尽管 X 在 AJAX 中代表 XML，但由于 JSON 格式的许多优势，比如更加轻量以及作为 JavaScript 的一部分，目前 JSON 的使用比 XML 更加普遍。JSON 和 XML 都被用于在 AJAX 模型中打包信息。

> 1999 年，微软公司发布 IE 浏览器 5.0 版，第一次引入新功能：允许 JavaScript 脚本向服务器发起 HTTP 请求。这个功能当时并没有引起注意，直到 2004 年 Gmail 发布和 2005 年 Google Map 发布，才引起广泛重视。2005 年 2 月，AJAX 这个词第一次正式提出。
>
> 后来，AJAX 这个词就成为 JavaScript 脚本发起 HTTP 通信的代名词，也就是说，只要用脚本发起通信，就可以叫做 AJAX 通信。W3C 也在 2006 年发布了它的国际标准。

具体来说，AJAX 包括以下几个步骤：

* 创建 XMLHttpRequest 实例
* 发出 HTTP 请求
* 接收服务器传回的数据
* 更新网页数据

概括起来，就是一句话，AJAX 通过原生的 XMLHttpRequest 对象发出 HTTP 请求，得到服务器返回的数据后，再进行处理。现在，服务器返回的都是 JSON 格式的数据，XML 格式已经过时了，但是 AJAX 这个名字已经成了一个通用名词，字面含义已经消失了。

常用的 AJAX 库有：**XMLHttpRequest、Node HTTP、fetch、node-fetch、axios。。。**

## XMLHTTP

浏览器与服务器之间，采用 HTTP 协议通信。用户在浏览器地址栏键入一个网址，或者通过网页表单向服务器提交内容，这时浏览器就会向服务器发出 HTTP 请求。

XMLHTTP 是一组 API 函数集，可被 JavaScript、JScript、VBScript 以及其它 Web 浏览器内嵌的脚本语言调用，通过 HTTP 在浏览器和 web 服务器之间收发 XML或其它数据。

XMLHTTP 最大的好处在于可以动态地更新网页，它无需重新从服务器读取整个网页，也不需要安装额外的插件。XMLHTTP 是 AJAX 网页开发技术的重要组成部分。除 XML 之外，XMLHTTP 还能用于获取其它格式的数据，如 JSON 或者甚至纯文本。

## XMLHttpRequest

XMLHttpRequest（XHR）对象用于客户端与服务器之间的通信。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL，获取数据。这允许网页在不影响用户操作的情况下，更新页面的局部内容。XMLHttpRequest 在 AJAX 编程中被大量使用。

XMLHttpRequest 对象是 AJAX 的主要接口。尽管名称有 XML 和 Http，实际上，XMLHttpRequest 可以用于获取任何类型的数据（包括字符串和二进制），而不仅仅是 XML。它甚至支持 HTTP 以外的协议（包括 file:// 和 FTP），但可能受到客户端出于安全等原因的限制。

XMLHttpRequest 本身是一个构造函数，可以使用 new 命令生成实例。它没有任何参数。

下面是 XMLHttpRequest 对象简单用法的完整例子：

```js
function sendXMLHttpRequest() {
    const xhr = new XMLHttpRequest()
    xhr.withCredentials = true
    xhr.onreadystatechange = function () {
        // 通信成功时，状态值为 4
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                console.log(xhr.responseText)
            } else {
                console.error(xhr.statusText)
            }
        }
    }
    xhr.onerror = function (e) {
        console.error(xhr.statusText)
    }
    xhr.open('GET', 'http://localhost:3000', true)
    xhr.send(null)
}
```

## XMLHttpRequest的实例属性

### 1.readyState

返回一个整数，表示实例对象的当前状态。该属性只读。它可能返回以下值：

* 0：表示 XMLHttpRequest 实例已经生成，但是实例的 open() 方法还没有被调用。
* 1：表示 open() 方法已经调用，但是实例的 send() 方法还没有调用，仍然可以使用实例的 setRequestHeader() 方法，设定 HTTP 请求的头信息。
* 2：表示实例的 send() 方法已经调用，并且服务器返回的头信息和状态码已经收到。
* 3：表示正在接收服务器传来的数据体（body 部分）。这时，如果实例的 responseType 属性等于 text 或者空字符串，responseText 属性就会包含已经收到的部分信息。
* 4：表示服务器返回的数据已经完全接收，或者本次接收已经失败。

通信过程中，每当实例对象发生状态变化，它的 readyState 属性的值就会改变。这个值每一次变化，都会触发 readyStateChange 事件。

```js
xhr.onreadystatechange = function(){
    if (xhr.readyState === 4){
    }
}
```

### 2.onreadystatechange

指向一个监听函数。readystatechange 事件发生时（实例的 readyState 属性变化），就会执行这个属性。另外，如果使用实例的 abort() 方法，终止 XMLHttpRequest 请求，也会造成 readyState 属性变化。

### 3.response

一个只读属性，表示服务器返回的数据体（即 HTTP 回应的 body 部分）。它可能是任何数据类型，比如字符串、对象、二进制对象等等，具体的类型由 responseType 属性决定。

如果本次请求没有成功或者数据不完整，该属性等于 null。但是，如果 responseType 属性等于 text 或空字符串，在请求没有结束之前（readyState 等于 3 的阶段），response 属性包含服务器已经返回的部分数据。

### 4.responseType

该属性是一个字符串，表示服务器返回数据的类型。这个属性是可写的，可以在调用 open() 方法之后、调用 send() 方法之前，设置这个属性的值，告诉服务器返回指定类型的数据。

可以取以下值：

* ”“（空字符串）：等同于 text，表示服务器返回文本数据。
* “arraybuffer”：ArrayBuffer 对象，表示服务器返回二进制数组。
* “blob”：Blob 对象，表示服务器返回二进制对象。
* “document”：Document 对象，表示服务器返回一个文档对象。
* “json”：JSON 对象。
* “text”：字符串。

上面几种类型之中，text 类型适合大多数情况，而且直接处理文本也比较方便；Document 类型适合返回 HTML / XML 文档的情况，这意味着，对于那些打开 CORS 的网站，可以直接用 AJAX 抓取网页，然后不用解析 HTML 字符串，直接对抓取回来的数据进行 DOM 操作；blob 类型适合读取二进制数据，比如图片文件。

```js
function loadImage() {
    const xhr = new XMLHttpRequest()
    xhr.withCredentials = true
    xhr.open('GET', 'assets/img/20190419141710_4735vxaqwhri_small.jpg', true)
    xhr.responseType = 'blob'
    xhr.onload = function (e) {
        const img = document.createElement('img')
        img.style.width = '100%'
        img.src = window.URL.createObjectURL(this.response)
        document.body.appendChild(img)
    }
    xhr.send()
}
```

[查看DEMO](https://1927344728.github.io/demo-lizh/html/19-ajax.html)

### 5.responseText

返回从服务器接收到的字符串，该属性为只读。只有 HTTP 请求完成接收以后，该属性才会包含完整的数据。

### 6.responseXML

返回从服务器接收到的 HTML 或 XML 文档对象，该属性为只读。如果本次请求没有成功，或者收到的数据不能被解析为 XML 或 HTML，该属性等于 null。

该属性生效的前提是 HTTP 回应的 Content-Type 头信息等于 `text/xml` 或 `application/xml`。这要求在发送请求前，responseType 属性要设为 document。如果 HTTP 回应的 Content-Type 头信息不等于 `text/xml` 和 `application/xml`，但是想从 responseXML 拿到数据（即把数据按照 DOM 格式解析），那么需要手动调用 overrideMimeType() 方法，强制进行 XML 解析。

该属性得到的数据，是直接解析后的文档 DOM 树。

```js
const xhr = new XMLHttpRequest()
xhr.open('GET', '/server', true)
xhr.responseType = 'document'
xhr.overrideMimeType('text/xml')
xhr.onload = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseXML)
    }
}
xhr.send(null)
```

### 7.responseURL

该属性是字符串，表示发送数据的服务器的网址。

**注意：** 这个属性的值与 open() 方法指定的请求网址不一定相同。如果服务器端发生跳转，这个属性返回最后实际返回数据的网址。另外，如果原始 URL 包括锚点，该属性会把锚点剥离。

### 8.status

一个只读属性，返回一个整数，表示服务器回应的 HTTP 状态码。一般来说，如果通信成功的话，这个状态码是200；如果服务器没有返回状态码，那么这个属性默认是200。请求发出之前，该属性为 0 。

* 200：OK，访问正常
* 301：Moved Permanently，永久移动
* 302：Move temporarily，暂时移动
* 304：Not Modified，未修改
* 307：Temporary Redirect，暂时重定向
* 401：Unauthorized，未授权
* 403：Forbidden，禁止访问
* 404：Not Found，未发现指定网址
* 500：Internal Server Error，服务器发生错误

### 9.statusText

一个只读属性，返回一个字符串，表示服务器发送的状态提示。不同于`status`属性，该属性包含整个状态信息，比如 `OK` 和 `Not Found` 。在请求发送之前（即调用 open() 方法之前），该属性的值是空字符串；如果服务器没有返回状态提示，该属性的值默认为 `OK` 。

### 10.timeout

返回一个整数，表示多少毫秒后，如果请求仍然没有得到结果，就会自动终止。如果该属性等于0，就表示没有时间限制。

### 11.ontimeout

用于设置一个监听函数，如果发生 timeout 事件，就会执行这个监听函数。

### 12.事件监听属性

XMLHttpRequest 对象可以对以下事件指定监听函数。

* onloadstart：loadstart 事件（HTTP 请求发出）的监听函数
* onprogress：progress事件（正在发送和加载数据）的监听函数
* onabort：abort 事件（请求中止，比如用户调用了abort()方法）的监听函数
* onerror：error 事件（请求失败）的监听函数
* onload：load 事件（请求成功完成）的监听函数
* ontimeout：timeout 事件（用户指定的时限超过了，请求还未完成）的监听函数
* onloadend：loadend 事件（请求完成，不管成功或失败）的监听函数

progress 事件的监听函数有一个事件对象参数，该对象有三个属性：

* loaded：返回已经传输的数据量
* total：属性返回总的数据量
* lengthComputable：属性返回一个布尔值，表示加载的进度是否可以计算。

所有这些监听函数里面，只有 progress 事件的监听函数有参数，其他函数都没有参数。

\*\*注意：\*\*如果发生网络错误（比如服务器无法连通），onerror 事件无法获取报错信息。

### 13.withCredentials

该属性是一个布尔值，表示跨域请求时，用户信息（比如 Cookie 和认证的 HTTP 头信息）是否会包含在请求之中，默认为 false ，即向 `example.com` 发出跨域请求时，不会发送 `example.com` 设置在本机上的 Cookie（如果有的话）。

如果需要跨域 AJAX 请求发送 Cookie，需要 withCredentials 属性设为 true。注意，同源的请求不需要设置这个属性。

为了让这个属性生效，服务器必须显式返回 `Access-Control-Allow-Credentials` 这个头信息。

```javascript
Access-Control-Allow-Credentials: true
```

withCredentials 属性打开的话，跨域请求不仅会发送 Cookie，还会设置远程主机指定的 Cookie。反之，如果 withCredentials 属性没有打开，那么跨域的 AJAX 请求即使明确要求浏览器设置 Cookie，浏览器也会忽略。

**注意：** 脚本总是遵守同源政策，无法从 `document.cookie` 或者 HTTP 回应的头信息之中，读取跨域的 Cookie， withCredentials 属性不影响这一点。

### 14.upload

XMLHttpRequest 不仅可以发送请求，还可以发送文件，这就是 AJAX 文件上传。发送文件以后，通过 upload 属性可以得到一个对象，通过观察这个对象，可以得知上传的进展。主要方法就是监听这个对象的各种事件：loadstart、loadend、load、abort、error、progress、timeout。

当请求一切正常时，相关的事件触发顺序如下：

```js
// 之后每次readyState变化时，都会触发一次
xhr.onreadystatechange

xhr.onloadstart

//上传开始
xhr.upload.onloadstart
xhr.upload.onprogress
xhr.upload.onload
xhr.upload.onloadend

//上传结束
xhr.onprogress
xhr.onload
xhr.onloadend
```

## XMLHttpRequest的实例方法

### 1.open()

用于指定 HTTP 请求的参数，或者说初始化 XMLHttpRequest 实例对象。它一共可以接受五个参数：

```js
xhr.open(method, url, async, user, password)
```

* method：表示 HTTP 动词方法，比如 GET、POST、PUT、DELETE、HEAD 等。
* url：表示请求发送目标 URL。
* async：布尔值，表示请求是否为异步，默认为 true。如果设为 false，则 send() 方法只有等到收到服务器返回了结果，才会进行下一步操作。该参数可选。由于同步 AJAX 请求会造成浏览器失去响应，许多浏览器已经禁止在主线程使用，只允许 Worker 里面使用。所以，这个参数不应该轻易设为 false。
* user：表示用于认证的用户名，默认为空字符串。该参数可选。
* password：表示用于认证的密码，默认为空字符串。该参数可选。

**注意：** 如果对使用过 open() 方法的 AJAX 请求，再次使用这个方法，等同于调用 abort()，即终止请求。

### 2.send()

用于实际发出 HTTP 请求。它的参数是可选的，如果不带参数，就表示 HTTP 请求只包含头信息，也就是只有一个 URL，典型例子就是 GET 请求；如果带有参数，就表示除了头信息，还带有包含具体数据的信息体，典型例子就是 POST 请求。

其参数技多种格式：ArrayBufferView、Blob、Document、String、FormData。

**注意：** 所有 XMLHttpRequest 的监听事件，都必须在 send() 方法调用之前设定。

### 3.setRequestHeader()

用于设置浏览器发送的 HTTP 请求的头信息。该方法必须在 open() 之后、send() 之前调用。如果该方法多次调用，设定同一个字段，**则每一次调用的值会被合并成一个单一的值发送**。

```js
xhr.setRequestHeader('Content-Type', 'application/json')
```

### 4.overrideMimeType()

用来指定 MIME 类型，覆盖服务器返回的真正的 MIME 类型，从而让浏览器进行不一样的处理。举例来说，服务器返回的数据类型是 `text/xml`，由于种种原因浏览器解析不成功报错，这时就拿不到数据了。为了拿到原始数据，我们可以把 MIME 类型改成 `text/plain`，这样浏览器就不会去自动解析，从而我们就可以拿到原始文本了。

```js
xhr.overrideMimeType('text/plain')
```

**注意：** 该方法必须在 send() 方法之前调用。

### 5.getResponseHeader()

返回 HTTP 头信息指定字段的值，如果还没有收到服务器回应或者指定字段不存在，返回 null。该方法的参数不区分大小写。

如果有多个字段同名，它们的值会被连接为一个字符串，每个字段之间使用 “逗号+空格” 分隔。

### 6.getAllResponseHeaders()

返回一个字符串，表示服务器发来的所有 HTTP 头信息。格式为字符串，每个头信息之间使用 `CRLF` 分隔（回车+换行），如果没有收到服务器回应，该属性为 null。如果发生网络错误，该属性为空字符串。

### 7.abort()

用来终止已经发出的 HTTP 请求。调用这个方法以后，readyState 属性变为 4，status 属性变为 0。

## XMLHttpRequest 实例的事件

更多现代浏览器，包括 Firefox，除了可以设置 on\* 属性外，也提供标准的监听器 addEventListener() API 来监听 XMLHttpRequest 事件。

```js
xhr.addEventListener('readystatechange', () => {})
```

### 1.readyStateChange 事件

readyState 属性的值发生改变，就会触发 readyStateChange 事件。

可以通过 onReadyStateChange 属性，指定这个事件的监听函数，对不同状态进行不同处理。尤其是当状态变为 4 的时候，表示通信成功，这时回调函数就可以处理服务器传送回来的数据。

### 2.progress 事件

上传文件时，XMLHTTPRequest 实例对象本身和实例的 upload 属性，都有一个 progress 事件，会不断返回上传的进度。

### 3.load 事件、error 事件、abort 事件、loadend 事件

* load 事件：表示服务器传来的数据接收完毕
* error 事件：表示请求出错
* abort 事件：表示请求被中断（比如用户取消请求）
* loadend 事件：abort、load、error 这三个事件，会伴随一个 loadend 事件，表示请求结束，但不知道其是否成功。

### 4.timeout 事件

服务器超过指定时间还没有返回结果，就会触发 timeout 事件。

## 常见问题

### AJAX 和 XMLHttpRequest 的正确理解

AJAX 是 asynchronous javascript and XML 的简写，中文翻译是异步的 javascript 和 XML，这一技术能够向服务器请求额外的数据而无须卸载页面，会带来更好的用户体验。**它是一种技术方案，但并不是一种新技术。**

XMLHttpRequest（XHR）是可用于 Web 浏览器脚本语言（例如 JavaScript）的 API。它用于将 HTTP 或 HTTPS 请求发送到 Web 服务器，并将服务器响应数据加载回脚本中。

**就是一句话，ajax 通过原生的 XMLHttpRequest 对象发出HTTP请求，得到服务器返回的数据后，再进行处理。**

### XMLHttpRequest、axios、fetch 有什么区别？

XMLHttpRequest、axios、fetch 都是常用的 AJAX 库。

#### XMLHttpRequest

XMLHttpRequest 是最早的用于浏览器与服务器交换数据的方式。它可以使用 JSON、XML、HTML 和 text 文本等格式发送和接收数据，并且允许网页在不影响用户操作的情况下，更新页面的局部内容。

#### Axios

Axios 是一个基于 Promise 网络请求库，作用于 Node.js 和浏览器中。 它是 isomorphic 的（即同一套代码可以运行在浏览器和 Node.js 中）。在服务端它使用原生 Node.js http 模块，而在客户端则使用 XMLHttpRequest。

就客户端而言，它是基于 XHR 进行二次封装形成的工具库，主要特性有：

* 从浏览器创建 XMLHttpRequests
* 支持 Promise API
* 拦截、转换请求和响应
* 取消请求
* 自动转换 JSON 数据
* 客户端支持防御 XSRF

#### Fetch

Fetch 提供了一种合理的方式来跨网络异步获取资源（包括跨域请求）。

Fetch 是一个现代的概念，提供了许多与 XMLHttpRequest 相同的功能，但被设计成更具可扩展性和高效性。Fetch 提供了一个更理想的替代方案，可以很容易地被其他技术使用，例如 Service Workers。

fetch 主要特性：

* fetch 是基于 Promise 的：无论请求成功与否，它都返回一个 Promise 对象，resolve 对应请求的 Response。该 promise 不会拒绝 Http 的错误状态，即使响应是一个 Http 404 或者 500。
* fetch 没有设置超时时间。
* fetch 没有办法原生监测请求的进度。
* fetch 默认不带 cookie：除非使用了 credentials 的初始化选项。credentials 可选值：include（始终发送用户凭据，包括跨域）、same-origin（同源，则发送用户凭据）、omit（永远不要发送或接收 cookie）。用户凭据是指 cookie、基本 http 身份验证等。

**一句话概括：** XMLHttpRequest 是最早用于客户端与服务器交互数据的方式，是 W3C 标准；Fetch 可以认为是 XMLHttpRequest 的升级版，也是 W3C 标准，但兼容性稍差；Axios 是对 Node.js http 模块（用于 Node.js）和 XMLHttpRequest（用于浏览器）的封装。

### 响应头取Date异常

获取请求响应中的Date对象: `res.request.getResponseHeader("Date")`，在浏览器中报错 `Refused to get unsafe header "Date"`。

**原因**：默认的请求上， 浏览器只能访问以下默认的响应头

* Cache-Control
* Content-Language
* Content-Type
* Expires
* Last-Modified
* Pragma

**解决**：想让浏览器能访问到其他的 响应头的话 需要在服务器上设置 `Access-Control-Expose-Headers`。比如想获取时间 `Access-Control-Expose-Headers : 'Date'`。

### 常用的几种 Content-Type ？

* **application/x-www-form-urlencoded：** 浏览器的原生 form 表单，如果不设置 enctype 属性，那么最终就会以 `application/x-www-form-urlencoded` 方式提交数据。该种方式提交的数据放在 body 里面，数据按照 `key1=val1&key2=val2` 的方式进行编码，key 和 val 都进行了 URL 转码。
* **multipart/form-data：** 该种方式也是一个常见的 POST 提交方式，通常表单上传文件时使用该种方式。
* **application/json：** 告诉服务器消息主体是序列化后的 JSON 字符串。
* **text/xml：** 该种方式主要用来提交 XML 格式的数据。

### 客户端（JavaScript）下载服务端返回的文件流

```javascript
import axios from 'axios'
axios.post("/api/download/excel/stream", {}, {
  responseType: 'blob'
}).then((response) => {
  const a = document.createElement('a')
  const url = URL.createObjectURL(response)
  a.href = url
  a.download = 'abcd.xlsx'
  a.click()
  URL.revokeObjectURL(url)
})
```

**特别注意：** 请求参数一定要带上 `responseType: 'blob'`。这是请求参数，不是请求头部的参数（即，不是在 Header 中）。

## 参考链接

[MDN XMLHttpRequest](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest)

[阮一峰 JavaScript 标准参考教程（alpha）- AJAX](https://javascript.ruanyifeng.com/bom/ajax.html)
