前言:
我目前是一名在校大学生,好巧不巧,学校为了降低成本选择自主开发考试系统,并对我所在的实验室委以重任,主播也是扛下了这杆大旗,所以准备开一个专栏好好讲讲如何被学校当牛马使唤。
正文
考试系统中,最为重要便是考生的答案提交保存,万一答案没能正确提交,挨骂的可不止是学校...那应该如何选择提交答案的时机呢?选择题选完就提交?填空题填完就提交?嘿嘿那先炸的肯定是学校的服务器...因为该考生被分为三部分:听力、阅读、写作,而后端接口返回的是一份完整的试卷,也就是说,在阅读和写作的时候并没有新的请求,主播机智的想到了触发提交的时机便是在跳转到下一部分试卷的时候。那应该怎么处理并发问题呢,一次 处理五个?还是十个?emm...好问题,主播立刻想到了自己考试的时候,一场考试足足把屁股坐得生疼,那么长的时间不用白不用,我们直接就一次处理一个,做一个请求的队列就好了呀。
代码如下:
ts
//data: 考生答案
//submitAnswer: 将答案提交到服务端的方法
export function requestConcurrency(data: StudentAnswer[]) {
return new Promise((resolve, reject) => {
//初始化请求队列
const waitQueue = data.map((item) => () =>
submitAnswer(item)
.then(res => {
resolve(res);
})
.catch(error => {
reject(error);
})
.finally(() => {
//请求完成后从队列中获取下一个请求
const next = waitQueue.shift();
if(next) {
next();
}
})
);
// 开始第一个请求
waitQueue.shift()?.();
});
}
好了看完代码,相信就有黑子要说了:如果中途有一个请求失败,不就使整个队列都 reject 了吗?这种事情都给你想到了,那我们只能将请求独立封装出来了
ts
export function requestConcurrency(data: StudentAnswer[]) {
return new Promise((resolve, reject) => {
let queue = [...data];
const results: any[] = [];
const processNext = () => {
if (queue.length === 0) {
return resolve(results);
}
const currentItem = queue.shift()!;
submitAnswer(currentItem)
.then(res => {
results.push(res);
processNext();
})
.catch(error => {
reject(new Error(`提交失败: ${error.message}`));
});
};
// 开始处理队列
processNext();
});
}
效果如图:

这时候就有黑子又要问了,那断网了怎么办,考生不就炸了吗...别急,主播怎么会让考生领着鸭蛋回家呢 早早就想到了这点,那就将答案在 localstorage 缓存,等网络恢复了再发送数据并清理缓存。
ts
function cacheFailedRequests(requests: StudentAnswer) {
const cached = JSON.parse(localStorage.getItem('cachedAnswers') || '[]');
cached.push({ data: requests, timestamp: Date.now() });
localStorage.setItem('cachedAnswers', JSON.stringify(cached));
}
function loadCachedRequests(): StudentAnswer[] {
const cached = JSON.parse(localStorage.getItem('cachedAnswers') || '[]');
return cached.map((entry: any) => entry.data);
}
//提交答案改为
submitAnswer(currentItem)
.then(res => {
results.push(res);
processNext();
})
.catch(error => {
if (error.isNetworkError) {
cacheFailedRequests([currentItem]);
reject(new Error('网络错误,请求已缓存'));
} else {
reject(new Error(`提交失败: ${error.message}`));
}
}
});
}
// 网络恢复监听(全局初始化)
if (typeof window !== 'undefined') {
window.addEventListener('online', () => {
const cachedData = loadCachedRequests();
if (cachedData.length > 0) {
requestConcurrency(cachedData)
.then(() => console.log('离线请求重试成功'))
.catch(() => console.warn('部分离线请求重试失败'));
}
});
}
这时候黑子又要给我们上强度了:如果网络一会掉线一会在线那不就造成频繁的请求了吗?好了不类比我们学校的校园网了...,我们可以在请求前检查网络环境,只要不对劲,就不继续请求了,并采用指数退避策略,当网络波动时智能重试.
最终方案如下:
ts
//data: 考生答案
//submitAnswer: 将答案提交到服务端的方法
function cacheFailedRequests(requests: StudentAnswer[]) {
const cached = JSON.parse(localStorage.getItem('cachedAnswers') || '[]');
cached.push(...requests.map((item) => ({ data: item, timestamp: Date.now() })));
localStorage.setItem('cachedAnswers', JSON.stringify(cached));
}
function loadCachedRequests(): StudentAnswer[] {
const cached = JSON.parse(localStorage.getItem('cachedAnswers') || '[]');
return cached.map((entry: any) => entry.data);
}
function retryWithBackoff(fn: Function, retries = 3): Promise<any> {
return new Promise((resolve, reject) => {
const attempt = (retryCount: number) => {
fn()
.then(resolve)
.catch((error: Error) => {
if (retryCount > 0) {
setTimeout(() => attempt(retryCount - 1), 1000 * 2 ** (3 - retryCount)); // 指数退避
} else {
reject(error);
}
});
};
attempt(retries);
});
}
export function requestConcurrency(data: StudentAnswer[]) {
return new Promise((resolve, reject) => {
// 初始网络检查
if (!navigator.onLine) {
cacheFailedRequests(data);
return reject(new Error('网络不可用,请求已缓存'));
}
let queue = [...data];
const results: any[] = [];
const processNext = () => {
if (queue.length === 0) {
localStorage.removeItem('cachedAnswers');
return resolve(results);
}
if (!navigator.onLine) {
cacheFailedRequests(queue);
return reject(new Error('网络中断,未完成请求已缓存'));
}
const currentItem = queue.shift()!;
retryWithBackoff(() => submitAnswer(currentItem), 3) // 使用重试机制
.then((res) => {
results.push(res);
processNext();
})
.catch((error) => {
if (error.isNetworkError) {
cacheFailedRequests([currentItem, ...queue]);
reject(new Error('网络错误,请求已缓存'));
} else {
reject(new Error(`提交失败: ${error.message}`));
}
});
};
// 开始处理队列
processNext();
});
}
// 网络恢复监听(全局初始化)
if (typeof window !== 'undefined') {
window.addEventListener('online', () => {
const cachedData = loadCachedRequests();
if (cachedData.length > 0) {
requestConcurrency(cachedData)
.then(() => console.log('离线请求重试成功'))
.catch(() => console.warn('部分离线请求重试失败'));
}
});
}
效果如图:

总结
目前的方案可能稍有缺陷,比如localstorage的内存限制,虽然我测了40个答案的缓存大小就 5142 bytes,所以没有采用 indexDB 做离线缓存,可能后续会考虑将考生的答案进行一个数据加密,不过目前看来应该不是很有必要,谁会看你选的啥答案啊,后续如果添加的话,会写进来的。
这是考试系统专栏的第一篇,下一篇我们讲讲另一个需求:将题型多样化(只有选填 ---> 打勾题、拖拽题多种题型支持),这需求可是让主播苦思冥想了些许时间。