RUVDS.com corporate blog
JavaScript
Node.JS
PDF
ReactJS
July 19

Создание динамических PDF-файлов с использованием React и Node.js

Original author: Adrian Hajdin
Translation
Материал, перевод которого мы сегодня публикуем, посвящён созданию динамических PDF-файлов с использованием HTML-кода в качестве шаблона. А именно, речь пойдёт о том, как сформировать простой счёт на оплату неких товаров или услуг, динамические данные, включённые в который, берутся из состояния 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,
}

Удалим стандартную JSX-разметку, созданную в шаблоне приложения средствами create-react-app и возвращаемую из метода render(). Вставим туда следующее:

<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-файлов.

Создание HTML-шаблона для PDF-файлов


Нам нужен HTML-шаблон, который будет использоваться при создании PDF-файлов. В деле создания подобного шаблона перед нами открываются бесконечные возможности. В виде PDF-файла может быть представлено всё, что можно создать, используя чистый HTML и CSS. Создадим в папке server директорию documents, перейдём в неё и создадим в ней файл index.js:

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

Из этого файла экспортируем стрелочную функцию, которая будет возвращать весь необходимый HTML-код. При вызове этой функции можно воспользоваться параметрами, которые мы тоже здесь опишем.

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

Тут я дам вам пример HTML-шаблона, а вы просто скопируете его в свой проект. Но вы, конечно же, можете создать собственный шаблон.

Приведём код index.js из папки server/documents к следующему виду:

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:

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

Создание PDF-файлов


Напомним, что на сервере мы собираемся создать два маршрута. POST-маршрут будет отвечать за приём данных с клиента и создание PDF-файла. GET-маршрут будет использоваться для отправки готового файла клиенту.

▍Маршрут create-pdf


В POST-маршруте create-pdf мы будем пользоваться командой pdf.create(), обращаясь к объекту, импортированному из модуля html-pdf.

В качестве первого параметра метода pdf.create() используется HTML-шаблон, а также данные, поступившие с клиента.

Для того, что возвратит pdf.create(), мы вызовем метод toFile(), передав ему имя, которое хотим назначить PDF-документу, а также стрелочную функцию обратного вызова. Эта функция, в случае возникновения ошибки, выполнит команду res.send(Promise.reject()). В том случае, если всё прошло успешно — она выполнит команду 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());
    });
});

▍Маршрут fetch-pdf


Заодно создадим и маршрут, который будет использоваться после того, как, по запросу клиента, будет успешно создан PDF-файл. Здесь мы просто берём готовый документ и отправляем его клиенту с использованием res.sendFile():

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

Клиентская функция createAndDownloadPdf


Теперь мы можем вернуться к клиентскому коду и продолжить работу над функцией createAndDownloadPdf. Здесь мы выполняем POST-запрос к серверу с использованием модуля axios. Сейчас эта функция выглядит так:

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

Если после выполнения POST-запроса к серверу был создан PDF-документ — нам нужно выполнить и GET-запрос, в ответ на который сервер отправит клиенту готовый документ.

Для реализации такой схемы поведения мы, после вызова axios.post(), вызываем .then(). Сделать это нам позволяет то, что в ответ на POST-запрос клиента мы возвращаем с сервера промис, который может быть либо успешно разрешён, либо отклонён.

Дополним функцию следующим кодом:

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

Тут можно обратить внимание на то, что в качестве responseType используется blob. Прежде чем мы продвинемся дальше — давайте поговорим о том, что это такое.

Объекты Blob


Blob — это иммутабельный объект, представляющий собой некие необработанные данные. Такие объекты часто используются для работы с данными, которые могут быть представлены не в родном формате JavaScript. Подобные объекты представляют собой последовательности байтов, хранящие, например, данные файлов. Может показаться, что объект Blob хранит ссылку на файл, но на самом деле это не так. Эти объекты хранят данные, с которыми можно работать. Например — их можно сохранять в файлы.

Завершение работы над проектом


Теперь, когда мы знаем о том, что представляют собой объекты Blob, мы можем воспользоваться ещё одним вызовом .then(), создав в нём, на основе res.data, новый Blob-объект. При создании этого объекта передадим его конструктору параметр, указывающий на то, что данные, которые будет хранить объект, имеют тип application/pdf. После этого мы можем воспользоваться методом saveAs(), который импортировали из модуля file-saver, и сохранить данные в файл. В итоге код метода 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-репозиторий с кодом проекта. Надеемся, идеи, которые вы встретили в этом материале, найдут применение в ваших разработках.

Уважаемые читатели! Как бы вы подошли к решению задачи программного создания PDF-файлов средствами Node.js?

+24
7k 121
Comments 3
Top of the day