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

为什么fs.readFile在首次fetch请求调用时返回null?

问题原因

  • React状态异步更新导致首次下载条件不触发
    你调用setInvoiceStatus('succeeded')后,React的state更新是异步批量执行的,当前函数作用域内的invoiceStatus仍然是更新前的旧值。首次点击时旧值不为succeeded,downloadFile函数内的条件判断直接不生效,下载逻辑没有执行。第二次点击时,invoiceStatus已经是上一次更新后的succeeded状态,所以条件判断通过,下载可正常执行。
  • 数据类型不匹配
    服务端返回的是base64格式的PDF data URI字符串,但window.URL.createObjectURL()仅支持接收Blob/File/MediaSource类型的入参,直接传入字符串会导致创建URL失败。
  • 请求头与响应类型不匹配
    fetch请求的Accept头仅声明了接受json和表单格式的响应,未包含application/pdf,可能导致fetch解析响应时抛出异常,另外需要确认你的handleResponseForDocuments方法是用response.text()读取响应内容,若用response.json()解析非JSON格式的base64字符串,会直接返回null。

修复方案

1. 修复下载触发逻辑

你已经将接口拿到的PDF数据直接作为参数传入downloadFile,不需要额外依赖state的invoiceStatus做判断,直接移除多余的状态判断即可:

async function fetchInvoice(fileLocation) {
  setInvoiceStatus('loading');
  const invoiceRes = await invoiceService.getDocByLoc({ location: fileLocation });
  setInvoice(invoiceRes);
  setInvoiceStatus('succeeded');
  // 直接传入接口返回的结果,不依赖state
  downloadFile(invoiceRes);
}

// download file 移除状态判断,新增空值校验
function downloadFile(invoice) {
  if(!invoice) return;
  // 将base64字符串转换为Blob对象
  const base64Content = invoice.split(',')[1];
  const byteChars = atob(base64Content);
  const byteArr = new Uint8Array(byteChars.length);
  for (let i = 0; i < byteChars.length; i++) {
    byteArr[i] = byteChars.charCodeAt(i);
  }
  const pdfBlob = new Blob([byteArr], { type: 'application/pdf' });
  
  const url = window.URL.createObjectURL(pdfBlob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'test.pdf';
  a.click();
  // 释放URL对象,避免内存泄漏
  window.URL.revokeObjectURL(url);
}

如果你更希望遵循React状态驱动的范式,可以改用useEffect监听状态变化触发下载:

useEffect(() => {
  if(invoiceStatus === 'succeeded' && invoice) {
    downloadFile(invoice);
  }
}, [invoice, invoiceStatus])

async function fetchInvoice(fileLocation) {
  setInvoiceStatus('loading');
  const res = await invoiceService.getDocByLoc({ location: fileLocation });
  setInvoice(res);
  setInvoiceStatus('succeeded');
}

2. 修正请求头

在fetchWrapper的请求头中添加application/pdf到Accept列表:

function getDocument(url, location) {
  const requestOptions = {
    method: 'POST',
    headers: {
      Accept: 'application/json, application/x-www-form-urlencoded, application/pdf',
      'Content-Type': 'application/json',
      ...authHeader(url),
    },
    credentials: 'include',
    body: JSON.stringify(location),
  };

  return fetch(`${AppSettings.serverEndpoint}${url}`, requestOptions).then(
    handleResponseForDocuments
  );
}

同时确认handleResponseForDocuments中使用response.text()读取响应内容,不要使用response.json()。

可选优化:服务端直接返回二进制流

你可以不用在服务端将PDF转成base64,直接返回二进制流,既减少传输体积,也能简化前端处理逻辑:
Node端代码修改:

function getDocumentByLocation(req, res, next) {
  const { location } = req.body;
  fs.readFile(location, (err, data) => {
    if (err) return res.status(500).send(err);
    res.contentType('application/pdf').send(data);
  });
}

对应前端的handleResponseForDocuments修改为直接返回Blob:

async function handleResponseForDocuments(response) {
  if (!response.ok) return Promise.reject(response.statusText);
  return response.blob();
}

这种场景下downloadFile不需要再做base64转Blob的处理,直接使用接口返回的Blob对象即可。

内容的提问来源于stack exchange,提问作者DRobinson

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.30 23:39:04