HTTP Request In Browser

日常开发中和服务器进行数据交换我们会发起 HTTP 请求,比较传统的做法是使用 XMLHttpRequest,也出现了很多基于 XMLHttpRequest 封装的比较不错的库如axios,或者使用较为新的 Fetch。本篇文章从 HTTP Messages 出发(关于 HTTP Messages 详见之前的文章),讲述 XMLHttpRequest 和 Fetch 两种方式下 Request 和 Response 组成内容对应的属性或方法,以及两种方式比较重要的配置项,最后探讨一下两种方式的优缺点。

Fetch

Fetch 在 Chrome 浏览器的 40 版本(2015)被引入,至今已经六年时间。作为规范标准,现如今主流浏览器都支持,基本上可以不用引入 polyfill 直接使用。在 TypeScript 中 fetch()的类型定义为fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;

Request Messages

在使用 fetch 时可以将 Request 实例作为参数传入,Request 类的使用也很简单:new Request(input[, init]),input 我们常用的就是目标 url,init 的类型为RequestInit,和 fetch 的可选 init 一致。
RequestInit的定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
interface RequestInit {
/**
* A BodyInit object or null to set request's body.
*/
body?: BodyInit | null;
/**
* A string indicating how the request will interact with the browser's cache to set request's cache.
*/
cache?: RequestCache;
/**
* A string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. Sets request's credentials.
*/
credentials?: RequestCredentials;
/**
* A Headers object, an object literal, or an array of two-item arrays to set request's headers.
*/
headers?: HeadersInit;
/**
* A cryptographic hash of the resource to be fetched by request. Sets request's integrity.
*/
integrity?: string;
/**
* A boolean to set request's keepalive.
*/
keepalive?: boolean;
/**
* A string to set request's method.
*/
method?: string;
/**
* A string to indicate whether the request will use CORS, or will be restricted to same-origin URLs. Sets request's mode.
*/
mode?: RequestMode;
/**
* A string indicating whether request follows redirects, results in an error upon encountering a redirect, or returns the redirect (in an opaque fashion). Sets request's redirect.
*/
redirect?: RequestRedirect;
/**
* A string whose value is a same-origin URL, "about:client", or the empty string, to set request's referrer.
*/
referrer?: string;
/**
* A referrer policy to set request's referrerPolicy.
*/
referrerPolicy?: ReferrerPolicy;
/**
* An AbortSignal to set request's signal.
*/
signal?: AbortSignal | null;
}

下面我们针对 HTTP Request Messages 的组成来说明一下对应的配置项:

  • method: 对应RequestInit中的 method
  • remote-url: 对应Request(input,[,init])fetch(input,[,init])的 input 入参
  • headers: 对应RequestInit中的 headers
  • body: 对应RequestInit中的 body

除了我们本身不能定义的 HTTP version,其它的 Messages 都可以自行定义。知道了这些对应的配置项,简单的开发需求也就可以轻松胜任了。但是还有一些配置项是比较重要的:

  • mode: 可选值: "cors" | "navigate" | "no-cors" | "same-origin",是否允许跨域请求的相关设置,默认值为cors,即允许跨域请求
  • credentials: 影响 cookies, HTTP auth 等凭证的传输和设置行为,可选值: "include" | "omit" | "same-origin",默认值为same-origin,即允许同源的credentials传输,在 CORS 跨域传输时需要设置值为include
  • integrity: subresource integrity文件完整性验证,目前支持的哈希算法包括:sha256, sha384, 和 sha512。如果对比的结果不一致,则会抛出错误
  • keepalive: 可以允许 fetch 请求在关闭页面之后仍然存在。在 window.onunload 中设置 fetch 的 keepalive 为 true,这样离开页面的动作和相关记录仍然可以传到服务器,可以应用于离开页面的相关统计需求。但是 keepalive 请求的 body 限制为 64kb,且是当前所有 keepalive 请求的总和。
  • signal: 取消 fetch 请求的标志, 取消请求会返回AbortError,示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let controller = new AbortController();
setTimeout(() => controller.abort(), 1000);

try {
let response = await fetch('/url', {
signal: controller.signal,
});
} catch (err) {
if (err.name == 'AbortError') {
// handle abort()
alert('Aborted!');
} else {
throw err;
}
}

Response Messages

Fetch 针对 HTTP 响应有专门的类,相关定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Body {
readonly body: ReadableStream<Uint8Array> | null;
readonly bodyUsed: boolean;
arrayBuffer(): Promise<ArrayBuffer>;
blob(): Promise<Blob>;
formData(): Promise<FormData>;
json(): Promise<any>;
text(): Promise<string>;
}

