近期开发任务严峻,在疯狂写代码、改 BUG的同时还得抽出时间跟后端对接,并且后端的接口给的字段很难对的上,于是就有了这样的场景:
我: 这个页面一共包含name,id,time,address, data五个字段,你们谁提供接口啊!
小明: 我提供一个接口,给你返回id, name, time字段。
小红:我也提供一个接口,我返给你address字段。
小张:我也是,我给你data字段。
我:你们是在为难我胖虎嘛???就五个字段我得请求三个接口,你们不能聚合一下嘛!
三位异口同声:我们现在是微服务只能提供这么多,前端自己聚合吧
我:卒···
上述例子在开发中经常遇到,解决倒是很简单,但如何保证页面的完整性是一个问题。
说这个接口请求的时候我先提出一个问题,就是你们发现没有我们在请求接口的时候有时候一个接口会请求两次,其实第一次发送的就是preflight request(预检请求),那么为什么要发预检请求,什么时候会发预检请求,预检请求都做了什么? 我们先搞清一下这个问题:
一. 为什么要发预检请求
我们都知道浏览器的同源策略,就是出于安全考虑,浏览器会限制从脚本发起的跨域HTTP请求,像XMLHttpRequest和Fetch都遵循同源策略。
浏览器限制跨域请求一般有两种方式:
- 浏览器限制发起跨域请求
- 跨域请求可以正常发起,但是返回的结果被浏览器拦截了
一般浏览器都是第二种方式限制跨域请求,那就是说请求已到达服务器,并有可能对数据库里的数据进行了操作,但是返回的结果被浏览器拦截了,那么我们就获取不到返回结果,这是一次失败的请求,但是可能对数据库里的数据产生了影响。
为了防止这种情况的发生,规范要求,对这种可能对服务器数据产生副作用的HTTP请求方法,浏览器必须先使用OPTIONS方法发起一个预检请求,从而获知服务器是否允许该跨域请求:如果允许,就发送带数据的真实请求;如果不允许,则阻止发送带数据的真实请求。
二. 什么时候发预检请求
HTTP请求包括: 简单请求 和 需预检的请求
1. 简单请求
简单请求不会触发CORS预检请求,“简属于
单请求”术语并不属于Fetch(其中定义了CORS)规范。
若满足所有下述条件,则该请求可视为“简单请求”:
- 使用下列方法之一:
- GET
- HEAD
- POST
- Content-Type: (仅当POST方法的Content-Type值等于下列之一才算做简单需求)
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
2.需预检的请求
“需预检的请求”要求必须首先使用OPTIONS方法发起一个预检请求到服务区,以获知服务器是否允许该实际请求。“预检请求”的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
当请求满足下述任一条件时,即应首先发送预检请求:
- 使用了下面任一 HTTP 方法:
- PUT
- DELETE
- CONNECT
- OPTIONS
- TRACE
- PATCH
- 人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。该集合为:
- Accept
- Accept-Language
- Content-Language
- Content-Type
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
- Content-Type的值不属于下列之一:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
先解决一下这个问题,这个问题当时让我迷惑了许久,接下来咱们再说和后端对接口的问题。
我们需要请求三个接口,分别是接口a、接口b和接口c。请求过程中由于服务不稳定导致a成功了但b和c失败了。这种情况我们要么直接报错误信息,有点不友好;要么只使用a返回的数据,一般这种情况前端都会有默认值,页面自然而然展示出了默认信息,用户就会用错误的数据做出错误的决策,这是很严重的错误。为了解决上述问题,本文实现了一个容错机制,去尝试解决这个问题。
重试方案
首先重试机制意味着我们需要多次请求失败接口,先解决这个问题,大概两张方案:
- socket
- 轮询
但是我们没有后端支持,socket直接干掉。那么只能通过轮询去尝试。先贴出轮询代码
export interface IParams {
maxCount?: number; // 最大轮询次数
intervalTime?: number; // 每次轮询增加时间长度
maxInterval?: number; // 最大轮询时间长度
}
export interface IProcessPayload<T> {
data: T;
count: number;
resolve?: (data: T) => void;
reject?: (err: any) => void;
}
/**
*
* error 失败
* process 继续轮询
* finish 结束轮询
*/
export type IProgressType = 'error' | 'process' | 'finish';
const defaultConfig = {
maxCount: 120,
intervalTime: 1000,
maxInterval: 1600,
};
export class PollingFun {
timeoutTimer: any;
cancelWhile: any;
constructor(private config: IParams = { maxCount: 120, intervalTime: 1000, maxInterval: 1600 }) {
this.config = { ...defaultConfig, ...config };
}
cancel() {
if (this.cancelWhile) {
this.cancelWhile();
this.cancelWhile = null;
}
if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer);
}
}
pollingSingleTask = async <T>(onProgress: (data: IProcessPayload<T>) => IProgressType, ajaxFun: () => Promise<T>) => {
const { maxCount, intervalTime, maxInterval } = this.config;
let pollingCount = 0;
let stopPolling = false;
this.cancel();
this.cancelWhile = () => (stopPolling = true);
while (!stopPolling && pollingCount < maxCount) {
// 刚开始密集,后续间隔加长,最长1s。
let realIntervalTime = Math.floor(pollingCount / 10) * 200 + intervalTime; // eslint-disable-line
realIntervalTime = Math.min(realIntervalTime, maxInterval);
try {
const resData = await ajaxFun();
if (stopPolling) {
return Promise.reject('cancel');
}
const progressRes = onProgress({ data: resData, count: pollingCount });
switch (progressRes) {
case 'finish':
stopPolling = true;
return Promise.resolve(resData);
case 'error':
stopPolling = true;
return Promise.reject(resData);
default:
await new Promise(resolve => {
this.timeoutTimer = setTimeout(resolve, realIntervalTime);
});
break;
}
} catch (error) {
stopPolling = true;
return Promise.reject(error);
}
pollingCount += 1;
}
if (pollingCount >= maxCount) {
return Promise.reject('overMaxCount');
}
};
}
可以看到我们实现了一个轮询类,使用方式也很简单,只需要每次new一个实例,然后调用对应的方法即可。轮询方法需要两个参数,一个是轮询处理函数,其接受一个参数,会携带本次轮询的数据,我们只需要对数据做判断,然后返回相应的数据处理轮询。
重试机制
由上述背景我们可以知道,请求成功意味着所有请求都返回了结果,脑袋一转,想到了Promise.all,瞬间解决了一半的问题。我们只需要把每个请求函数包裹成轮询的方式,然后等着拿值就行,上手开干!
type AjaxFun<T> = [() => Promise<T>, (data: IProcessPayload<T>) => IProgressType, IParams];
const createPromise = <T>(ajaxFunArr: AjaxFun<T>[]) => {
return ajaxFunArr.map(item => {
const [ajaxFn, onProcess, options] = item;
const pollInstance = new PollingFun(options);
return new Promise((resolve, reject) => {
pollInstance.cancel();
pollInstance
.pollingSingleTask(payload => onProcess({ ...payload, resolve, reject }), ajaxFn)
.catch(err => reject(err));
});
});
};
export const ajaxCatch = async <T>(ajaxFunArr: AjaxFun<T>[]) => {
const wrapAjaxFunArr = await createPromise(ajaxFunArr);
return Promise.all([...wrapAjaxFunArr])
.then(res => ({
status: true,
data: res,
}))
.catch(err => ({
status: false,
data: err,
}));
};
可以看到,我们封装了一个ajax处理函数,这个函数需要一个数组类型参数,每个数组子值需要提供有三个值,分别是当前请求函数、控制轮询状态的函数以及轮询初始化的值。看着还不错 试试效果
const wake = async val => {
console.log(val);
return await val;
};
const onProcess = pay => {
const { data, resolve, count } = pay;
if (data === 'q2' && count === 3) {
resolve(data);
return 'finish';
}
if (data === 'q2') {
return 'process';
}
return 'finish';
};
export const getData = async () => {
const res = await ajaxCatch([
[() => wake('q1'), onProcess, { maxCount: 5 }],
[() => wake('q2'), onProcess, { maxCount: 5 }],
[() => wake('q3'), onProcess, { maxCount: 5 }],
]);
console.log(res, 'cdc');
};
完美达到我们需要的效果!