Express res.write分块响应 Vue客户端fetch实时接收进度方案
问题背景
现有技术栈如下:
- 客户端(VUE):运行端口8080
- 服务端(express.js):运行端口3000
客户端向服务端路由发送GET请求,服务端路由内执行遍历数组的耗时任务,任务进度可量化统计。核心疑问:服务端是否可通过res.write发送响应,实时触发客户端侧动作?
当前测试现象
服务端编写代码定时调用res.write发送进度数据,Chrome网络面板可观测到分块传输的内容,但客户端fetch调用时,仅在服务端触发res.end()后才能拿到完整数据,无法在每次服务端write时触发界面更新,目标是实现*“正在执行第n项操作,共x项”*的实时进度计数器。
现有测试代码
服务端代码(server.mjs)
server.get('/action', async (req, res) => { res.setHeader('Content-Type', 'application/json'); // 测试逻辑 res.write(JSON.stringify({current: 0, total: 0, status: 'pending'})); setTimeout(() => {res.write(JSON.stringify({current: 2, total: 0, status: 'pending'}));},1000); setTimeout(() => {res.write(JSON.stringify({current: 3, total: 0, status: 'pending'}));},2000); setTimeout(() => {res.write(JSON.stringify({current: 4, total: 0, status: 'pending'}));},3000); setTimeout(() => {res.write(JSON.stringify({current: 5, total: 0, status: 'pending'}));},4000); res.end() })
客户端代码(Vue组件内client.js)
const requestOptions = { headers: { "Content-Type": "application/json", "Accept": "application/json", "Authorization": this.authKey, "Save-As": this.saveAs, "UUID": this.uuid, "Name": this.name } }; fetch(url, requestOptions) .then(response => { return response.json() }) .then((data) => { console.log(data) this.downloadLink = data.url }) .catch(error => { console.error(error); this.showError("Backend not reachable") });
问题原因与解决方案
问题根源
客户端调用response.json()会等待整个响应体完全接收完成(即服务端调用res.end()之后)才会解析返回完整JSON对象,无法获取中间分块发送的进度数据。
同时服务端设置的Content-Type: application/json不适配分块流式传输场景:标准JSON响应要求完整合法的JSON结构,分块发送的多段JSON拼接后本身属于非法JSON格式。
服务端调整
修改响应头,开启分块传输编码,使用适配流场景的内容类型,每个数据块后添加换行符方便客户端切分:
server.get('/action', async (req, res) => { // 替换原有Content-Type,开启分块传输,禁用缓存避免中间代理缓存响应块 res.setHeader('Content-Type', 'text/plain; charset=utf-8'); res.setHeader('Transfer-Encoding', 'chunked'); res.setHeader('Cache-Control', 'no-cache, no-transform'); res.setHeader('Connection', 'keep-alive'); // 每个块末尾加换行符作为消息分隔符 res.write(JSON.stringify({current: 0, total: 5, status: 'pending'}) + '\n'); setTimeout(() => {res.write(JSON.stringify({current: 2, total: 5, status: 'pending'}) + '\n');},1000); setTimeout(() => {res.write(JSON.stringify({current: 3, total: 5, status: 'pending'}) + '\n');},2000); setTimeout(() => {res.write(JSON.stringify({current: 4, total: 5, status: 'pending'}) + '\n');},3000); setTimeout(() => { res.write(JSON.stringify({current: 5, total: 5, status: 'done', url: 'xxx'}) + '\n'); res.end() },4000); })
客户端调整
放弃使用response.json(),通过response.body返回的可读流(ReadableStream)逐块读取数据,解析每一段进度内容实时更新UI:
const requestOptions = { headers: { // 移除Accept: application/json配置 "Authorization": this.authKey, "Save-As": this.saveAs, "UUID": this.uuid, "Name": this.name } }; const response = await fetch(url, requestOptions); const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; // 缓存未读完的分片,规避TCP拆包导致的JSON解析失败问题 while (true) { const { done, value } = await reader.read(); if (done) break; // 将当前读取到的二进制块转成字符串拼入缓存 buffer += decoder.decode(value, { stream: true }); // 按换行切分完整的消息块 const chunks = buffer.split('\n'); // 最后一段可能是不完整的分片,留到下次读取后拼接处理 buffer = chunks.pop(); for (const chunk of chunks) { if (!chunk.trim()) continue; const data = JSON.parse(chunk); console.log('收到进度更新', data); // 实时更新页面UI this.progressText = `正在执行第${data.current}项操作,共${data.total}项`; if (data.status === 'done') { this.downloadLink = data.url; } } }
注意:如果开发环境使用了webpack/vite代理、Nginx反向代理,需要关闭代理的响应缓冲配置,否则代理会等待完整响应接收完成才转发给前端,会出现Chrome网络面板能看到分块但前端无法实时接收的问题。
内容的提问来源于stack exchange,提问作者Marco
相关产品推荐
相关产品推荐

