为什么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
相关产品推荐
相关产品推荐

