使用React和Node.js创建动态PDF

该材料(我们今天发布的翻译)致力于使用HTML代码作为模板来创建动态PDF文件。 即,我们将讨论如何创建用于支付某些商品或服务的简单发票,其中包含的动态数据取自React应用程序的状态。 React应用程序的基础是使用create-react-app创建的,项目的服务器部分基于Node.js,Express框架用于其开发中。



该材料的作者指出,他准备了一个视频来演示该项目的发展。 如果您决定观看视频并阅读文章,建议您这样做。 首先,浏览文章,然后打开视频并重新创建您正在考虑的系统。 之后,只需阅读文章即可。

项目创建


创建一个项目目录并转到它:

mkdir pdfGenerator && cd pdfGenerator 

创建一个新的React应用程序:

 create-react-app client 

完成应用程序创建之后,转到您刚刚创建的目录并安装依赖项:

 cd client && npm i -S axios file-saver 

创建一个Express服务器。 为此,请在项目目录中创建server文件夹,然后转到该文件夹​​。 在其中创建index.js文件并开始项目初始化:

 mkdir server && cd server && touch index.js && npm init 

在这里,要形成package.json ,只需按几次Enter 。 之后,执行以下命令以将必要的依赖项添加到项目的服务器部分:

 npm i -S express body-parser cors html-pdf 

现在,在client/package.json文件的依赖项描述部分上方,添加以下内容:

 "proxy": "http://localhost:5000/" 

这将有助于从客户端代码使用本地服务器。

现在您需要打开终端的两个窗口。

在第一个窗口中,转到client目录并执行以下命令:

 npm start 

在第二个窗口中,转到server文件夹并执行以下命令:

 nodemon index.js 

初始客户端设置


我们项目的客户部分看起来非常简单。

首先,在应用程序客户端部分的src/App.js ,我们将依赖项导入代码中:

 import axios from 'axios'; import { saveAs } from 'file-saver'; 

之后,在组件描述的顶部,初始化状态:

 state = {   name: 'Adrian',   receiptId: 0,   price1: 0,   price2: 0, } 

让我们删除使用create-react-app在应用程序模板中create-react-app并从render()方法返回的标准JSX标记。 将以下内容插入其中:

 <div className="App">   <input type="text" placeholder="Name" name="name" onChange {this.handleChange}/>   <input type="number" placeholder="Receipt ID" name="receiptId"  onChange={this.handleChange}/>   <input type="number" placeholder="Price 1" name="price1" onChange={this.handleChange}/>   <input type="number" placeholder="Price 2" name="price2" onChange={this.handleChange}/>   <button onClick={this.createAndDownloadPdf}>Download PDF</button></div> 

让我们创建handleChange方法,该方法将负责更新与输入字段相关的应用程序状态数据:

 handleChange = ({ target: { value, name }}) => this.setState({ [name]: value }) 

现在,我们可以继续创建PDF文件的任务。 其中的一部分(通过客户端解决)是向服务器创建POST请求。 该请求发送应用程序状态:

 createAndDownloadPdf = () => {   axios.post('/create-pdf', this.state) } 

在继续进行项目客户端之前,我们需要在服务器上配置路由。 这将允许服务器从客户端接收数据,生成PDF文件并将该文件传输回客户端。

初始服务器设置


项目的服务器部分将仅包括两条路线。 需要一个来创建PDF。 第二个是用于将文件发送到客户端。

首先,将依赖项导入到index.js文件中:

 const express = require('express'); const bodyParser = require('body-parser'); const pdf = require('html-pdf'); const cors = require('cors'); 

我们初始化Express应用程序并配置端口:

 const app = express(); const port = process.env.PORT || 5000; 

设置查询解析器。 我们需要的将以req.body形式提供。 我们还将配置CORS,以便我们的Cross-Origin Request Blocked错误不会干扰我们的工作:

 app.use(cors()); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); 

之后,启动服务器:

 app.listen(port, () => console.log(`Listening on port ${port}`)); 

