数据流请求/递归分页请求

213 阅读4分钟

前言

我们在开发过程中,时时都能遇到大量数据请求的情况;例如一次性请求100个接口,在这100次接口中,需要考虑并发,渲染,报错等情况,下文主要讲的是针对这一情况的个人的见解

数据流式请求

场景:数据流式请求,是针对于服务器响应很快,但是文件下载很大。例如像音频/视频类的边消耗边请求这类的。一般用于文件类的请求(二进制), 或者chatGPT文案展示(application/json);当然也可以用于数据请求

专业名称:后端SSE(Server-Sent Events) 服务端发送事件,前端发起一个请求链接,客户端与服务器建立数据传输通道,服务器定时传输数据给客户端

当用户就是需要很多dom节点的数据渲染, 前端解决方案有

  • 虚拟列表
  • requestIdleCallback分时函数
  • indexDB存储

现在讲解的数据流式请求,前后端解决方案

效果为:

4.gif

前端请求核心实现思路: 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+三连即可实现

递归分页循环

数据流式请求并不是我们的重点,这个才是我们的重点

场景:用于表格请求大数据的实现,

分页请求,一般应用于后台服务查询数据库,当数据库字段量大,查询数据众多,牵涉表数众多,这不可避免地导致分页请求缓慢。此种情景主要是在于服务器响应缓慢,而用户需要大量数据的情况下,前端做了一种折中处理

递归请求的本质上,是把批量任务拆分成众多的小任务,每个任务都可以异步进行,每个任务都相互独立,互不影响

效果为:

4.gif

核心实现

    /**
     * @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/…

参考文档