前言
学会了 Ajax 的请求以及如何处理服务端的响应。这一章节,我们着重来封装一个简单的 Ajax。
- 本章节会使用部分 ES6 语法
- 本章节使用 Promise
- 支持 Promise 语法处理结果
- 支持自定义配置,包括 headers
- 内置 url、params、 data、headers 处理
1. 构造一个这样的 xhr
function xhr(config) {
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
request.open(method, url);
request.onreadystatechange = function handleLoad() {
if (request.readyState !== 4) return
if (request.status === 0) return
const responseData = request.response resolve(responseData)
}
request.send(data)
});}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
首先, 我们的 xhr 函数支持 config 传入, 内部通过 XMLHttpRequest 技术来进行请求的收发, 大致就是上面这样结构的代码,内部的实现我们前面章节都讲过,唯一不同的是,在 onreadystatechange 上,我们挂载的方法最后使用 resolve()
来进行断言,这样做的目的是,后续可以通过 .then()
的方式进行数据操作。
1.1 method 标准化
首先, 用户传进来的 method 可能是大写也可能是小写,我们可以先做一个标准化,对 method 做一个转化,将其变为大写:
1.2 构建 url
有些同学很奇怪,为什么说构建 url,我们不是通过 config 传入 url 吗?
因此,我们需要把 params 上的参数进行一定格式序列化拼接到 url 后面 ,构成 "url?a=xxx&b=xxx"
的格式。为此,我们需要提供了一个 buildUrl 的函数:
function buildUrl(url, params) {
if (!params || !isPlainObject(params)) return url;
let values = [];
Object.keys(params).forEach(key => {
const val = params[key];
if (typeof val === undefined || val === null) {
return;
}
values.push(`${key}=${val}`);
});
let serializedParams = values.join("&");
if (serializedParams) {
url += (url.indexOf("?") === -1 ? "?" : "&") + serializedParams;
}
return url;}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
在这个函数中,我们可以传参 url 和 params。如果传入params 为假值,那我们直接忽略,返回 url 即可。否则,我们需要对 params 中 的每一项目进行序列化,变为 "key=vaue"
这样的形式, 添加到 values 数组中。接着我们通过数组的 .join("&")
的方法,把 values 数组通过 “&” 进行拼接。最后拼接到 url 后面,构成 "url?key=value&key=value"
的形式返回。
这里,我们也涉及到了一个工具函数 isPlainObject,在本章节中好几处都会用到,他的作用是判断该对象是不是一个纯 “{}” 的对象,它的实现如下:
const toString = Object.prototype.toString; function isPlainObject(val) {
return toString.call(val) === "[object Object]";}
1.3 标准化 data
因为 .send() 是无法支持 Json 格式数据的,所以我们需要对 data 做一个序列化处理:
function transformData (data) {
if (isPlainObject(data)) {
return JSON.stringify(data)
}
return data}
实现非常简单,如果判断 data 是一个纯对象的话,就加一道 JSON.stringify(data)
的操作进行序列化, 否则直接返回 data 本身。
对于 headers 的操作,我们会着重对 Content-Type 进行处理,在没有 Content-Type 的时候,我们应该有个默认的支持。因为 headers 属性上是大小写不敏感的,因此我们会对 Content-Type 做一个统一处理:
function transformHeaders (headers) {
const contentTypeKey = 'Content-Type'
if (isPlainObject(headers)) {
Object.keys(headers).forEach(key => {
if (key !== contentTypeKey && key.toUpperCase() === contentTypeKey.toLowerCase()) {
headers[contentTypeKey] = headers[key]
delete headers[key]
}
})
if (!headers[contentTypeKey]) {
headers[contentTypeKey] = 'application/json;charset=utf-8'
}
}}transformHeaders(headers)Object.keys(headers).forEach(key => {
if (!data && key === 'Content-Type') {
delete headers[key]
return
}
request.setRequestHeader(key, headers[key])})
- 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
transformHeaders 函数对 headers 进行了一定程度的转化,包括为 Content-Type 提供了默认的支持,这里默认为 "application/json;charset=utf-8"
。在 xhr 函数中,我们还会对headers的每一项进行判断,如果没有 data ,那我们会删除 Content-Type。同时,我们会调用 setRequestHeader 方法将 headers 属性添加到头部。
1.5 设置响应类型
if (responseType) {
request.responseType = responseType;}
1.6 设置超时时间
if (timeout) {
request.timeout = timeout;}
1.7 处理结果
request.onreadystatechange = function handleLoad() {
if (request.readyState !== 4) return;
if (request.status === 0) return;
const responseData =
request.responseType === "text"
? request.responseText : request.response;
if (request.status >= 200 && request.status < 300 || request.status === 304) {
resolve(responseData);
} else {
reject(new Error(`Request failed with status code ${request.status}`));
}};request.onerror = function hadleError() {
reject(new Error('Network Error'))}request.ontimeout = function handleTimeout() {
reject(new Error(`Timeout of ${timeout} ms exceeded`))}
- 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
其中,正常处理服务端响应还要判断状态码,这里判断正确的是 200 至 300 之间状态码,再一个是 304 缓存。此时我们会通过 resolve 断言数据。否则,通过 reject 来断言失败原因。
1.8 xhr 函数
function xhr(config) {
return new Promise((resolve, reject) => {
const {
url,
method = "get",
params = {},
data = null,
responseType,
headers,
timeout } = config;
const request = new XMLHttpRequest();
request.open(method.toUpperCase(), buildUrl(url, params));
if (responseType) {
request.responseType = responseType;
}
if (timeout) {
request.timeout = timeout;
}
transformHeaders(headers);
Object.keys(headers).forEach(key => {
if (!data && key === "Content-Type") {
delete headers[key];
return;
}
request.setRequestHeader(key, headers[key]);
});
request.onreadystatechange = function handleLoad() {
if (request.readyState !== 4) return;
if (request.status === 0) return;
const responseData =
request.responseType === "text"
? request.responseText : request.response;
if (request.status >= 200 && request.status < 300 || request.status === 304) {
resolve(responseData);
} else {
reject(new Error(`Request failed with status code ${request.status}`));
}
};
request.onerror = function hadleError() {
reject(new Error("Network Error"));
};
request.ontimeout = function handleTimeout() {
reject(new Error(`Timeout of ${timeout} ms exceeded`));
};
request.send(transformData(data));
});}
- 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
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
2. 创建 Ajax
有了 xhr ,我们当然希望 Ajax 能够提供一些默认配置。这里的 Ajax 函数不做太过复杂的功能,但我们会简单模拟支持默认 config。
事实上,最后在 Ajax 中,内部调用的就是 xhr 函数。类似这个样子:
function Ajax(config) {
return xhr(config);}
2.1 提供默认 config
const defaultconf = {
method: "get",
timeout: 500,
headers: {
Accept: "application/json, text/plain, */*"
}};["get", "delete", "options", "head"].forEach(method => {
defaultconf.headers[method] = {};});// 为 headers 上添加一些方法的默认 headers, 暂时挂在 headers[method] 下["put", "post", "patch"].forEach(method => {
defaultconf.headers[method] = {
"Content-Type": "application/x-www-form-urlencoded"
};});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
这里我们提供了默认的配置,包括默认的 method、 timeout、 headers 等,其中,get、 delete、 options、 head 的 headers 默认为空;而 put、 post 和 patch 涉及到 data 传送的会给一个默认的配置: "Content-Type": "application/x-www-form-urlencoded"
。
2.2 合并配置
const method = config.method || defaultconf.method; const headers = Object.assign(
{},
defaultconf.headers,
defaultconf[method],
config.headers || {});const conf = Object.assign({}, defaultconf, config);conf.headers = headers; ["get", "delete", "options", "head", "put", "post", "patch"].forEach(key => {
delete conf.headers[key];});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
如上所示,我们会通过方法名获取方法名对应的默认的 headers,并与传入配置 headers 和默认 headers 进行合并。然后我们会合并配置。最后我们不要忘了把合并后的配置中,headers 中方法名对应的配置块删除。
2.3 Ajax 函数
function Ajax(config) {
const method = config.method || defaultconf.method;
const headers = Object.assign(
{},
defaultconf.headers,
defaultconf[method],
config.headers || {}
);
const conf = Object.assign({}, defaultconf, config);
conf.headers = headers;
["get", "delete", "options", "head", "put", "post", "patch"].forEach(key => {
delete conf.headers[key];
});
return xhr(conf);}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
3.简单的示例
3.1 请求的代码块
Ajax({
method: 'post',
url: '/simple/post',
data: {
a:1,
b:2
}}).then(data => {
console.log(data)}).catch(e => {
console.log('/simple/post', e)})Ajax({
method: 'post',
url: '/test/post',
data: {
a:1,
b:2
}}).then(data => {
console.log(data)}).catch(e => {
console.log('/test/post', e)})Ajax({
url: '/simple/get',
params: {
c:1,
d:2
}}).then(data => {
console.log(data)}).catch(e => {
console.log('/simple/get', e)})
- 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
3.2 请求结果
请求正确接口的 Ajax 请求都得到了正确的返回。而访问服务端暂时没有的接口则返回了 404 错误。同时,GET 请求中没有显式提供 method,默认配置也能够及时生效,默认为 GET。
4.小结
本章节到此为止,关于 Ajax 的封装,核心技术使用的依然是 XMLHttpRequest 技术。在自定义 Ajax 中,我们可以提供多种属性和方法来丰富和强壮我们的方法,比方说,我们可以提供 默认配置、Promise 语法支持、错误检测及处理、参数标准化 等等。
本章节的 Ajax 依然是不完美的,有兴趣的同学可以思考一下还能怎样去封装。至少我们还可以提供 request 和 response 的拦截和处理,我们也可以优化 config 合并策略。希望这能够发动同学们的脑洞风暴!