You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 21:33:24