【问题标题】:ExpressJS and PDFKit - generate a PDF in memory and send to client for downloadExpressJS 和 PDFKit - 在内存中生成 PDF 并发送到客户端以供下载
【发布时间】:2020-01-25 04:14:48
【问题描述】:

在我的api 路由器中,有一个名为generatePDF 的函数,它旨在使用PDFKit 模块在内存中生成PDF 文件并发送到客户端进行下载而不是仅显示。

api.js:

var express = require('express');
var router = express.Router();

const PDFDocument = require('pdfkit');

router.get('/generatePDF', async function(req, res, next) {
    var myDoc = new PDFDocument({bufferPages: true});
    myDoc.pipe(res);
    myDoc.font('Times-Roman')
         .fontSize(12)
         .text(`this is a test text`);
    myDoc.end();
    res.writeHead(200, {
        'Content-Type': 'application/pdf',
        'Content-disposition': 'attachment;filename=test.pdf',
        'Content-Length': 1111
    });
    res.send( myDoc.toString('base64'));
});

module.exports = router;

这不起作用。错误信息是(node:11444) UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

我该如何解决问题并让它发挥作用?

另外,一个相关的问题是如何将 PDF 生成的业务逻辑与路由器分离并将它们链接起来?

【问题讨论】:

    标签: javascript node.js express node-pdfkit


    【解决方案1】:

    完整的解决方案。

    var express = require('express');
    var router = express.Router();
    
    const PDFDocument =  require('pdfkit');
    
    router.get('/generatePDF', async function(req, res, next) {
    var myDoc = new PDFDocument({bufferPages: true});
    
    let buffers = [];
    myDoc.on('data', buffers.push.bind(buffers));
    myDoc.on('end', () => {
    
        let pdfData = Buffer.concat(buffers);
        res.writeHead(200, {
        'Content-Length': Buffer.byteLength(pdfData),
        'Content-Type': 'application/pdf',
        'Content-disposition': 'attachment;filename=test.pdf',})
        .end(pdfData);
    
    });
    
    myDoc.font('Times-Roman')
         .fontSize(12)
         .text(`this is a test text`);
    myDoc.end();
    });
    
    module.exports = router;
    

    【讨论】:

    • 谢谢。身体是由什么引用的?是mydoc吗?
    • mydoc 看起来像一个对象,因此请了解如何从中获取实际数据。
    • 添加了完整的解决方案
    • 感谢您的回答。这行得通!唯一的问题是我仍然对将end 事件绑定到myDoc 感到困惑。 PDFKit 文档中的任何地方都没有提到这一点,并且文档确实提到了使用 doc.pipe(res) 将 pdf doc 写入 http 响应。但是您的答案中没有使用它。
    • 另一件事是在我原来的问题的末尾。 how I can separate the business logic of PDF generation from the router and chain them up? 我想从 req.query 中获取一个参数并将其传递给 PDF 生成逻辑。值得单独提出一个问题吗?
    【解决方案2】:

    首先我建议为 PDF 工具包创建一个服务。然后是一个控制器到你想要的路由。

    我使用get-stream 来简化此操作。

    它还回答了您对已接受答案的问题:

    如何将 PDF 生成的业务逻辑与 路由器并将它们链接起来?

    这是我的专业解决方案:

    import PDFDocument from 'pdfkit';
    import getStream from 'get-stream';
    import fs from 'fs';
    
    
    export default class PdfKitService {
      /**
       * Generate a PDF of the letter
       *
       * @returns {Buffer}
       */
      async generatePdf() {
        try {
          const doc = new PDFDocument();
    
          doc.fontSize(25).text('Some text with an embedded font!', 100, 100);
    
          if (process.env.NODE_ENV === 'development') {
            doc.pipe(fs.createWriteStream(`${__dirname}/../file.pdf`));
          }
    
          doc.end();
    
          const pdfStream = await getStream.buffer(doc);
    
          return pdfStream;
        } catch (error) {
          return null;
        }
      }
    }
    
    

    然后是Controller的方法:

    (...) 
    
      async show(req, res) {
        const pdfKitService = new PdfKitService();
        const pdfStream = await pdfKitService.generatePdf();
    
        res
          .writeHead(200, {
            'Content-Length': Buffer.byteLength(pdfStream),
            'Content-Type': 'application/pdf',
            'Content-disposition': 'attachment;filename=test.pdf',
          })
          .end(pdfStream);
    
     
      }
    

    最后是路线:

    routes.get('/pdf', FileController.show);
    

    【讨论】:

      【解决方案3】:

      对于那些不想浪费 RAM 缓冲 PDF 并立即将块发送给客户端的用户:

          const filename = `Receipt_${invoice.number}.pdf`;
          const doc = new PDFDocument({ bufferPages: true });
          const stream = res.writeHead(200, {
            'Content-Type': 'application/pdf',
            'Content-disposition': `attachment;filename=${filename}.pdf`,
          });
          doc.on('data', (chunk) => stream.write(chunk));
          doc.on('end', () => stream.end());
      
          doc.font('Times-Roman')
            .fontSize(12)
            .text(`this is a test text`);
          doc.end();
      

      【讨论】:

        【解决方案4】:

        你可以像这样使用 blob 流。

        参考:https://pdfkit.org/index.html

        const PDFDocument = require('pdfkit');
        
        const blobStream  = require('blob-stream');
        
        // create a document the same way as above
        
        const doc = new PDFDocument;
        
        // pipe the document to a blob
        
        const stream = doc.pipe(blobStream());
        
        // add your content to the document here, as usual
        
        doc.font('fonts/PalatinoBold.ttf')
           .fontSize(25)
           .text('Some text with an embedded font!', 100, 100);
        
        // get a blob when you're done
        doc.end();
        stream.on('finish', function() {
          // get a blob you can do whatever you like with
          const blob = stream.toBlob('application/pdf');
        
          // or get a blob URL for display in the browser
          const url = stream.toBlobURL('application/pdf');
          iframe.src = url;
        });
        

        将所有 pdf 数据通过管道传输到 blob,然后将其写入文件或 url。 或者您可以将pdf直接存储到firebase存储等云存储中并将下载链接发送给客户端。

        如果您想动态生成 pdf,那么您也可以尝试使用 node 中的 html-pdf 库,它允许您从 html 模板创建 pdf 并在其中添加动态数据。它也比pdfkit更可靠 https://www.npmjs.com/package/html-pdf 另请参阅此链接 Generate pdf file using pdfkit and send it to browser in nodejs-expressjs

        【讨论】:

        • 谢谢!最初的问题是针对节点服务器端而不是浏览器端。我去看看 html-pdf 库。
        • 是的,解决方案是浏览器端的,但是一旦你创建了blob,你就可以在你的文件系统中创建写入流,直接写入文件并存储在本地存储中,或者直接将流写入云存储。
        猜你喜欢
        • 2014-06-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-07-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-04-25
        相关资源
        最近更新 更多