封装 fetch 与 Error,返回 Promise 对象
fetch 在目前已经是很成熟的请求资源的方法,但为了方便在项目中调用,一般都会进行二次封装
一、定义错误类型
对于封装公共组件或方法,一定要多想,七分设计,三分开发
而对于一个网络请求来说,除了处理请求体、响应体之外,还有一个常常被忽略的环节,那就是定义 Error
项目中关于网络请求的错误有很多种,比如超时错误、服务器错误、断网错误,这些都可以加以封装
// errors.js
export class ApiError extends Error {
constructor(message, url) {
super(message);
this.message = message;
this.name = 'ApiError';
this.url = url;
}
}
export class DisconnectError extends ApiError {
constructor(url) {
super('网络已断开,请重新连接', url);
this.name = 'DisconnectError';
}
}
export class ApiServerError extends ApiError {
constructor(statusCode, url) {
super(`请求服务器出错:${statusCode}`, url);
this.name = 'ApiServerError';
this.statusCode = statusCode;
}
}
export class ApiJsonError extends ApiError {
constructor(url) {
super('请求服务器出错:无法转换为JSON', url);
this.name = 'ApiJsonError';
}
}
export class ApiTimeoutError extends ApiError {
constructor(time, url) {
super('请求超时', url);
this.name = 'ApiTimeoutError';
this.time = time;
}
}
上面定义了几种常见的请求错误,其实还可以定义一种业务错误
比如某个请求的状态是 200,但不符合后端定义的业务逻辑,返回了特殊的 code
这时就可以根据后端返回的 code 进行业务错误的封装
二、抛出错误
在抛出上面定义的 Error 的时候,需要做一些判断,这部分逻辑可以抽出来
// 检查网络状态是否已连接
function checkonLine(url) {
return new Promise((resolve, reject) => {
if (!window.navigator.onLine) {
reject(new DisconnectError(url));
} else {
resolve(url);
}
});
}
// 校验状态码
function checkStatus(response) {
const status = Number(response.status);
if (status >= 200 && status < 300) {
return response;
}
throw new ApiServerError(status, response.url);
}
// 解析 fetch 的响应结果
function parseJSON(response) {
return new Promise((resolve, reject) => {
response
.json()
.then((json) => {
// 记录请求的地址
// eslint-disable-next-line
json._SERVER_URL = response.url;
resolve(json);
})
.catch((error) => {
if (error instanceof SyntaxError) {
reject(new ApiJsonError(response.url));
} else {
reject(error);
}
});
});
}
三、封装 fetch
准备就绪,可以上主菜了
const FETCH_TIMEOUT = 1000 * 30;
function request(path, params, options = {}) {
const { body, method = 'GET', ...other } = params || {};
const newMethod = `${method}`.toUpperCase();
const newParams = { method: newMethod, credentials: 'include', ...other };
const timeout = newParams.timeout || FETCH_TIMEOUT;
let url = path;
if (newMethod !== 'GET') {
if (!(body instanceof FormData)) {
newParams.headers = {
Accept: 'application/json',
'Content-Type': 'application/json; charset=utf-8',
...newParams.headers,
};
newParams.body = JSON.stringify(body);
}
} else {
// 对GET请求增加时间戳 以避免IE缓存
const timestamp = Date.now();
const queryURL = qs.stringify({ ...body, t: timestamp });
url = `${url}?${queryURL}`;
}
// 封装请求头
newParams.headers = {
...newParams.headers,
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
credentials: 'include',
};
return Promise.race([
// 校验网络连接
checkonLine(url)
.then(() => {
return fetch(url, newParams, options);
})
.then(checkStatus)
.then(parseJSON)
.catch((err) => {
throw err;
}),
new Promise((resolve, reject) => {
setTimeout(reject, timeout, new ApiTimeoutError(timeout, url));
}),
]);
}
请求函数 request 已经搞定,这时可以简单的粗暴的 export 这个函数
也可以导出具体的 get、post 方法
export default {
get: async (path, params, options) =>
request(path, { method: 'GET', body: params }, options),
post: async (path, params, options) =>
request(path, { method: 'POST', body: params }, options),
delete: async (path, params, options) =>
request(path, { method: 'DELETE', body: params }, options),
put: async (path, params, options) =>
request(path, { method: 'PUT', body: params }, options),
};
四、更进一步
上面的代码导出的是一个含有 get 等方法的对象,需要这么使用:
import http from './request'
http.get('/api/get', { name: 'wise' });
http.post('/api/save', { name: 'wise' });
不过对于 get 请求,很多的库做了进一步的封装,可以直接调用
// 直接调用,默认使用 get 请求
http('/api/get', { name: 'wise' });
为了更好的体验,我们的代码也可以更进一步:
const http = (url) => {
return http.get(url);
};
http.get = async (path, params, options) =>
request(path, { method: 'GET', body: params }, options);
http.post = async (path, params, options) =>
request(path, { method: 'POST', body: params }, options);
http.delete = async (path, params, options) =>
request(path, { method: 'DELETE', body: params }, options);
http.put = async (path, params, options) =>
request(path, { method: 'PUT', body: params }, options);
export default http;
搞定~