前言
我们在开发过程中,时时都能遇到大量数据请求的情况;例如一次性请求100个接口,在这100次接口中,需要考虑并发,渲染,报错等情况,下文主要讲的是针对这一情况的个人的见解
数据流式请求
场景:数据流式请求,是针对于服务器响应很快,但是文件下载很大。例如像音频/视频类的边消耗边请求这类的。一般用于文件类的请求(二进制), 或者chatGPT文案展示(application/json);当然也可以用于数据请求
专业名称:后端SSE(Server-Sent Events) 服务端发送事件,前端发起一个请求链接,客户端与服务器建立数据传输通道,服务器定时传输数据给客户端
当用户就是需要很多dom节点的数据渲染, 前端解决方案有
- 虚拟列表
- requestIdleCallback分时函数
- indexDB存储
现在讲解的数据流式请求,前后端解决方案
效果为:
前端请求核心实现思路: 1、通过fetch函数请求,TextDecoder解码器解码resp.body.getReader()获取数据流 2、再通过while循环, 一直请求到数据流完成为止
1)idnex.html文件
<!--
* @Author: junsong Chen 779217162@qq.com
* @Date: 2023-08-31 17:42:03
* @LastEditTime: 2024-07-16 19:42:53
* @Description:
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h2>数据流式请求</h2>
<p>数据流式请求,是针对于服务器响应很快,但是文件下载很大。例如像音频/视频类的边消耗边请求这类的。一般用于文件类的请求, 或者chatGPT文案展示</p>
<p>当然也可以用于数据请求,当用户就是需要很多dom节点的数据渲染, 当然,视图之外的虚拟列表也是可以的,当然使用requestIdleCallback分时函数也可以,现在使用的是后端解决方案</p>
<div style="margin: 10px;">
<button onclick='getRequest("/order/list?type=1", "once")'>一次性请求</button>
<button onclick='getRequest("/order/step","segmentation")'>分段请求</button>
</div>
<div id="result"></div>
<script>
// 请求
async function getRequest(url, type = 'once') {
const baseUrl = `http://localhost:9000${url}`;
// 响应分为响应头和响应体,await 等待的是响应体,而不是响应头,响应体如果数据量很大,会导致用户等很久,所以采用
// 流式读取,用户读取一部分,请求一部分
const resp = await fetch(baseUrl, {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
content: "测试",
}),
});
// 对于类型化数组, unit8Array, 需要使用解码器来解码
const textDecoder = new TextDecoder();
// 数据流式读取, 创建一个读取器并将流锁定于其上。一旦流被锁定,其他读取器将不能读取它,直到它被释放。
const reader = resp.body.getReader();
// 后台数据
let result = "";
// 循环
while (1) {
const { done, value } = await reader.read();
if (done) {
break;
}
const str = textDecoder.decode(value);
console.log('%c获取得到数据=====>', 'color:#f00;')
console.log('获取得到数据=====>', str)
if (type === 'once') {
result += str;
} else {
renderDom(str)
}
}
console.log("result", result);
if (type === 'once') {
renderDom(result)
}
}
function renderDom(result) {
const dom = document.getElementById('result')
dom.innerHTML = result
}
</script>
</body>
</html>
后端接口核心实现思路: 1、通过读取order.json文件里的数据为接口需要返回的数据 2、再通过迭代器和等分数组分割请求批次 3、再通过定时器输出,模拟请求延迟
2)app.js
/*
* @Author: junsong Chen 779217162@qq.com
* @Date: 2023-08-31 16:59:45
* @LastEditTime: 2024-07-16 19:17:12
* @Description:
*
* 参考文档: https://www.codetd.com/article/15481030
*
*/
const http = require("http");
const url = require("url");
const PORT = 9000;
const orderJson = require("./order.json");
const getPostData = (req, callback) => {
// post传参获取字符串方式一
let postData = "";
// 通过流传递数据,stream
req.on("data", (chunk) => {
postData += chunk.toString();
});
// 监听传输结束
req.on("end", () => {
console.log("postData", postData);
callback(postData);
});
// post传参获取字符串方式二
// let buffers = [];
// req.on("data", (chunk) => {
// buffers.push(chunk);
// });
// req.on("end", function () {
// let data = Buffer.concat(buffers).toString();
// res.writeHead(200, { "Content-Type": "text/json;charset=utf-8" });
// res.end(data);
// callback(data);
// });
};
// 等分数组
function spliceArrByNum(arr, num) {
let newArr = [...arr]; // 因为splice会改变原数组,要深拷贝一下
let list = [];
for (let i = 0; i < newArr.length; i++) {
list.push(newArr.splice(i, num));
}
return list;
}
// 迭代器
function* walk(data) {
if (data.length && Array.isArray(data)) {
const newList = spliceArrByNum(data, 5);
for (let item of newList) {
yield JSON.stringify(item);
}
} else {
return [];
}
}
const server = http.createServer((req, res) => {
const { pathname, query } = url.parse(req.url, true);
console.log("query", query);
const method = req.method;
// post请求
if (/post|options/i.test(method)) {
// 可跨域
res.writeHead(200, {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "X-Token,Content-Type",
"Access-Control-Allow-Methods": "PUT",
"Content-Type": "text/html;charset=utf-8",
});
if (pathname === "/order/list") {
// 获取所有数据
getPostData(req, (bodyData) => {
console.log("获取请求头提交的post参数", typeof bodyData);
res.end(JSON.stringify(orderJson));
});
} else if (pathname === "/order/step") {
// 分步骤所有数据
if (orderJson.length) {
const iterator = walk(orderJson);
// 定时输出
let timerId;
timerId = setInterval(() => {
const { value, done } = iterator.next();
if (done) {
clearInterval(timerId);
}
console.log("/order/step发送片段=====>");
if (value) {
res.write(JSON.stringify(value));
} else {
res.end("请求终止");
}
}, 1000);
} else {
res.end("");
}
} else if (pathname === "/order/page") {
// 通过分页传参获取数据
const page = Number(query.page) || 1;
const pageSize = Number(query.pageSize) || 10;
console.log("page =====>", page);
console.log("pageSize =====>", pageSize);
const resultArr = spliceArrByNum(orderJson, pageSize);
setTimeout(() => {
res.end(
JSON.stringify({
code: 200,
data: resultArr[page - 1] || [],
message: "请求成功",
total: orderJson.length,
currentPage: Number(page),
currentPageSize: Number(pageSize),
})
);
}, 500);
} else {
res.end("Not Found");
}
}
// get请求
if (/get/i.test(method)) {
if (url === "/file") {
let postData = getPostData(req);
console.log("获取请求头提交的post参数", postData);
} else if (url === "/test") {
res.end("测试成功");
} else {
res.end("Not Found");
}
}
});
server.listen(PORT, () => {
console.log(`server run at http://localhost:${PORT}`);
});
// 监听服务器错误
server.on("error", (e) => {
console.log("监听服务器错误", e);
});
// 调用server.close()可以触发下方监听
server.on("close", () => {
console.log("服务关闭啦");
});
server.setTimeout(60 * 1000 * 1000, () => {
console.log("响应超时");
});
// 超时相应监听
server.on("timeout", function () {
console.log("连接已经超时");
});
效果看效果图,实现方式看代码,有对应的注释,ctrl+三连即可实现
递归分页循环
数据流式请求并不是我们的重点,这个才是我们的重点
场景:用于表格请求大数据的实现,
分页请求,一般应用于后台服务查询数据库,当数据库字段量大,查询数据众多,牵涉表数众多,这不可避免地导致分页请求缓慢。此种情景主要是在于服务器响应缓慢,而用户需要大量数据的情况下,前端做了一种折中处理
递归请求的本质上,是把批量任务拆分成众多的小任务,每个任务都可以异步进行,每个任务都相互独立,互不影响
效果为:
核心实现
/**
* @description: 异步方法请求,含最大并发数
* @param {array} rLists 请求列表
* @param {number} maxNum 最大并发数
* @return {*}
*
* 在递归调用请求中,这里只考虑针对统一接口请求
*
*/
function concurRequest(rLists, maxNum) {
return new Promise((resolve) => {
// 如果rLists长度为空
if (rLists.length === 0) {
resolve([]);
return;
}
let index = 0; // 下一个发送请求的下标
let count = 0; // 当前请求完成数量
async function request() {
// 如果当前请求下标超出rLists的长度则返回
if (index === rLists.length) {
return;
}
// 记录本次请求的下标
const i = index;
// 更新index
index++;
try {
// 当前请求的url
const currentRequest = rLists[i];
const resp = await fetchFun(i + 1);
currentRequest.response = resp;
console.log("currentRequest==>", currentRequest);
} catch (err) {
console.error("throw error", err);
} finally {
count++;
const cItem = rLists[count - 1]
const cResData = cItem.response?.data
const cPageSize = cItem.params.pageSize
if (count === rLists.length || (cResData?.length < cPageSize)) {
console.log("抛出结果", rLists);
const fResult = rLists.filter(ele=> ele.response)
console.log("过滤掉空数据,抛出最终结果", fResult);
resolve(fResult);
return
}else{
switchObj.isRecursive && request();
}
}
}
const times = Math.min(maxNum, rLists.length);
for (let i = 0; i < times; i++) {
request();
}
});
}
源码
github: github.com/ArcherNull/…