Ajax 封裝
前言
學(xué)會(huì)了 Ajax 的請(qǐng)求以及如何處理服務(wù)端的響應(yīng)。這一章節(jié),我們著重來(lái)封裝一個(gè)簡(jiǎn)單的 Ajax。
前置知識(shí):
- 本章節(jié)會(huì)使用部分 ES6 語(yǔ)法
- 本章節(jié)使用 Promise
簡(jiǎn)單需求:
- 支持 Promise 語(yǔ)法處理結(jié)果
- 支持自定義配置,包括 headers
- 內(nèi)置 url、params、 data、headers 處理
1. 構(gòu)造一個(gè)這樣的 xhr
function xhr(config) {
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
/**
* 調(diào)用 open 方法
*/
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)
});
}
首先, 我們的 xhr 函數(shù)支持 config 傳入, 內(nèi)部通過(guò) XMLHttpRequest 技術(shù)來(lái)進(jìn)行請(qǐng)求的收發(fā), 大致就是上面這樣結(jié)構(gòu)的代碼,內(nèi)部的實(shí)現(xiàn)我們前面章節(jié)都講過(guò),唯一不同的是,在 onreadystatechange 上,我們掛載的方法最后使用 resolve()
來(lái)進(jìn)行斷言,這樣做的目的是,后續(xù)可以通過(guò) .then()
的方式進(jìn)行數(shù)據(jù)操作。
1.1 method 標(biāo)準(zhǔn)化
首先, 用戶傳進(jìn)來(lái)的 method 可能是大寫也可能是小寫,我們可以先做一個(gè)標(biāo)準(zhǔn)化,對(duì) method 做一個(gè)轉(zhuǎn)化,將其變?yōu)榇髮懀?/p>
method.toUpperCase()
1.2 構(gòu)建 url
有些同學(xué)很奇怪,為什么說(shuō)構(gòu)建 url,我們不是通過(guò) config 傳入 url 嗎?
是的,但是同學(xué)你別忘了,我們支持 params!
因此,我們需要把 params 上的參數(shù)進(jìn)行一定格式序列化拼接到 url 后面 ,構(gòu)成 "url?a=xxx&b=xxx"
的格式。為此,我們需要提供了一個(gè) buildUrl 的函數(shù):
/**
* 構(gòu)建 url
* @param {*} url
* @param {*} params
*/
function buildUrl(url, params) {
if (!params || !isPlainObject(params)) return url; // 如果 params 沒(méi)有傳或者不是一個(gè)純對(duì)象,直接返回原 url
let values = [];
Object.keys(params).forEach(key => {
// 對(duì) params 中的每一項(xiàng)進(jìn)行處理
const val = params[key];
if (typeof val === undefined || val === null) {
// 如果當(dāng)前項(xiàng)的值為 undefined 或者 null,則忽略
return;
}
values.push(`${key}=${val}`); // 將 “key=value”的形式加入到 values 數(shù)組中
});
let serializedParams = values.join("&"); // 序列化,將 values 數(shù)組轉(zhuǎn)化為字符串,格式為 "key=value&key=value"
if (serializedParams) {
// 如果有值,則加入到url后面。構(gòu)成 "url?key=value&key=value" 的形式
url += (url.indexOf("?") === -1 ? "?" : "&") + serializedParams;
}
return url;
}
在這個(gè)函數(shù)中,我們可以傳參 url 和 params。如果傳入params 為假值,那我們直接忽略,返回 url 即可。否則,我們需要對(duì) params 中 的每一項(xiàng)目進(jìn)行序列化,變?yōu)?"key=vaue"
這樣的形式, 添加到 values 數(shù)組中。接著我們通過(guò)數(shù)組的 .join("&")
的方法,把 values 數(shù)組通過(guò) “&” 進(jìn)行拼接。最后拼接到 url 后面,構(gòu)成 "url?key=value&key=value"
的形式返回。
這里,我們也涉及到了一個(gè)工具函數(shù) isPlainObject,在本章節(jié)中好幾處都會(huì)用到,他的作用是判斷該對(duì)象是不是一個(gè)純 “{}” 的對(duì)象,它的實(shí)現(xiàn)如下:
const toString = Object.prototype.toString; // 由于 Object.prototype.toString 在判斷類型的時(shí)候非常好用,并且用到的次數(shù)經(jīng)常會(huì)比較多,我們通??梢赃@樣緩存起來(lái)
/**
* 判斷當(dāng)前 val 是否是一個(gè)純對(duì)象
* @param {*} val
*/
function isPlainObject(val) {
return toString.call(val) === "[object Object]";
}
1.3 標(biāo)準(zhǔn)化 data
因?yàn)?.send() 是無(wú)法支持 Json 格式數(shù)據(jù)的,所以我們需要對(duì) data 做一個(gè)序列化處理:
/**
* 處理 data,因?yàn)?send 無(wú)法直接接受 json 格式數(shù)據(jù),這里我們可以直接序列化之后再傳給服務(wù)端
* @param {*} data
*/
function transformData (data) {
if (isPlainObject(data)) {
return JSON.stringify(data)
}
return data
}
實(shí)現(xiàn)非常簡(jiǎn)單,如果判斷 data 是一個(gè)純對(duì)象的話,就加一道 JSON.stringify(data)
的操作進(jìn)行序列化, 否則直接返回 data 本身。
1.4 設(shè)置 headers
對(duì)于 headers 的操作,我們會(huì)著重對(duì) Content-Type 進(jìn)行處理,在沒(méi)有 Content-Type 的時(shí)候,我們應(yīng)該有個(gè)默認(rèn)的支持。因?yàn)?headers 屬性上是大小寫不敏感的,因此我們會(huì)對(duì) Content-Type 做一個(gè)統(tǒng)一處理:
function transformHeaders (headers) {
const contentTypeKey = 'Content-Type' // Content-Type 的 key 值常量
if (isPlainObject(headers)) {
Object.keys(headers).forEach(key => {
if (key !== contentTypeKey && key.toUpperCase() === contentTypeKey.toLowerCase()) {
// 如果 key 的大寫和 contentTypeKey 的大寫一致,證明是同一個(gè),這時(shí)就可以用 contentTypeKey 來(lái)替代 key 了
headers[contentTypeKey] = headers[key]
delete headers[key]
}
})
if (!headers[contentTypeKey]) {
// 如果最后發(fā)現(xiàn)沒(méi)有 Content-Type,那我們就設(shè)置一個(gè)默認(rèn)的
headers[contentTypeKey] = 'application/json;charset=utf-8'
}
}
}
// 在 function xhr 中
// 設(shè)置頭部
transformHeaders(headers)
Object.keys(headers).forEach(key => {
if (!data && key === 'Content-Type') {
delete headers[key]
return
}
request.setRequestHeader(key, headers[key])
})
transformHeaders 函數(shù)對(duì) headers 進(jìn)行了一定程度的轉(zhuǎn)化,包括為 Content-Type 提供了默認(rèn)的支持,這里默認(rèn)為 "application/json;charset=utf-8"
。在 xhr 函數(shù)中,我們還會(huì)對(duì)headers的每一項(xiàng)進(jìn)行判斷,如果沒(méi)有 data ,那我們會(huì)刪除 Content-Type。同時(shí),我們會(huì)調(diào)用 setRequestHeader 方法將 headers 屬性添加到頭部。
1.5 設(shè)置響應(yīng)類型
if (responseType) {
// 如果設(shè)置了響應(yīng)類型,則為 request 設(shè)置 responseType
request.responseType = responseType;
}
1.6 設(shè)置超時(shí)時(shí)間
if (timeout) {
// 如果設(shè)置超時(shí)時(shí)間, 則為 request 設(shè)置 timeout
request.timeout = timeout;
}
1.7 處理結(jié)果
// 狀態(tài)變化處理函數(shù)
request.onreadystatechange = function handleLoad() {
if (request.readyState !== 4) return;
if (request.status === 0) return;
// 獲取響應(yīng)數(shù)據(jù)
const responseData =
request.responseType === "text"
? request.responseText
: request.response;
if (request.status >= 200 && request.status < 300 || request.status === 304) {
// 成功則 resolve 響應(yīng)數(shù)組
resolve(responseData);
} else {
// 失敗則 reject 錯(cuò)誤原因
reject(new Error(`Request failed with status code ${request.status}`));
}
};
// 錯(cuò)誤處理事件
request.onerror = function hadleError() {
//reject 錯(cuò)誤原因
reject(new Error('Network Error'))
}
// 超時(shí)處理事件
request.ontimeout = function handleTimeout() {
// reject 錯(cuò)誤原因
reject(new Error(`Timeout of ${timeout} ms exceeded`))
}
處理結(jié)果分為幾個(gè)部分:
- 正常處理服務(wù)端響應(yīng)
- 請(qǐng)求錯(cuò)誤
- 請(qǐng)求超時(shí)
其中,正常處理服務(wù)端響應(yīng)還要判斷狀態(tài)碼,這里判斷正確的是 200 至 300 之間狀態(tài)碼,再一個(gè)是 304 緩存。此時(shí)我們會(huì)通過(guò) resolve 斷言數(shù)據(jù)。否則,通過(guò) reject 來(lái)斷言失敗原因。
1.8 xhr 函數(shù)
至此,我們會(huì)得到這樣一個(gè) xhr 函數(shù):
function xhr(config) {
return new Promise((resolve, reject) => {
const {
url,
method = "get",
params = {},
data = null,
responseType,
headers,
timeout
} = config;
const request = new XMLHttpRequest();
/**
* 調(diào)用 open 方法
* method.toUpperCase() 的作用主要是講 method 都標(biāo)準(zhǔn)統(tǒng)一為大寫字母狀態(tài)。 比如 'get'.toUpperCase() 會(huì)返回 'GET'
*/
request.open(method.toUpperCase(), buildUrl(url, params));
if (responseType) {
// 如果設(shè)置了響應(yīng)類型,則為 request 設(shè)置 responseType
request.responseType = responseType;
}
if (timeout) {
// 如果設(shè)置超時(shí)時(shí)間, 則為 request 設(shè)置 timeout
request.timeout = timeout;
}
// 設(shè)置頭部
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));
});
}
2. 創(chuàng)建 Ajax
有了 xhr ,我們當(dāng)然希望 Ajax 能夠提供一些默認(rèn)配置。這里的 Ajax 函數(shù)不做太過(guò)復(fù)雜的功能,但我們會(huì)簡(jiǎn)單模擬支持默認(rèn) config。
事實(shí)上,最后在 Ajax 中,內(nèi)部調(diào)用的就是 xhr 函數(shù)。類似這個(gè)樣子:
function Ajax(config) {
// code ...
return xhr(config);
}
2.1 提供默認(rèn) config
首先,我們來(lái)定義默認(rèn)配置
// 默認(rèn)配置
const defaultconf = {
method: "get",
timeout: 500,
headers: {
Accept: "application/json, text/plain, */*"
}
};
// 為 headers 上添加一些方法的默認(rèn) headers, 暫時(shí)掛在 headers[method] 下
["get", "delete", "options", "head"].forEach(method => {
defaultconf.headers[method] = {};
});
// 為 headers 上添加一些方法的默認(rèn) headers, 暫時(shí)掛在 headers[method] 下
["put", "post", "patch"].forEach(method => {
defaultconf.headers[method] = {
"Content-Type": "application/x-www-form-urlencoded"
};
});
這里我們提供了默認(rèn)的配置,包括默認(rèn)的 method、 timeout、 headers 等,其中,get、 delete、 options、 head 的 headers 默認(rèn)為空;而 put、 post 和 patch 涉及到 data 傳送的會(huì)給一個(gè)默認(rèn)的配置: "Content-Type": "application/x-www-form-urlencoded"
。
2.2 合并配置
const method = config.method || defaultconf.method; // 請(qǐng)求的方法名
// 合并 headers
const headers = Object.assign(
{},
defaultconf.headers,
defaultconf[method],
config.headers || {}
);
// 合并默認(rèn)配置和自定義配置,這里簡(jiǎn)單的進(jìn)行后者對(duì)前者的覆蓋
const conf = Object.assign({}, defaultconf, config);
conf.headers = headers; // 配置的 headers 為我們上面合并好的 headers
// 刪除 conf 配置中,headers 下默認(rèn)的方法的headers塊
["get", "delete", "options", "head", "put", "post", "patch"].forEach(key => {
delete conf.headers[key];
});
如上所示,我們會(huì)通過(guò)方法名獲取方法名對(duì)應(yīng)的默認(rèn)的 headers,并與傳入配置 headers 和默認(rèn) headers 進(jìn)行合并。然后我們會(huì)合并配置。最后我們不要忘了把合并后的配置中,headers 中方法名對(duì)應(yīng)的配置塊刪除。
2.3 Ajax 函數(shù)
最后,我們會(huì)得到這樣一個(gè) 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);
}
3.簡(jiǎn)單的示例
3.1 請(qǐng)求的代碼塊
// 服務(wù)端現(xiàn)有接口,進(jìn)行 post 請(qǐng)求
Ajax({
method: 'post',
url: '/simple/post',
data: {
a:1,
b:2
}
}).then(data => {
console.log(data)
}).catch(e => {
console.log('/simple/post', e)
})
// 服務(wù)端暫時(shí)沒(méi)有的接口, 進(jìn)行 post 請(qǐng)求
Ajax({
method: 'post',
url: '/test/post',
data: {
a:1,
b:2
}
}).then(data => {
console.log(data)
}).catch(e => {
console.log('/test/post', e)
})
// 服務(wù)端現(xiàn)有接口, 進(jìn)行 get 請(qǐng)求
Ajax({
url: '/simple/get',
params: {
c:1,
d:2
}
}).then(data => {
console.log(data)
}).catch(e => {
console.log('/simple/get', e)
})
3.2 請(qǐng)求結(jié)果
如圖所示,請(qǐng)求正確接口的 Ajax 請(qǐng)求都得到了正確的返回。而訪問(wèn)服務(wù)端暫時(shí)沒(méi)有的接口則返回了 404 錯(cuò)誤。同時(shí),GET 請(qǐng)求中沒(méi)有顯式提供 method,默認(rèn)配置也能夠及時(shí)生效,默認(rèn)為 GET。
4.小結(jié)
本章節(jié)到此為止,關(guān)于 Ajax 的封裝,核心技術(shù)使用的依然是 XMLHttpRequest 技術(shù)。在自定義 Ajax 中,我們可以提供多種屬性和方法來(lái)豐富和強(qiáng)壯我們的方法,比方說(shuō),我們可以提供 默認(rèn)配置、Promise 語(yǔ)法支持、錯(cuò)誤檢測(cè)及處理、參數(shù)標(biāo)準(zhǔn)化 等等。
本章節(jié)的 Ajax 依然是不完美的,有興趣的同學(xué)可以思考一下還能怎樣去封裝。至少我們還可以提供 request 和 response 的攔截和處理,我們也可以優(yōu)化 config 合并策略。希望這能夠發(fā)動(dòng)同學(xué)們的腦洞風(fēng)暴!