现在我们可以处理负责创建PDF的代码。

为PDF创建HTML模板


创建PDF文件时,我们需要使用HTML模板。 在创建这样的模板时,我们面前蕴藏着无限的可能性。 可以使用纯HTML和CSS创建的所有内容都可以表示为PDF文件。 在server文件夹中创建documents目录,转到该目录并在其中创建index.js文件:

 mkdir documents && cd documents && touch index.js 

从该文件中,我们导出一个箭头函数,该函数将返回所有必需的HTML代码。 调用此函数时,可以使用参数,我们还将在此处进行描述。

 module.exports = ({ name, price1, price2, receiptId }) => { ... } 

在这里,我将为您提供一个 HTML模板的示例 ,您只需将其复制到您的项目中即可。 但是您当然可以创建自己的模板。

让我们index.js server/documents文件夹中index.js代码转换为以下格式:

 module.exports = ({ name, price1, price2, receiptId }) => {    const today = new Date(); return `    <!doctype html>    <html>       <head>          <meta charset="utf-8">          <title>PDF Result Template</title>          <style>             .invoice-box {             max-width: 800px;             margin: auto;             padding: 30px;             border: 1px solid #eee;             box-shadow: 0 0 10px rgba(0, 0, 0, .15);             font-size: 16px;             line-height: 24px;             font-family: 'Helvetica Neue', 'Helvetica',             color: #555;             }             .margin-top {             margin-top: 50px;             }             .justify-center {             text-align: center;             }             .invoice-box table {             width: 100%;             line-height: inherit;             text-align: left;             }             .invoice-box table td {             padding: 5px;             vertical-align: top;             }             .invoice-box table tr td:nth-child(2) {             text-align: right;             }             .invoice-box table tr.top table td {             padding-bottom: 20px;             }             .invoice-box table tr.top table td.title {             font-size: 45px;             line-height: 45px;             color: #333;             }             .invoice-box table tr.information table td {             padding-bottom: 40px;             }             .invoice-box table tr.heading td {             background: #eee;             border-bottom: 1px solid #ddd;             font-weight: bold;             }             .invoice-box table tr.details td {             padding-bottom: 20px;             }             .invoice-box table tr.item td {             border-bottom: 1px solid #eee;             }             .invoice-box table tr.item.last td {             border-bottom: none;             }             .invoice-box table tr.total td:nth-child(2) {             border-top: 2px solid #eee;             font-weight: bold;             }             @media only screen and (max-width: 600px) {             .invoice-box table tr.top table td {             width: 100%;             display: block;             text-align: center;             }             .invoice-box table tr.information table td {             width: 100%;             display: block;             text-align: center;             }             }          </style>       </head>       <body>          <div class="invoice-box">             <table cellpadding="0" cellspacing="0">                <tr class="top">                   <td colspan="2">                      <table>                         <tr>                            <td class="title"><img src="https://i2.wp.com/cleverlogos.co/wp-content/uploads/2018/05/reciepthound_1.jpg?fit=800%2C600&ssl=1"                               style="width:100%; max-width:156px;"></td>                            <td>                               Datum: ${`${today.getDate()}. ${today.getMonth() + 1}. ${today.getFullYear()}.`}                            </td>                         </tr>                      </table>                   </td>                </tr>                <tr class="information">                   <td colspan="2">                      <table>                         <tr>                            <td>                               Customer name: ${name}                            </td>                            <td>                               Receipt number: ${receiptId}                            </td>                         </tr>                      </table>                   </td>                </tr>                <tr class="heading">                   <td>Bought items:</td>                   <td>Price</td>                </tr>                <tr class="item">                   <td>First item:</td>                   <td>${price1}$</td>                </tr>                <tr class="item">                   <td>Second item:</td>                   <td>${price2}$</td>                </tr>             </table>             <br />             <h1 class="justify-center">Total price: ${parseInt(price1) + parseInt(price2)}$</h1>          </div>       </body>    </html>    `; }; 

