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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 11:52:06