html-pdf服务端生成PDF正常 客户端下载后为空白页问题
问题根因
你遇到的PDF空白、文件损坏问题全部来自代码实现错误,和html-pdf库本身无关,核心错误点如下:
response.sendFile()用法错误:该方法第一个参数必须是文件系统中真实存在的文件绝对路径,你将路径字符串转成base64后传入,属于无效参数,根本没有正确读取到生成的PDF文件。- 异步逻辑错误:
pdf.create().toFile()是回调风格API,本身不返回Promise,你用Promise.all包裹后外层await根本无法等待PDF生成完成,存在响应发送时机不对、内容传输不完整的问题。 - 前端请求配置缺失:axios默认会将二进制响应当成UTF-8文本解析,会直接破坏PDF的二进制结构,这就是典型的字节截断问题,也是页数正确但全空白的核心诱因。
- 代码存在冗余和变量冲突:你读取文件转buffer的逻辑写完后完全没有使用,同时回调内参数命名和外层响应对象重名,很容易触发隐式的变量引用错误。
- 路径拼接不规范:手动拼接路径没有用
path.join处理跨平台分隔符,很容易出现路径找不到文件的问题。
修复实现
服务端修正
不需要转base64,直接以二进制流返回文件即可,传输效率比base64高30%以上,修正后的代码:
const pdf = require("html-pdf"); const asyncHandler = require("../middleware/async"); const fs = require("fs"); const path = require("path"); const Deck = require("../models/deckModel"); const DeckListTemp = require("../templates/DeckListTemp"); const Dynamic = require("../models/dynamicModel"); /** * @description Generate PDF document of deck list * @param {Number} deckId - Deck ID * @returns {String} - PDF document * @author Austin Howard * @version 1.0.0 * @since 1.0.0 * @date 2022-5-30 * */ module.exports = asyncHandler(async (req, res, next) => { try { // find the deck const deck = await Deck.findById(req.params.deckId); // get the logo of the application const logo = await Dynamic.findOne({ type: "Logo" }); // check to see if the deck and logo exist if (!deck || !logo) { return res.status(404).json({ message: "Cannot find deck or logo on Server", }); } // sort cards by name deck.cards.sort((a, b) => { if (a.name < b.name) return -1; if (a.name > b.name) return 1; return 0; }); // pass the deck to the template const html = DeckListTemp(deck, logo.value); // sanitize deck name for file path const safeDeckName = deck.deck_name.replace(/\//g, " ").substring(0, 50); const pdfPath = path.join(__dirname, "../../../public/pdf", `${safeDeckName}.pdf`); // wrap pdf generation in promise to properly await await new Promise((resolve, reject) => { pdf .create(html, { encoding: "UTF-8", format: "A3", border: { top: "1in", right: "1in", bottom: "1in", left: "1in", }, }) .toFile(pdfPath, (err, result) => { if (err) reject(err); resolve(result); }); }); // set response headers and send file res.setHeader("Content-Type", "application/pdf"); res.setHeader( "Content-Disposition", `attachment; filename="${encodeURIComponent(deck.deck_name)}.pdf"` ); res.sendFile(pdfPath, (err) => { if (err) { console.error("PDF send error:", err); return res.status(500).json({ success: false, message: "Failed to send PDF" }); } // optional: delete temp file after send completes // fs.unlinkSync(pdfPath); }); } catch (error) { console.error(error); res.status(500).json({ success: false, message: `Server Error - ${error.message}`, }); } });
注意:文件名在Content-Disposition中需要用
encodeURIComponent处理,否则包含特殊字符、中文时会出现文件名乱码问题。
前端修正
axios必须指定响应类型为blob,避免二进制数据被文本解析破坏,修正后的代码:
import axios from "axios"; import { DECK_PREVIEW_REQUEST, DECK_PREVIEW_SUCCESS, } from "../../constants/deckConstants"; import { setAlert } from "../../utils/alert"; export const generatePdf = (deck) => async (dispatch) => { try { dispatch({ type: DECK_PREVIEW_REQUEST }); const { data } = await axios.post( `/api/deck/${deck._id}/pdf`, {}, { responseType: "blob", // 核心配置:指定响应为二进制blob类型 headers: { "Content-Type": "application/json", }, } ); // create blob link to download const url = window.URL.createObjectURL( new Blob([data], { type: "application/pdf" }) ); const link = document.createElement("a"); link.href = url; link.setAttribute("download", `${deck.deck_name.replace(/\//g, " ")}.pdf`); document.body.appendChild(link); link.click(); // clean up window.URL.revokeObjectURL(url); link.parentNode.removeChild(link); dispatch({ type: DECK_PREVIEW_SUCCESS }); } catch (error) { console.error(error); dispatch(setAlert(`problem generating pdf ${error}`, "danger")); } };
注意:下载完成后需要调用
window.URL.revokeObjectURL释放内存,避免长时间使用出现内存泄漏。
验证流程
- 修复后先直接在浏览器访问PDF生成接口,确认直接下载的文件可以正常打开,排除服务端问题。
- 对比前端下载的文件大小和服务端本地生成的PDF文件大小,大小完全一致说明传输过程没有字节丢失。
- 如果仍有异常,先注释掉临时文件删除逻辑,确认服务端存储的本地PDF文件本身内容正常。
内容的提问来源于stack exchange,提问作者Austin Howard
相关产品推荐
相关产品推荐

