ajax、web sockets 及跨域

ajax

ajax 全称 “Asynchronous Javascript + XML”,它利用浏览器原生的通信能力,能实现页面的局部更新。ajax 请求可以通过 XMLHttpRequest 对象发送;在兼容性上,IE7+, Firefox, Opera, Chrome 和 Safari 均支持原生的 XHR 对象。以下是 XHR 对象的基本使用样例:

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
const xhr = new XMLHttpRequest();

// readyState 四种状态
// 0: 未初始化,即尚未调用 xhr.open 方法
// 1: 启动,已调用 xhr.open 方法,但未调用 xhr.send 方法
// 2: 发送,已调用 xhr.send 方法,但未接收到响应
// 3: 接收,已接收到部分响应数据
// 4: 完成,接收到全部响应数据,且可以在客户端使用了
xhr.onreadystatechange = () => {
// 响应数据自动填充到 xhr 对象上
// responseText 响应主体返回的文本
// responseXML 如果响应的内容类型为 "text/xml" 或 "application/xml"。responseXML 会保存响应数据的 XML DOM 文档
// status 响应的 HTTP 状态
// statusText HTTP 状态的说明
if (xhr.readyState == 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
console.log(xhr.responseText);
} else {
console.log("Request was unsuccessful: "+ xhr.status);
}
}
}

// 准备发送请求
// 第三个参数表示是否发送异步请求
xhr.open("get", "test.json", false);

// 发送请求
xhr.send(null);

Comet

Comet 指服务器推送,促使浏览器持续不断地接受服务端响应。基本的 Comet 实现可借助 XHR 对象长轮询:服务端每发回一波响应,xhr.readyState 状态值都会被置为 3;直到服务端发送完响应内容后,xhr.readyState 才会被置为 4。使用 XHR 对象实现长轮询的样例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 通过 progress, finished 回调处理接受到的内容
function createStreamingClient(url, progress, finished) {
const xhr = new XMLHttpRequest();
let received = 0;

xhr.open("get", url, true);
xhr.onreadystatechange = () => {
let result;

if (xhr.readyState == 3) {
result = xhr.responseText.substring(received);
received += result.length;

progress(result);
} else if (xhr.readyState == 4) {
finished(xhr.responseText);
};
}

xhr.send(null);
return xhr;
}

SSE

SSE 全称 Server-Sent Event,服务器发送事件。SSE 适用于处理只读 Comet 交互。在兼容性上,Firefox 6+, Safari 5+, Opera 11+, Chrome 和 iOS 4+ 版 Safari 支持 SSE。SSE 的基本使用样例如下:

1
2
3
4
5
6
7
// 参数 url 必须与页面同源
const source = new EventSource("test.json");

// 通过 onmessage 事件持续不断的接受数据
source.onmessage = (event) => {
console.log(event.data);
}

Web Sockets

Web Sockets 可用于实现浏览器和服务端的全双工、双向通信。创建 WebSocket 对象后,首先会发送 http 请求到服务端;取得响应后,建立的连接会从 http 协议升级成 Web Sockets 协议,以支持双向通信。Web Sockets 没有同源策略,因此可以跨域通信。支持 Web Sockets 的浏览器有:Firefox 6+, Safari 5+, Chrome 和 iOS 4+ 版 Safari。Web Sockets 的使用样例如下:

1
2
3
4
5
const socket = new WebSocket("ws://www.example.com/socket");// 必须是绝对路径
socket.send(data);
socket.onmessage = (event) => {
console.log(event.data);
};

跨域

CORS

CORS 全称 Cross-Origin Resource Sharing,跨域资源共享。服务器通过设置 Access-Control-Allow-Origin 头,开启跨域访问资源的可能。此时浏览器若想跨域访问资源,需要设置 Origin 请求头为许可的请求页面地址信息。在跨域请求的过程中,当浏览器接受到响应时,会根据 Access-Control-Allow-Origin 判断这次请求是不是有效的。注意,请求和响应都不包含 cookie 信息。