interface Response extends Body {
readonly headers: Headers;
readonly ok: boolean;
readonly redirected: boolean;
readonly status: number;
readonly statusText: string;
readonly type: ResponseType;
readonly url: string;
clone(): Response;
}

还是针对 HTTP Response Messages 的组成来说明一下对应的配置项:

  • status: 对应Response Interface中的 status,此外还有针对 2xx 状态码的 ok
  • reason-phrase: 对应Response Interface中的 statusText
  • headers: 对应Response Interface中的 headers
  • body: 对应Body Interface中的 body,此外还有判断 body 内容是否被获取的 bodyUsed,以及 arrayBuffer(),blob(),formData(),json(),text()方法获取 body 内容。需要注意的是Body Interface相关方法只有第一次能返回结果,后续调用相关方法则会直接抛出TypeError错误

XMLHttpRequest

XMLHttpRequest 自从 1999 年被 IE 5 引入之后,陆续被各浏览器支持,并被很多著名应用所使用(例如 Google 的 Gmail)。现在很多网站还是基于 XHR 来进行 HTTP 请求,可见 XHR 的经典之处。XHR 的使用也比较简单,让我们快速回顾一下:

1
2
3
4
5
6
7
8
const xhr = new XMLHttpRequest();
xhr.open('GET', '/analyze');
xhr.onload = function () {
if (xhr.readyState === xhr.DONE && xhr.status === 200) {
console.log(xhr.response);
}
};
xhr.send();

Request Messages

沿用之前的思路,我们来分析一下 HTTP Request Messages 对应的 XHR 配置项:

  • method: 对应open(method, url[, async[, user[, password]]])方法中的 method
  • remote-url: 对应open(method, url[, async[, user[, password]]])方法中 url
  • headers: 可以使用setRequestHeader(header, value)来设置需要的 headers
  • body: 对应send(body)中的 body

Response Messages

XHR 的 HTTP Response Messages 的组成对应的配置项为:

  • status: 对应 XHR 实例的 status 值
  • reason-phrase: 对应 XHR 实例的 statusText 值
  • headers: XHR 实例获取 headers 内容有两种方法:getAllResponseHeaders()getResponseHeader(headerName)getAllResponseHeaders()是获取全部 headers 并且用\r\n来分割每个 header,getResponseHeader(headerName)则是获取某个 header 对应的值
  • body: 对应 XHR 实例的 response 值,此外 responseType 的值为返回内容的类型,可以根据相关返回类型来做进一步处理。

XHR 相比 Fetch 之外有一个比较特殊的属性 readyState,可选范围与意义如下所示:
| Value | State | Description |
| :—- | :—————– | :———————————————————– |
| 0 | UNSENT | Client has been created. open() not called yet. |
| 1 | OPENED | open() has been called. |
| 2 | HEADERS_RECEIVED | send() has been called, and headers and status are available. |
| 3 | LOADING | Downloading; responseText holds partial data. |
| 4 | DONE | The operation is complete. |

LOADINGDONE等作为 XMLHttpRequest 的 static properties 是可以直接访问的,我们可以用readyState === XMLHttpRequest.DONE来判断请求是否完成,好处是有更好的语义化。但是在早期的 IE 浏览器定义不一样,如果要适配 IE 11 以下的浏览器,还是建议以readyState === 4为请求结束的判断条件。

其它比较重要的属性或者方法为:

  • XHR.timeout: 设置超时时间,单位为毫秒
  • XHR.withCredentials: 在进行跨域 CORS 请求时,是否带上credentials,默认值为false
  • XHR.abort(): 取消请求的方法

此外 XHR 还有相关的事件,包括 load,loadstart,loadend,process,abort,timeout,error,每种事件对应 XHR 实例上有相应的on方法。

Summary

从 XMLHttpRequest 和 Fetch 的整体设计来看,Fetch 将 Request,Response,Header 等划分的更为具体,整体更加的清晰。而 XMLHttpRequest 则将各种方法和属性以及相关的事件糅合在一起,显得较为杂乱无章。而且 Fetch 返回的值是一个 Promise,能更好的避免回调地狱的问题。但是 Fetch 在现阶段对上传过程中的进度处理没有比较好的解决方法,这一点 XMLHttpRequest 则可以监听 process 事件来比较方便的解决上传进度问题。

References

Comments