server/index.js将此文件包含在server/index.js

 const pdfTemplate = require('./documents'); 

创建PDF


回想一下,我们将在服务器上创建两条路由。 POST路由将负责从客户端接收数据并创建PDF文件。 GET路由将用于将完成的文件发送到客户端。

▍create-pdf路线


create-pdf POST路由中,我们将使用pdf.create()命令,引用从html-pdf模块导入的对象。

作为pdf.create()方法的第一个参数,将使用HTML模板以及从客户端接收的数据。

要返回pdf.create() ,我们调用toFile()方法,并为其传递我们要分配给PDF文档的名称以及箭头回调函数。 发生错误时,此函数将执行res.send(Promise.reject())命令。 如果一切顺利,她将执行res.send(Promise.resolve())命令res.send(Promise.resolve())

 app.post('/create-pdf', (req, res) => {    pdf.create(pdfTemplate(req.body), {}).toFile('result.pdf', (err) => {        if(err) {            res.send(Promise.reject());        }        res.send(Promise.resolve());    }); }); 

etch获取-pdf路线


同时,我们将创建一条路由,该路由将在应客户端请求成功创建PDF文件之后使用。 在这里,我们只是获取完成的文档,然后使用res.sendFile()将其发送给客户端:

 app.get('/fetch-pdf', (req, res) => {    res.sendFile(`${__dirname}/result.pdf`) }) 

客户端功能createAndDownloadPdf


现在,我们可以返回客户端代码,并继续使用createAndDownloadPdf函数。 在这里,我们使用axios模块向服务器发出POST请求。 现在,该函数如下所示:

 createAndDownloadPdf = () => {   axios.post('/create-pdf', this.state) } 

如果在对服务器执行POST请求之后创建了PDF文档,则我们需要执行GET请求,作为响应,服务器会将完成的文档发送给客户端。

为了实现此行为方案,我们在调用axios.post()之后进行调用。 then() 。 这使我们能够响应客户的POST请求,从服务器返回一个可以成功解决或拒绝的承诺。

我们用以下代码补充该功能:

 .then(() => axios.get('/fetch-pdf', { responseType: 'blob' })) .then(( res) => {}) 

在这里,您可以注意blob用作responseType的事实。 在继续之前,让我们谈谈它的含义。

Blob对象


Blob是代表某些原始数据的不可变对象。 此类对象通常用于处理可能不是本机JavaScript格式的数据。 这样的对象是存储例如文件数据的字节序列。 似乎Blob对象正在存储指向文件的链接,但实际上并非如此。 这些对象存储您可以使用的数据。 例如-它们可以保存到文件中。

项目完成


现在我们知道了Blob对象是什么,我们可以使用另一个.then()调用基于res.data在其中res.data新的Blob对象。 创建此对象时,我们会将一个参数传递给它的构造函数,指示该对象将存储的数据为application/pdf类型。 之后,我们可以使用从file-saver保存器模块导入的saveAs()方法,并将数据保存到文件中。 结果, createAndDowndloadPdf方法的代码如下所示:

   createAndDownloadPdf = () => {    axios.post('/create-pdf', this.state)      .then(() => axios.get('fetch-pdf', { responseType: 'blob' }))      .then((res) => {        const pdfBlob = new Blob([res.data], { type: 'application/pdf' });        saveAs(pdfBlob, 'newPdf.pdf');      })  } 

这是浏览器界面的外观。


浏览器中的应用

填写字段并单击“ Download PDF按钮后,数据将传输到服务器,并从服务器下载PDF文档。


下载PDF

这是PDF文档本身。


软件生成的PDF

总结


我们研究了一种允许您以编程方式创建PDF文件的机制。 这是一个包含项目代码 GitHub存储库。 我们希望您在本材料中遇到的想法能够在您的开发中找到应用。

亲爱的读者们! 您将如何解决使用Node.js以编程方式创建PDF文件的问题?

Source: https://habr.com/ru/post/zh-CN460018/


All Articles