Express中PDFKit生成PDF阻塞请求,如何实现同步生成?
嘿,我来帮你搞定这个问题!你遇到的情况是因为当前的PDF生成逻辑是同步阻塞式的——Node.js是单线程事件循环模型,同步代码会一直占用主线程,导致Express没法处理其他进来的请求。咱们可以通过异步化处理来解决,下面给你几个实用的方案:
1. 把PDF生成逻辑包装成异步函数(最基础的改造)
Express支持异步路由,只要把你的PDF生成逻辑改成返回Promise的异步函数,再用async/await调用,就能让主线程解放出来处理其他请求。示例代码如下:
// 先把PDF生成逻辑封装成返回Promise的异步函数 function generatePDF(publicationId, booklet, canvasWidth, canvasHeight, pageWidth) { return new Promise((resolve, reject) => { // 这里放你原来的PDF生成代码(比如用pdfkit、html-pdf等库) // 当PDF生成完成后,调用resolve传递生成的Buffer或流 // 如果出错,调用reject传递错误信息 // 举个简单的pdfkit示例: const PDFDocument = require('pdfkit'); const doc = new PDFDocument(); let buffers = []; doc.on('data', buffers.push.bind(buffers)); doc.on('end', () => resolve(Buffer.concat(buffers))); doc.on('error', reject); // 这里添加你的PDF内容渲染逻辑 doc.text(`Publication ID: ${publicationId}`); if (booklet) doc.text('Booklet mode enabled'); doc.end(); }); } // 改造后的路由处理函数 app.get('/pdf', async function (req, res) { try { const publication_id = req.query['id']; const booklet = req.query['booklet']; const canvasWidth = 450; const canvasHeight = 636; const pageWidth = canvasWidth / 1.34; // 异步生成PDF,不会阻塞主线程 const pdfBuffer = await generatePDF(publication_id, booklet, canvasWidth, canvasHeight, pageWidth); // 设置响应头并返回PDF res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', 'inline; filename="publication.pdf"'); res.send(pdfBuffer); } catch (error) { console.error('PDF生成失败:', error); res.status(500).send('生成PDF时出现错误,请稍后重试'); } });
2. 用子进程处理CPU密集型生成任务
如果你的PDF生成需要渲染大量页面、处理复杂逻辑(属于CPU密集型操作),即使是异步Promise也会占用主线程资源。这时候可以用Node.js的child_process模块,把生成任务放到独立的子进程中执行,主线程完全不会被阻塞:
const { spawn } = require('child_process'); const path = require('path'); app.get('/pdf', function (req, res) { const publication_id = req.query['id']; const booklet = req.query['booklet']; // 启动子进程,传递生成所需的参数 const pdfProcess = spawn('node', [ path.join(__dirname, 'pdf-generator.js'), publication_id, booklet ? 'true' : 'false' ]); let pdfBuffer = Buffer.from([]); // 收集子进程输出的PDF数据 pdfProcess.stdout.on('data', (chunk) => { pdfBuffer = Buffer.concat([pdfBuffer, chunk]); }); // 处理子进程的错误输出 pdfProcess.stderr.on('data', (data) => { console.error('子进程执行错误:', data.toString()); res.status(500).send('PDF生成失败'); }); // 子进程结束时返回结果 pdfProcess.on('close', (code) => { if (code === 0) { res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', 'attachment; filename="publication.pdf"'); res.send(pdfBuffer); } else { res.status(500).send('PDF生成进程异常退出'); } }); });
对应的pdf-generator.js文件(单独的子进程脚本):
const PDFDocument = require('pdfkit'); const [publicationId, bookletStr] = process.argv.slice(2); const booklet = bookletStr === 'true'; const doc = new PDFDocument(); // 直接将PDF输出到stdout,供主进程收集 doc.pipe(process.stdout); // 这里写你的PDF渲染逻辑 doc.text(`Publication ID: ${publicationId}`); if (booklet) doc.text('Booklet mode enabled'); doc.end();
3. 队列系统(高并发场景推荐)
如果你的服务有大量PDF生成请求,上面的方法可能还是会导致服务器过载。这时候可以引入消息队列(比如Bull、Bee-Queue),把生成任务放到队列中,用独立的worker进程后台处理,前端通过轮询或WebSocket获取生成状态:
// 主进程路由代码 const Queue = require('bull'); const pdfQueue = new Queue('pdf-generation', 'redis://localhost:6379'); // 需要安装Redis // 提交PDF生成任务的路由 app.get('/pdf', async function (req, res) { const publication_id = req.query['id']; const booklet = req.query['booklet']; const job = await pdfQueue.add({ publicationId: publication_id, booklet: booklet, canvasWidth: 450, canvasHeight: 636, pageWidth: 450 / 1.34 }); // 返回任务ID,让客户端查询生成状态 res.json({ jobId: job.id, message: 'PDF生成任务已提交,请稍后查询状态' }); }); // 查询任务状态的路由 app.get('/pdf/status/:jobId', async function (req, res) { const job = await pdfQueue.getJob(req.params.jobId); if (!job) return res.status(404).send('任务不存在'); const status = await job.getState(); if (status === 'completed') { const result = await job.returnvalue(); res.json({ status, pdfUrl: result.pdfUrl }); } else if (status === 'failed') { res.json({ status, error: 'PDF生成失败' }); } else { res.json({ status, message: 'PDF生成中,请稍后再试' }); } }); // Worker进程代码(可以单独放在worker.js文件中启动) pdfQueue.process(async (job) => { const { publicationId, booklet, canvasWidth, canvasHeight, pageWidth } = job.data; // 调用你的PDF生成函数 const pdfBuffer = await generatePDF(publicationId, booklet, canvasWidth, canvasHeight, pageWidth); // 把PDF保存到本地或云存储,这里示例存到uploads目录 const fs = require('fs/promises'); const filePath = `./uploads/${publicationId}.pdf`; await fs.writeFile(filePath, pdfBuffer); // 返回PDF的访问地址 return { pdfUrl: `/uploads/${publicationId}.pdf` }; });
核心原理说明
Node.js的单线程事件循环,一旦被同步阻塞代码占用,就没法处理其他请求。所以解决思路就是:
- 把阻塞逻辑移到异步回调/Promise中,让事件循环能继续处理其他任务
- 对于CPU密集型任务,用子进程/线程隔离,避免占用主线程
- 高并发场景用队列+Worker,实现任务的异步解耦和流量削峰
内容的提问来源于stack exchange,提问作者slwr
相关产品推荐
相关产品推荐

