场景:数据的流式读取

367 阅读3分钟

在现代 web 应用中,用户体验至关重要。长时间的加载会导致用户流失,因此我们需要一种方法来优化数据加载。本文将探讨如何通过 fetch 方法实现后端分块返回数据的功能,模拟 AI 对话的逻辑,避免等待时间过长导致的加载缓慢。

一、背景介绍

在传统的请求-响应模型中,前端发送请求后,需要等待后端处理完成并返回所有数据。这种方式可能导致用户在等待过程中感到无聊,尤其是数据量大或处理时间长的情况下。为了解决这一问题,我们可以采用分块响应的方式,将数据分成多个小块逐步返回,前端可以即时处理和展示这些数据,提升用户体验。

二、后端实现

我们以 Node.js 的 Express 框架为例,创建一个简单的后端接口,模拟 AI 聊天。每次请求时,后端将数据分块返回。以下是后端代码示例:

const express = require('express');
const app = express();
const PORT = 3000;

// 中间件:解析 JSON 请求体
app.use(express.json());

// 模拟的聊天响应内容
const chatResponses = `这是一个示例的长文本消息。可以包含多行文本,可以包含各种信息,比如产品描述、用户指南等.`
;

// 创建 POST 路由 /chat
app.post('/chat', (req, res) => {
    const userMessage = req.body.message;
    console.log(`Received message: ${userMessage}`);

    let index = 0;

    // 定时器模拟逐块发送响应
    const intervalId = setInterval(() => {
        if (index < chatResponses.length) {
            res.write(chatResponses[index]); // 逐块发送响应
            index++;
        } else {
            clearInterval(intervalId); // 清除定时器
            res.end(); // 结束响应
        }
    }, 500); // 每500毫秒发送一个小块
});

// 启动服务器
app.listen(PORT, () => {
    console.log(`服务器正运行在 http://localhost:${PORT}`);
});

三、前端实现

前端使用 fetch 方法发送请求并处理逐块响应。我们可以通过 ReadableStream 读取响应流,从而实现分块处理。以下是前端代码示例:

<template>
  <div class="Home">
    <p> {{ result }}</p>
    <input type="text" v-model="content" placeholder="请输入内容">
    <button @click="send">发送</button>
  </div>
</template>

<script>
export default {
    name: 'Home',
    data(){
        return {
          content: ``,
          result:'',
        }
    },
    methods: {
      async getAITalk(content){
        let url = `http://localhost:8080/chat`
        let res = await window.fetch(url,{
          method: 'post',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({content, role: 'user'})
        })
        // console.log('res',res)
        const reader = res.body.getReader();
        const flag = true
        while(flag){
          const textDecoder = new TextDecoder();
          const { done, value } = await reader.read();
            if (done) {
              break;
            }
          this.result += textDecoder.decode(value, { stream: true })
        }
      },
      send(){
        this.getAITalk(this.content)
      }
    },
}
</script>

<style lang="less" scoped>

</style>
/**
 * @description apiMap 服务映射
 */
// apiMap 服务映射
const apiMap = new Map([
  ['aaa-test.com', { http: 'http://aaa-api.com' }], // only HTTP
  ['localhost', { http: 'http://aaa-api.com', https: 'https://aaa-api.com' }], // localhost test, both HTTP & HTTPS
])

// 提取当前页面的协议和主机名
const currentProtocol = window.location.protocol // 获取当前协议,如 "http:" 或 "https:"
const currentHost = new URL(window.location.origin).hostname // 获取当前主机名

let apiUrl = '' // 初始化 API URL

// 根据主机名获取对应的 API 域名映射
// console.log('apiMapping', apiMapping)
const apiMapping = apiMap.get(currentHost)
if (apiMapping) {
  // 优先使用当前页面的协议构建 API URL,如果当前协议不匹配则回退到 http 协议
  if (apiMapping[currentProtocol.replace(':', '')]) {
    apiUrl = apiMapping[currentProtocol.replace(':', '')]
  } else if (apiMapping['http']) {
    apiUrl = apiMapping['http']
  }
} else {
  console.error(`No API mapping found for hostname: ${currentHost}`)
}

export default apiUrl
// vue.config.js
module.exports = {
    devServer: {
        // 设置代理规则
        proxy: {
            '/chat': {
                target: 'http://localhost:3000', // 目标服务器地址
                changeOrigin: true, // 是否更改请求头中的 Origin
                // pathRewrite: {
                //     '^/chat': '' // 重写路径:移除路径中的 `/api`
                // }
            }
        }
    }
};

四、实现原理

  1. 后端分块发送:后端使用定时器,每隔一段时间(例如 500毫 秒)发送一小块响应数据。这样,前端无需等待所有数据完成加载,即可开始显示内容。
  2. 前端读取流:前端使用 fetch 获取响应,通过 ReadableStream 逐块读取数据。TextDecoder 用于将接收到的 Uint8Array 转换为字符串,便于处理。

五、总结

通过使用 fetch 方法和后端的分块响应,我们可以有效地减少用户的等待时间,提升应用的响应速度。这种方式特别适合于需要处理大量数据的场景,如 AI 对话、实时数据更新等。当然,还有更好的方案进行流式读取,使用第三方库@microsoft/fetch-event-source具体可参考该库的文档,希望本文能够为你提供有关数据分块处理的思路,并在实际项目中有所帮助。

参考资料