在现代 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`
// }
}
}
}
};
四、实现原理
- 后端分块发送:后端使用定时器,每隔一段时间(例如 500毫 秒)发送一小块响应数据。这样,前端无需等待所有数据完成加载,即可开始显示内容。
- 前端读取流:前端使用
fetch获取响应,通过ReadableStream逐块读取数据。TextDecoder用于将接收到的 Uint8Array 转换为字符串,便于处理。
五、总结
通过使用 fetch 方法和后端的分块响应,我们可以有效地减少用户的等待时间,提升应用的响应速度。这种方式特别适合于需要处理大量数据的场景,如 AI 对话、实时数据更新等。当然,还有更好的方案进行流式读取,使用第三方库@microsoft/fetch-event-source具体可参考该库的文档,希望本文能够为你提供有关数据分块处理的思路,并在实际项目中有所帮助。