简介
浏览器与服务器之间采用 HTTP 协议通信,用户在浏览器地址栏键入一个网址,或者通过网页表单向服务器提交内容,这时浏览器就会向服务器发出 HTTP 请求。
1999年,微软公司发布 IE 浏览器5.0版,第一次引入新功能:允许 JavaScript 脚本向服务器发起 HTTP 请求。这个功能当时并没有引起注意,直到2004年 Gmail 发布和2005年 Google Map 发布,才引起广泛重视。
2005年2月,Ajax (Asynchronous JavaScript and XML) 这个词第一次正式提出,指的是通过 JavaScript 的异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。W3C 在2006年发布了它的国际标准。
XML 与 HTML 类似,都是一种基于标签的标记语言,不过 XML 没有预定义标签,一般用来存储数据。现在 XML 格式已经过时了,服务器返回的都是 JSON 格式的数据,AJAX 这个词的字面含义已经消失了,但它已成为 JavaScript 脚本发起 HTTP 通信的代名词,也就是说,只要用脚本发起通信,就可以叫做 AJAX 通信。
Ajax 的优点包括:
- 允许只更新一个
HTML页面的部分DOM,而无须重新加载整个页面 - 允许异步工作,这意味着当网页的一部分正试图重新加载时,您的代码可以继续运行(相比之下,同步会阻止代码继续运行,直到这部分的网页完成重新加载)。
存在的问题包括:
- 没有浏览历史(无法后退)
- 存在跨域问题(A网站向B网站发送内容)
- 对SEO优化不友好
通过交互式网站和现代 Web 标准,AJAX 正在逐渐被 JavaScript 框架中的函数和官方的 Fetch API 标准取代
Ajax 通过 xhr 对象实现,实现步骤为:
// 1. 创建 XMLHttpRequest 实例
const xhr = new XMLHttpRequest()
// 2. 发出 HTTP 请求
xhr.open('GET', 'http://127.0.0.1:3000/server?page=1')
xhr.send()
// 3. 接收服务器传回的数据
xhr.onreadystatechange = () => {
// 4. 更新网页数据
if(xhr.readyState === XMLHttpRequest.DONE || xhr.status===200) {
document.getElementById('target').innerHTML = xhr.responseText
}
}
XHR
XHR (XMLHttpRequest) 是一种创建 Ajax 请求的 JavaScript API。实际上 XHR 可以用于获取任何类型的数据,而不仅仅是 XML。它甚至支持 HTTP 以外的协议(包括 file:// 和 FTP),尽管可能受到更多出于安全等原因的限制。
构造函数 XMLHttpRequest,可以使用 new 命令生成实例,没有任何参数。
const xhr = new XMLHttpRequest()
Open & Send
生成实例后,就可以使用 open() 方法初始化一个请求
/**
* method: 要使用的 HTTP 方法,比如 GET、POST、PUT、DELETE
* url: 一个 DOMString 表示要向其发送请求的 URL
* async: 可选,表示是否异步执行操作,默认为true
* user: 可选,用户名用于认证用途;默认为 null
* password: 可选,密码用于认证用途,默认为 null
*/
xhr.open(method, url)
xhr.open(method, url, async)
xhr.open(method, url, async, user)
xhr.open(method, url, async, user, password)
然后使用 send() 方法发送请求
/**
* body: 可选参数,默认为null
* 返回值:undefined
*/
XMLHttpRequest.send(body)
body 参数接受的类型包括:
nullDocument对象,在发送之前被序列化XMLHttpRequestBodyInit
type XMLHttpRequestBodyInit =
Blob |
BufferSource |
FormData |
URLSearchParams |
string;
异常情况:
InvalidStateError:send()方法已经被调用但该请求还未结束NetworkError:请求发送资源类型是Blob,但请求方法不是GET
请求头
setRequestHeader()
用于设置浏览器发送的 HTTP 请求的头信息。该方法必须在 open() 之后、send() 之前调用。如果该方法多次调用,设定同一个字段,则每一次调用的值会被合并成一个单一的值发送。
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.setRequestHeader('Content-Length', JSON.stringify(data).length)
xhr.send(JSON.stringify(data))
getResponseHeader()
返回响应指定字段的值,如果还没有收到服务器回应或者指定字段不存在则返回 null,该方法的参数不区分大小写,如果有多个字段同名,它们的值会被连接为一个字符串,每个字段之间使用“逗号+空格”分隔
getAllResponseHeaders()
返回一个字符串,表示服务器发来的所有 HTTP 头信息。格式为字符串,每个头信息之间使用 CRLF 分隔(回车+换行),如果没有收到服务器回应,该属性为 null。如果发生网络错误,该属性为空字符串。
date: Fri, 08 Dec 2017 21:04:30 GMT\r\n
content-encoding: gzip\r\n
x-content-type-options: nosniff\r\n
server: meinheld/0.6.1\r\n
x-frame-options: DENY\r\n
content-type: text/html; charset=utf-8\r\n
connection: keep-alive\r\n
strict-transport-security: max-age=63072000\r\n
vary: Cookie, Accept-Encoding\r\n
content-length: 6502\r\n
x-xss-protection: 1; mode=block\r\n
withCredentials
XMLHttpRequest.withCredentials 属性是一个布尔值,表示跨域请求时,用户信息(比如 Cookie 和认证的 HTTP 头信息)是否会包含在请求之中,默认为false,即向 example.com 发出跨域请求时,不会发送 example.com 设置在本机上的 Cookie(如果有的话)。
xhr.open('GET', 'http://example.com/', true)
xhr.withCredentials = true
xhr.send(null)
为了让这个属性生效,服务器必须显式返回 Access-Control-Allow-Credentials 头信息
Access-Control-Allow-Credentials: true
withCredentials 属性打开的话,跨域请求不仅会发送 Cookie,还会设置远程主机指定的 Cookie。反之也成立,如果 withCredentials 属性没有打开,那么跨域的 AJAX 请求即使明确要求浏览器设置 Cookie,浏览器也会忽略。
注意,脚本总是遵守同源政策,无法从 document.cookie 或者 HTTP 回应的头信息之中,读取跨域的 Cookie,withCredentials 属性不影响这一点。
如果需要跨域 AJAX 请求发送 Cookie,需要 withCredentials 属性设为 true。注意,同源的请求不需要设置这个属性。
状态监听
一个 XHR 实例当前所处的状态可以通过 readyState 属性获取
| 值 | 状态 | 描述 |
|---|---|---|
0 | UNSENT | 对象被创建,但尚未调用 open() 方法 |
1 | OPENED | open() 方法已经被调用 |
2 | HEADERS_RECEIVED | send() 方法已经被调用,并且头部和状态可读取 |
3 | LOADING | 加载中,responseText 属性已经包含部分数据 |
4 | DONE | 加载操作已完成 |
通信过程中,每当实例对象发生状态变化,它的 readyState 属性的值就会改变。这个值每一次变化,都会触发 readyStateChange 事件。XMLHttpRequest.onreadystatechange 属性指向一个监听函数,readystatechange 事件发生时,就会执行这个属性。
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) { } // 请求结束,处理服务器返回的数据
else { } // 显示提示 加载中...
}
另外,如果调用实例的 abort() 方法将终止请求,也会造成 readyState 属性变化
事件监听
XHR 对象还继承了 XMLHttpRequestEventTarget 接口定义的事件相关属性
| 属性 | 事件类型 | 描述 |
|---|---|---|
onloadstart | loadstart | 开始加载 |
onload | load | 请求结束,数据加载完毕 |
onloadend | loadend | 请求结束,状态未知 在触发 error,abort 或 load 事件之后 |
onprogress | progress | 请求接收到数据的时候被周期性触发 |
onabort | abort | 请求终止 |
onerror | error | 请求遇到错误 |
ontimeout | timeout | 当进度由于预定时间到期而终止 |
进度
上传文件时,通过 XMLHttpRequest.upload 属性可以得到一个 upload 对象
interface XMLHttpRequest extends XMLHttpRequestEventTarget {
readonly upload: XMLHttpRequestUpload;
}
interface XMLHttpRequestUpload extends XMLHttpRequestEventTarget {}
XMLHttpRequestUpload 接口同样继承了 XMLHttpRequestEventTarget 接口,通过监听其 progress 事件,可以周期性的监听上传的进度
const upload = xhr.upload
upload.addEventListener('progress', (evt) => {
if (evt.lengthComputable) {
const percentComplete = evt.loaded / evt.total;
}
})
超时
XMLHttpRequest.timeout 属性返回一个整数,表示多少毫秒后,如果请求仍然没有得到结果,就会自动终止。如果该属性等于 0,就表示没有时间限制。
xhr.timeout = 10 * 1000 // 指定 10 秒钟超时
xhr.ontimeout = () => {
console.error('请求超时')
}
中断
除了超时自动请求外,可以调用 XMLHttpRequest.abort() 方法主动中断请求
xhr.abort()
xhr.onabort = () => {
console.log('请求已中断')
}
响应处理
Status
XMLHttpRequest.status 属性返回一个整数,表示服务器回应的 HTTP 状态码。XMLHttpRequest.statusText 属性返回一个字符串,表示服务器发送的状态提示。
| status | statusText | 描述 |
|---|---|---|
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 | 服务器发生错误 |
基本上,只有 2xx 和 304 的状态码,表示服务器返回是正常状态
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || (xhr.status === 304)) {
// 处理服务器的返回数据
} else {
// 出错
}
}
Response
XHR 实例表达响应数据相关的属性包括:
response
响应的正文。返回的类型为 ArrayBuffer、Blob 、 Document、JavaScript Object 或 DOMString 中的一个,这取决responseType 属性。
responseType
一个枚举字符串值,用于指定响应中包含的数据类型,可以采用以下值:
- 空字符串:与默认类型
text相同 arraybuffer: 包含二进制数据的JavaScript ArrayBufferblob: 包含二进制数据的Blob对象document: 根据接收到的数据的MIME类型而定,HTML Document或XML XMLDocumentjson: 通过将接收到的数据内容解析为JSON而创建的JavaScript对象text:DOMString对象中的文本
将 responseType 设置为特定值时,应确保服务器实际发送的响应与该格式兼容。如果服务器返回的数据与设置的 responseType 不兼容,则 response 的值将为null
responseText
在一个请求被发送后,从服务器端返回的纯文本的值,为 null 时,表示请求失败了,为空字符串时,表示这个请求还没有被 send()
reponseURL
返回响应的序列化 URL,如果 URL 为空则返回空字符串。如果 URL 有锚点,则位于 URL # 后面的内容会被删除。如果 URL 有重定向,responseURL 的值会是经过多次重定向后的最终 URL。
responseXML
从 XMLHttpRequest 中收到的 HTML 节点或解析后的 XML 节点,也可能是在没有收到任何数据或数据类型错误的情况下返回的 null
如果服务器没有明确指出 Content-Type 头是 text/xml 还是 application/xml, 可以使用 XMLHttpRequest.overrideMimeType() 强制 XMLHttpRequest 解析为 XML。
overrideMimeType()
XMLHttpRequest.overrideMimeType() 方法用来指定 MIME 类型,覆盖服务器返回的真正的 MIME 类型,从而让浏览器进行不一样的处理。该方法必须在 send() 方法之前调用。
正常情况下应该使用 responseType 属性告诉服务器返回指定类型的数据,只有在服务器无法返回某种数据类型时,才使用 overrideMimeType() 方法。
typescript 声明参考
typescript/lib/lib.dom.d.ts
XMLHttpRequestEventTarget
/**
* 度量一个正在进行的过程的事件接口:
* 1. HTTP请求
* 2. 加载 <img>, <audio>, <video>, <style> 或 <link>
*/
interface ProgressEvent<T extends EventTarget = EventTarget> extends Event {
readonly lengthComputable: boolean;
readonly loaded: number;
readonly target: T | null;
readonly total: number;
}
/**
* XMLHttpRequest事件类型
*/
interface XMLHttpRequestEventTargetEventMap {
"abort": ProgressEvent<XMLHttpRequestEventTarget>;
"error": ProgressEvent<XMLHttpRequestEventTarget>;
"load": ProgressEvent<XMLHttpRequestEventTarget>;
"loadend": ProgressEvent<XMLHttpRequestEventTarget>;
"loadstart": ProgressEvent<XMLHttpRequestEventTarget>;
"progress": ProgressEvent<XMLHttpRequestEventTarget>;
"timeout": ProgressEvent<XMLHttpRequestEventTarget>;
}
/**
* XMLHttpRequest事件类型对应的属性接口
*/
interface XMLHttpRequestEventTarget extends EventTarget {
onabort: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
onerror: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
onload: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
onloadend: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
onloadstart: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
onprogress: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
ontimeout: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
addEventListener<K extends keyof XMLHttpRequestEventTargetEventMap>(type: K, listener: (this: XMLHttpRequestEventTarget, ev: XMLHttpRequestEventTargetEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof XMLHttpRequestEventTargetEventMap>(type: K, listener: (this: XMLHttpRequestEventTarget, ev: XMLHttpRequestEventTargetEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
/**
* XMLHttpRequest事件对象的构造函数
*/
declare var XMLHttpRequestEventTarget: {
prototype: XMLHttpRequestEventTarget;
new(): XMLHttpRequestEventTarget;
};
XHR
type XMLHttpRequestBodyInit = Blob | BufferSource | FormData | URLSearchParams | string;
type XMLHttpRequestResponseType = "" | "arraybuffer" | "blob" | "document" | "json" | "text";
/**
* XMLHttpRequest事件类型
*/
interface XMLHttpRequestEventMap extends XMLHttpRequestEventTargetEventMap {
"readystatechange": Event;
}
/**
* 使用 XMLHttpRequest (XHR) 对象和服务器进行交互
* 1. 在不刷新整个页面的情况下从一个URL获取数据
* 2. 网页部分更新而不阻断用户的操作
*/
interface XMLHttpRequest extends XMLHttpRequestEventTarget {
onreadystatechange: ((this: XMLHttpRequest, ev: Event) => any) | null;
/** 请求客户端的状态 */
readonly readyState: number;
/** 响应体 */
readonly response: any;
/**
* 响应体的文本形式
* 如果responseType为空字符串或text的时候抛出 InvalidStateError 错误
*/
readonly responseText: string;
/**
* 响应体的类型
* 1. 空字符串(default)
* 2. arraybuffer
* 3. blob
* 4. document
* 5. json
* 6. text
*
* 全局对象不是window的时候,设置为 document将被忽略
* 请求在加载中或已完成时设置抛出 InvalidStateError
* 同步请求且当前全局对象不是window时抛出 InvalidAccessError
*/
responseType: XMLHttpRequestResponseType;
readonly responseURL: string;
/**
* document 形式的响应
* 当responseType不为空字符串或document的时候抛出 InvalidStateError
*/
readonly responseXML: Document | null;
readonly status: number;
readonly statusText: string;
/**
* 毫秒为单位
* 设置为非0值会在指定时间结束后终止请求
* 指定时间结束后,请求未完成且非异步请求,timeout事件被触发,或者send()方法抛出 TimeoutError
* 同步请求且当前全局对象不是window时抛出 InvalidAccessError
*/
timeout: number;
/**
* 返回一个关联的XMLHttpRequestUpload对象
* 用于获取文件传输过程中的信息
*/
readonly upload: XMLHttpRequestUpload;
/**
* True when credentials are to be included in a cross-origin request. False when they are to be excluded in a cross-origin request and when cookies are to be ignored in its response. Initially false.
*
* When set: throws an "InvalidStateError" DOMException if state is not unsent or opened, or if the send() flag is set.
*/
withCredentials: boolean;
/** 中止网络请求 */
abort(): void;
getAllResponseHeaders(): string;
getResponseHeader(name: string): string | null;
/**
* Sets the request method, request URL, and synchronous flag.
*
* Throws a "SyntaxError" DOMException if either method is not a valid method or url cannot be parsed.
*
* Throws a "SecurityError" DOMException if method is a case-insensitive match for `CONNECT`, `TRACE`, or `TRACK`.
*
* Throws an "InvalidAccessError" DOMException if async is false, current global object is a Window object, and the timeout attribute is not zero or the responseType attribute is not the empty string.
*/
open(method: string, url: string | URL): void;
open(method: string, url: string | URL, async: boolean, username?: string | null, password?: string | null): void;
/**
* Acts as if the `Content-Type` header value for a response is mime. (It does not change the header.)
* Throws an "InvalidStateError" DOMException if state is loading or done.
*/
overrideMimeType(mime: string): void;
/**
* 初始化请求
* 当请求方式为GET或HEAD时忽略body参数
* Throws an "InvalidStateError" DOMException if either state is not opened or the send() flag is set.
*/
send(body?: Document | XMLHttpRequestBodyInit | null): void;
/**
* Combines a header in author request headers.
* Throws an "InvalidStateError" DOMException if either state is not opened or the send() flag is set.
* Throws a "SyntaxError" DOMException if name is not a header name or if value is not a header value.
*/
setRequestHeader(name: string, value: string): void;
readonly DONE: number;
readonly HEADERS_RECEIVED: number;
readonly LOADING: number;
readonly OPENED: number;
readonly UNSENT: number;
addEventListener<K extends keyof XMLHttpRequestEventMap>(type: K, listener: (this: XMLHttpRequest, ev: XMLHttpRequestEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof XMLHttpRequestEventMap>(type: K, listener: (this: XMLHttpRequest, ev: XMLHttpRequestEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
/**
* XMLHttpRequest 构造函数
*/
declare var XMLHttpRequest: {
prototype: XMLHttpRequest;
new(): XMLHttpRequest;
readonly DONE: number;
readonly HEADERS_RECEIVED: number;
readonly LOADING: number;
readonly OPENED: number;
readonly UNSENT: number;
};
upload 属性对应的接口定义
interface XMLHttpRequestUpload extends XMLHttpRequestEventTarget {
addEventListener<K extends keyof XMLHttpRequestEventTargetEventMap>(type: K, listener: (this: XMLHttpRequestUpload, ev: XMLHttpRequestEventTargetEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof XMLHttpRequestEventTargetEventMap>(type: K, listener: (this: XMLHttpRequestUpload, ev: XMLHttpRequestEventTargetEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
declare var XMLHttpRequestUpload: {
prototype: XMLHttpRequestUpload;
new(): XMLHttpRequestUpload;
};