Fetch封装
javascript
/*
http([config]);
+ url 请求地址
+ method
+ credentials 携带资源凭证 *include, same-origin, omit
+ headers: null 自定义请求头信息 必须是纯粹对象
+ body: null 请求主体信息 只针对与POST系列请求 根据当前服务器要求 如果用户传递的是一个纯粹对象 需要转为urlencoded格式字符串
+ params: null 设定问号传参信息 格式必须是纯粹对象 在内部我们将其拼接在url的末尾
+ reponseType 预设服务器返回结果的读取方式 *json/text/arrayBuffer/blob
+ signal 中断请求的信号
----------
http.get/head/delete/options([url], [config]) 预先指定了预置项中的url/method
http.post/put/patch([url], [body], [config]) 预先指定了预置项中的url/method/body
----------
e.g.
http.get('/api/xxx', {...});
http({
method: 'GET',
url: '/api/xxx',
...
});
http.post('/api/xxx', {}, {...});
http({
method: 'POST',
url: '/api/xxx',
body: {},
...
});
*/
import { getToken } from '@/utils/auth';
import qs from 'qs';
const baseURL = '/api';
const pendingRequests = {};
// 防抖API白名单
const ABORT_WHITE_LIST = [];
function isObject(o) {
return Object.prototype.toString.call(o) === '[object Object]';
}
// 判断对象是不是一个纯对象
function isPlainObject(o) {
var ctor, prot;
if (isObject(o) === false) { return false; }
ctor = o.constructor;
if (ctor === undefined) { return true; }
prot = ctor.prototype;
if (isObject(prot) === false) { return false; }
if (prot.hasOwnProperty('isPrototypeOf') === false) {
return false;
}
return true;
}
const http = function http(config) {
// 规则校验
if (!config.url) {
throw new TypeError('url is required!');
}
let controller = null
// 防抖处理
if (!ABORT_WHITE_LIST.includes(config.url)) {
const previousController = pendingRequests[config.url];
// 存在相同URL则中止请求
if (previousController) {
previousController.abort();
}
if (!isPlainObject(config)) {
config = {};
}
controller = new AbortController();
pendingRequests[config.url] = controller
}
// 设定默认值
config = Object.assign({
url: '',
method: 'GET',
credentials: 'include',
reponseType: 'json',
headers: null,
body: null,
params: null,
signal: controller ? controller.signal : null
}, config);
if (!isPlainObject(config.headers)) {
config.headers = {};
}
if (config.params !== null && !isPlainObject(config.params)) {
config.params = null;
}
// 开始处理
let { url, method, credentials, headers, body, params, signal, reponseType } = config;
// 处理问号传参
url = baseURL + url;
if (params) {
url += `${url.indexOf('?') === -1 ? '?' : '&'}${qs.stringify(params)}`; // xxx=xxx&xxx=xxx
}
// 处理请求主体
method = method.toUpperCase();
params = qs.stringify(params);
if (method === 'GET') {
headers['Content-Type'] = 'application/x-www-form-urlencoded'
} else if (['HEAD', 'DELETE', 'OPTIONS'].includes(method) === 'POST') {
// TODO
} else {
body = JSON.stringify(body); // 已做 encodeURIComponent 处理
headers['Content-Type'] = 'application/json;charset=UTF-8';
}
// 统一处理,类似axios的请求拦截
let token = getToken();
if (token) {
headers['Authorization'] = token;
}
// 整理配置项准备发送fetch请求
config = {
method,
credentials,
headers,
signal,
cache: 'no-cache'
}
if (/^(POST|PUT|PATCH)$/.test(method) && body) {
config.body = body;
}
return fetch(url, config)
.then(response => { // 响应拦截
let { status, statusText } = response;
if (/^(2|3)\d{2}$/.test(status)) { // 请求成功
let res;
// *json/text/arrayBuffer/blob
switch (reponseType) { // 这些函数执行也可能因为流转换失败,返回失败promise实例
case 'text':
res = response.text();
break;
case 'arrayBuffer':
res = response.arrayBuffer();
break;
case 'blob':
res = response.blob();
break;
default:
res = response.json();
}
return res;
}
// 请求失败 HTTP状态码失败
return Promise.reject({
code: -100,
status,
statusText
});
})
.catch(reason => { // 失败的统一提示
if (reason && typeof reason === 'object') {
let { code, status } = reason;
if (code === -100) { // 状态码出错
switch (+status) {
case 400:
console.error('请求参数出现问题!');
break;
case 401: // 未登录 token过期 无权限
// 未登录则跳转登录页面,并携带当前页面的路径
// 在登录成功后返回当前页面,这一步需要在登录页操作。
console.error('未授权,请重新登录!');
localStorage.removeItem('token');
break;
case 403:
console.error('服务器拒绝访问!');
break;
case 404:
console.error('网络请求不存在!');
break;
default:
console.error(`出错了!错误原因是 ${reason.status}: ${reason.statusText}`);
}
} else if (code === 20) { // 请求被中断
console.error('请求被中断了~');
} else {
console.error('当前网络繁忙,请您稍后再试试吧~');
}
} else {
console.error('当前网络繁忙,请您稍后再试试吧~');
}
// http.get('...').catch(() => {...})
return Promise.reject(reason);
})
.finally(() => {
delete pendingRequests[config.url]
})
};
// get系列的快捷方法
['GET', 'HEAD', 'DELETE', 'OPTIONS'].forEach(method => {
http[method.toLowerCase()] = function (url, config) {
if (!isPlainObject(config)) {
config = {};
}
config['url'] = url;
config['method'] = method;
return http(config);
}
});
// post系列的快捷方法
['POST', 'PUT', 'PATCH'].forEach(method => {
http[method.toLowerCase()] = function (url, body, config) {
if (!isPlainObject(config)) {
config = {};
}
config['url'] = url;
config['body'] = body;
config['method'] = method;
return http(config);
}
});
export default http;