IE 8~9 需要使用 XDomainRequest 对象发送跨域请求。

Firefox 3.5+, Safari 4+, Chrome, iOS 版 Safari 和 Android 平台中的 Webkit 都可以通过 XHR 对象发送跨域请求,而不需要其他处理。该跨域请求默认不会携带 cookie 和 http 认证信息,此时可以将 xhr.withCredentials 置为 true,这样就会携带 cookie 和 http 认证信息。有些浏览器默认会发送 cookie,此时通过设置 xhr.withCredentials = false,可以禁止携带 cookie 信息。

非简单请求的跨域,会执行一次预检请求,详情可参看跨域资源共享 CORS 详解

检测浏览器是否支持 XHR 对象的跨域请求,可以通过判断 XHR 对象是否包含 withCredentials 属性。在此基础上,再判断 XDomainRequest 对象是否存在,就可以覆盖所有浏览器。

图像 Ping

鉴于加载图像不存在跨域问题,图像 Ping 技术通过动态的 Image 实例进行跨域通信。它只能将请求通知给服务器,不能接受响应,最常用于收集点击信息。

JSONP

与图像 Ping 技术相仿,JSONP 通过插入动态的 script 节点进行跨域通信,因此也只适用于 get 请求。在 JSONP 中,服务端返回的脚本信息会被执行,因此可用于处理响应。为了处理上的灵活性,JSONP 请求会携带回调函数的名称作为请求参数,以便服务器拼接响应,约束浏览器在执行脚本时调用特定的函数处理响应。

Web Sockets

见上文。

axios

在设计上,axios 采用适配器模式切换发送请求的模块(分别为浏览器端的 XHR 对象、服务器端的 http, https 模块),该发送请求的模块作为 axios 的主流程。在主流程之外,axios 借助 Promise 对象装配拦截器(包含在主流程前的请求拦截器、在主流程后的响应拦截器),其实现等同一条链式处理的中间件机制。其核心流程如下图,首先由请求拦截器处理请求,然后通过选用特定的适配器发送请求,最后由响应拦截器处理响应。

在代码中的表现为:

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
Axios.prototype.request = function request(config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}

config = mergeConfig(this.defaults, config);

// Set config.method
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}

// Hook up interceptors middleware
// dispatchRequest 本质使用特定的适配器发送请求
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);

this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});

while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}

return promise;
};

在这条链式的处理流程中,随处流动的实体是 config 对象。对于 get 和 post 请求,在适配器 xhr 以及 http 模块中,config.url, config.params 将被转化成实际请求路径,config.data 将作为请求数据。config.params 也能在 post 请求中使用。此外,get 和 post 请求中不同的头部信息通过策略模式处理;传输数据和头部 content-type 值的联动关系则通过 config.transformRequest 方法处理。

xhr 模块对请求头的处理包含防 csrf 攻击的机制:同源或 request.withCredentials 为真值时,将 cookie 中 xsrf 相关内容写入请求头中。

http 模块包含代理机制的实现,即使用 config.proxy 配置项制作实际的请求地址等值。

此外,axios 使用 Promise 实现了撤销请求的机制。cancel 操作会为 token 令牌注入标识,同时通过 resolvePromise 方法间接调用 xhr.abort 或 req.abort,从而取消请求。详情可参看源码。

umi-request

umi-request 与 axios 不同的是:采用如 koa 的中间件机制应用拦截器;使用 isomorphic-fetch 库切换客服端、服务端的请求模块;对 get 请求实现缓存机制。因为 isomorphic-fetch 没有实现超时、撤销请求这两个功能,umi-request 使用 Promise.race 权衡正常请求、超时请求、撤销请求的优先级,最终实现这两个功能。

参考

跨域资源共享 CORS 详解
传统 Ajax 已死,Fetch 永生