Ajax & XHR 基础

560 阅读10分钟

简介

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

1999年,微软公司发布 IE 浏览器5.0版,第一次引入新功能:允许 JavaScript 脚本向服务器发起 HTTP 请求。这个功能当时并没有引起注意,直到2004年 Gmail 发布和2005年 Google Map 发布,才引起广泛重视。

2005年2月,Ajax (Asynchronous JavaScript and XML) 这个词第一次正式提出,指的是通过 JavaScript 的异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。W3C 在2006年发布了它的国际标准。

XMLHTML 类似,都是一种基于标签的标记语言,不过 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),尽管可能受到更多出于安全等原因的限制。

规范:xhr.spec.whatwg.org/

构造函数 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 参数接受的类型包括:

  • null
  • Document 对象,在发送之前被序列化
  • XMLHttpRequestBodyInit
type XMLHttpRequestBodyInit = 
  Blob | 
  BufferSource | 
  FormData | 
  URLSearchParams | 
  string;

异常情况:

  • InvalidStateErrorsend() 方法已经被调用但该请求还未结束
  • 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 回应的头信息之中,读取跨域的 CookiewithCredentials 属性不影响这一点。

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

状态监听

一个 XHR 实例当前所处的状态可以通过 readyState 属性获取

状态描述
0UNSENT对象被创建,但尚未调用 open() 方法
1OPENEDopen() 方法已经被调用
2HEADERS_RECEIVEDsend() 方法已经被调用,并且头部和状态可读取
3LOADING加载中,responseText 属性已经包含部分数据
4DONE加载操作已完成

通信过程中,每当实例对象发生状态变化,它的 readyState 属性的值就会改变。这个值每一次变化,都会触发 readyStateChange 事件。XMLHttpRequest.onreadystatechange 属性指向一个监听函数,readystatechange 事件发生时,就会执行这个属性。

xhr.onreadystatechange = () => {
  if (xhr.readyState === 4) { } // 请求结束,处理服务器返回的数据
  else { } // 显示提示 加载中...
}

另外,如果调用实例的 abort() 方法将终止请求,也会造成 readyState 属性变化

事件监听

XHR 对象还继承了 XMLHttpRequestEventTarget 接口定义的事件相关属性

属性事件类型描述
onloadstartloadstart开始加载
onloadload请求结束,数据加载完毕
onloadendloadend请求结束,状态未知
在触发 errorabortload 事件之后
onprogressprogress请求接收到数据的时候被周期性触发
onabortabort请求终止
onerrorerror请求遇到错误
ontimeouttimeout当进度由于预定时间到期而终止

进度

上传文件时,通过 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 属性返回一个字符串,表示服务器发送的状态提示。

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

基本上,只有 2xx304 的状态码,表示服务器返回是正常状态

if (xhr.readyState === 4) {
  if ((xhr.status >= 200 && xhr.status < 300) || (xhr.status === 304)) {
    // 处理服务器的返回数据
  } else {
    // 出错
  }
}

Response

XHR 实例表达响应数据相关的属性包括:

  1. response

响应的正文。返回的类型为 ArrayBufferBlobDocumentJavaScript ObjectDOMString 中的一个,这取决responseType 属性。

  1. responseType

一个枚举字符串值,用于指定响应中包含的数据类型,可以采用以下值:

  • 空字符串:与默认类型 text 相同
  • arraybuffer: 包含二进制数据的 JavaScript ArrayBuffer
  • blob: 包含二进制数据的 Blob 对象
  • document: 根据接收到的数据的 MIME 类型而定, HTML DocumentXML XMLDocument
  • json: 通过将接收到的数据内容解析为 JSON 而创建的 JavaScript 对象
  • text: DOMString 对象中的文本

responseType 设置为特定值时,应确保服务器实际发送的响应与该格式兼容。如果服务器返回的数据与设置的 responseType 不兼容,则 response 的值将为null

  1. responseText

在一个请求被发送后,从服务器端返回的纯文本的值,为 null 时,表示请求失败了,为空字符串时,表示这个请求还没有被 send()

  1. reponseURL

返回响应的序列化 URL,如果 URL 为空则返回空字符串。如果 URL 有锚点,则位于 URL # 后面的内容会被删除。如果 URL 有重定向,responseURL 的值会是经过多次重定向后的最终 URL

  1. 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;
};