ایجاد PDF با Node.js — از صفر تا صد
همه ما از PDF استفاده میکنیم. PDF یکی از رایجترین فرمتهای فایل به حساب میآید. از این رو بسیاری از اپلیکیشنها، این قابلیت را دارند که از انواع مختلفی از اسناد، فایل PDF بسازند. ایجاد PDF با Node.js با استفاده از کتابخانههای شخص ثالث کار آسانی است و میتوانیم به سهولت این قابلیت را به اپلیکیشنهای خود اضافه کنیم.
در این مقاله، اپلیکیشنی میسازیم که به کاربران امکان میدهد سندهای خود را در یک ادیتور متنی وارد کنند و از روی آن PDF ایجاد نمایند. ما از Express به عنوان بکاند و از ریاکت به عنوان فرانتاند استفاده میکنیم.
بکاند
کار خود را از بکاند آغاز میکنیم.
برای شروع یک دایرکتوری پروژه ایجاد میکنیم که پوشهای به نام backend در آن قرار دارد. سپس در پوشه backend دستور زیر را اجرا میکنیم تا اپلیکیشن Express اجرا شود:
npx express-generator
در ادامه با اجرای دستور npm i پکیجها را نصب میکنیم. سپس پکیجهای خودمان را نصب میکنیم. برای اجرای اپلیکیشن با جدیدترین نسخه جاوا اسکریپت به Babel نیاز داریم. برای درخواستهای «بین دامنهای» (cross-domain) از فرانتاند، پکیج CORS را نصب میکنیم، پکیج HTML-PDF برای تبدیل رشتههای HTML به PDF استفاده میشود، پکیج Multer برای آپلود، Sequelize برای ORM و SQLite3 نیز برای پایگاه داده مورد استفاده قرار خواهند گرفت.
همه این پکیجها را با دستور زیر نصب میکنیم:
npm i @babel/cli @babel/core @babel/node @babel/preset-env cors html-pdf sequelize sqlite3 multer
سپس بخش scripts فایل package.json را طوری تغییر میدهیم که به صورت زیر درآید:
"start": "nodemon --exec npm run babel-node --./bin/www","babel-node": "babel-node"
بدین ترتیب میتوانیم اپلیکیشن را به جای محیط زمان اجرای معمول Node با Babel اجرا کنیم. سپس باید یک فایل به نام .babelrc در پوشه backend ایجاد کرده و کد زیر را در آن قرار دهیم:
1{
2 "presets": [
3 "@babel/preset-env"
4 ]
5}
کد فوق تعیین میکند که اپلیکیشن را با جدیدترین نسخه از جاوا اسکریپت اجرا میکنیم. سپس کد پایگاه داده را اضافه میکنیم. ابتدا دستور زیر را در پوشه backend اجرا کنید تا کد Sequelize تولید شود:
npx sequelize-cli init
اینک باید فایل config.js را در پروژه خود داشته باشیم. کد زیر را در این فایل اضافه کنید:
1{
2 "development": {
3 "dialect": "sqlite",
4 "storage": "development.db"
5 },
6 "test": {
7 "dialect": "sqlite",
8 "storage": "test.db"
9 },
10 "production": {
11 "dialect": "sqlite",
12 "storage": "production.db"
13 }
14}
کد فوق SQLite را برای پایگاه داده ما اعلان میکند. سپس مدل خود را ایجاد کرده و با اجرای دستور زیر migration میکنیم:
npx sequelize-cli model:create --name Document --attributes name:string,document:text,pdfPath:string
بدین ترتیب یک مدل Document و یک جدول Documents ایجاد میشود. سپس برای ایجاد پایگاه داده دستور زیر را اجرا میکنیم:
npx sequelize-cli db:migrate
اکنون باید مسیرهای خود را ایجاد نماییم. به این منظور فایلی به نام pdf.js در پوشه routes میسازیم و کد زیر را به آن اضافه میکنیم:
1var express = require("express");
2var pdf = require("html-pdf");
3const models = require("../models");
4var multer = require("multer");
5const fs = require("fs");
6var router = express.Router();
7const storage = multer.diskStorage({
8 destination: (req, file, cb) => {
9 cb(null, "./files");
10 },
11 filename: (req, file, cb) => {
12 cb(null, `${file.fieldname}_${+new Date()}.jpg`);
13 }
14});
15const upload = multer({
16 storage
17});
18router.get("/", async (req, res, next) => {
19 const documents = await models.Document.findAll();
20 res.json(documents);
21});
22router.post("/", async (req, res, next) => {
23 const document = await models.Document.create(req.body);
24 res.json(document);
25});
26router.put("/:id", async (req, res, next) => {
27 const id = req.params.id;
28 const { name, document } = req.body;
29 const doc = await models.Document.update(
30 { name, document },
31 { where: { id } }
32 );
33 res.json(doc);
34});
35router.delete("/:id", async (req, res, next) => {
36 const id = req.params.id;
37 await models.Document.destroy({ where: { id } });
38 res.json({});
39});
40router.get("/generatePdf/:id", async (req, res, next) => {
41 const id = req.params.id;
42 const documents = await models.Document.findAll({ where: { id } });
43 const document = documents[0];
44 const stream = await new Promise((resolve, reject) => {
45 pdf.create(document.document).toStream((err, stream) => {
46 if (err) {
47 reject(reject);
48 return;
49 }
50 resolve(stream);
51 });
52 });
53const fileName = `${+new Date()}.pdf`;
54 const pdfPath = `${__dirname}/../files/${fileName}`;
55 stream.pipe(fs.createWriteStream(pdfPath));
56 const doc = await models.Document.update(
57 { pdfPath: fileName },
58 { where: { id } }
59 );
60 res.json(doc);
61});
62router.post("/uploadImage", upload.single('upload'), async (req, res, next) => {
63 res.json({
64 uploaded: true,
65 url: `${process.env.BASE_URL}/${req.file.filename}`
66 });
67});
68module.exports = router;
عملیات استاندارد CRUD را روی جدول Documents در 4 مسیر نخست اجرا میکنیم. عملیات GET را برای واکشی همه Documents، عملیات POST را برای ایجاد یک Document از روی پارامترهای آن، عملیات PUT را برای بهروزرسانی یک Document بر اساس ID و عملیات DELETE را برای حذف کردن یک Document با گشتن به دنبال ID آن داریم. از HTML در فیلد document برای تولید PDF در ادامه استفاده خواهیم کرد.
generatePdf تابعی است که به ما امکان میدهد PDF را بسازیم. ID را از URL به دست میآوریم و سپس از پکیج HTML-PDF برای تولید PDF بهره میگیریم. با تبدیل سند HTML به یک شیء استریم فایل با استفاده از پکیج HTML-PDF اقدام به تولید PDF میکنیم. سپس استریم را در یک فایل مینویسیم و مسیر را در فایل در مدل Document با ID موجود در پارامتر URL ذخیره میکنیم.
همچنین یک مسیر uploadImage داریم تا به کاربر امکان دهیم که تصاویر را با CKEditor آپلود کند. این پلاگین در پاسخ انتظار پارامترهای uploaded و url را میکشد که تابع ما بازگشت میدهد. سپس باید یک پوشه به نام files در دایرکتوری backend اضافه کنیم. در ادامه در فایل app.js کد موجود را با کد زیر عوض میکنیم:
1var createError = require("http-errors");
2var express = require("express");
3var path = require("path");
4var cookieParser = require("cookie-parser");
5var logger = require("morgan");
6var cors = require("cors");
7var indexRouter = require("./routes/index");
8var pdfRouter = require("./routes/pdf");
9var app = express();
10// view engine setup
11app.set("views", path.join(__dirname, "views"));
12app.set("view engine", "jade");
13app.use(logger("dev"));
14app.use(express.json());
15app.use(express.urlencoded({ extended: false }));
16app.use(cookieParser());
17app.use(express.static(path.join(__dirname, "public")));
18app.use(express.static(path.join(__dirname, "files")));
19app.use(cors());
20app.use("/", indexRouter);
21app.use("/pdf", pdfRouter);
22// catch 404 and forward to error handler
23app.use(function(req, res, next) {
24 next(createError(404));
25});
26// error handler
27app.use(function(err, req, res, next) {
28 // set locals, only providing error in development
29 res.locals.message = err.message;
30 res.locals.error = req.app.get("env") === "development" ? err : {};
31// render the error page
32 res.status(err.status || 500);
33 res.render("error");
34});
35module.exports = app;
پوشه فایل را با کد زیر عرضه میکنیم:
app.use(express.static(path.join(__dirname, "files")));
همچنین مسیر pdf را با کد زیر عرضه میکنیم:
var pdfRouter = require("./routes/pdf"); app.use("/pdf", pdfRouter);
فرانتاند
اکنون که بکاند کامل شده است، میتوانیم به فرانتاند بپردازیم. با استفاده از اسکریپت Create React App اپلیکیشن React را ایجاد میکنیم. سپس دستور زیر را در پوشه root پروژه اجرا میکنیم:
npx create-react-app frontend
در ادامه پکیجهای خود را نصب میکنیم. از CKEditor به عنوان ادیتور متن استفاده خواهیم کرد. همچنین از Axios برای ایجاد درخواستهای HTTP، از Bootstrap برای استایلبندی، از MobX برای مدیریت ساده حالت، از React Router برای مسیریابی URL-ها به کامپوننتها و از Formik و Yup برای مدیریت مقادیر فرم و اعتبارسنجی آن استفاده خواهیم کرد.
پکیجهای فوق را با دستور زیر نصب میکنیم:
npm i @ckeditor/ckeditor5-build-classic @ckeditor/ckeditor5-react axios bootstrap formik mobx mobx-react react-bootstrap react-router-dom yup
زمانی که پکیجها نصب شدند، میتوانیم کار را شروع کنیم. در فایل App.js کد موجود را با کد زیر عوض میکنیم:
1import React from "react";
2import HomePage from "./HomePage";
3import { Router, Route } from "react-router-dom";
4import { createBrowserHistory as createHistory } from "history";
5import TopBar from "./TopBar";
6import { DocumentStore } from "./store";
7import "./App.css";
8const history = createHistory();
9const documentStore = new DocumentStore();
10function App() {
11 return (
12 <div className="App">
13 <Router history={history}>
14 <TopBar />
15 <Route
16 path="/"
17 exact
18 component={props => (
19 <HomePage {...props} documentStore={documentStore} />
20 )}
21 />
22 </Router>
23 </div>
24 );
25}
26export default App;
بدین ترتیب نوار فوقانی و مسیر به صفحه اصلی اضافه میشود. در فایل App.js کد موجود را با کد زیر عوض میکنیم:
1.page {
2 padding: 20px;
3}
4.content-invalid-feedback {
5 width: 100%;
6 margin-top: 0.25rem;
7 font-size: 80%;
8 color: #dc3545;
9}
10nav.navbar {
11 background-color: green !important;
12}
بدین ترتیب مقداری فاصلهبندی به صفحه اضافه شده و پیامهای اعتبارسنجی مربوط به ادیتور متنی استایلبندی شده و رنگ navbar نیز تغییر مییابد. سپس فرم را برای افزودن و ویرایش سندها ایجاد میکنیم. به این منظور یک فایل به نام DocumentForm.js در پوشه src ایجاد کرده و کد زیر را اضافه میکنیم:
1import React from "react";
2import * as yup from "yup";
3import Form from "react-bootstrap/Form";
4import Col from "react-bootstrap/Col";
5import Button from "react-bootstrap/Button";
6import { observer } from "mobx-react";
7import { Formik, Field } from "formik";
8import { addDocument, editDocument, getDocuments, APIURL } from "./request";
9import CKEditor from "@ckeditor/ckeditor5-react";
10import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
11const schema = yup.object({
12 name: yup.string().required("Name is required")
13});
14function DocumentForm({ documentStore, edit, onSave, doc }) {
15 const [content, setContent] = React.useState("");
16 const [dirty, setDirty] = React.useState(false);
17 const handleSubmit = async evt => {
18 const isValid = await schema.validate(evt);
19 if (!isValid || !content) {
20 return;
21 }
22 const data = { ...evt, document: content };
23 if (!edit) {
24 await addDocument(data);
25 } else {
26 await editDocument(data);
27 }
28 getAllDocuments();
29 };
30 const getAllDocuments = async () => {
31 const response = await getDocuments();
32 documentStore.setDocuments(response.data);
33 onSave();
34 };
35 return (
36 <>
37 <Formik
38 validationSchema={schema}
39 onSubmit={handleSubmit}
40 initialValues={edit ? doc : {}}
41 >
42 {({
43 handleSubmit,
44 handleChange,
45 handleBlur,
46 values,
47 touched,
48 isInvalid,
49 errors
50 }) => (
51 <Form noValidate onSubmit={handleSubmit}>
52 <Form.Row>
53 <Form.Group as={Col} md="12" controlId="name">
54 <Form.Label>Name</Form.Label>
55 <Form.Control
56 type="text"
57 name="name"
58 placeholder="Name"
59 value={values.name || ""}
60 onChange={handleChange}
61 isInvalid={touched.name && errors.name}
62 />
63 <Form.Control.Feedback type="invalid">
64 {errors.name}
65 </Form.Control.Feedback>
66 </Form.Group>
67 </Form.Row>
68 <Form.Row>
69 <Form.Group as={Col} md="12" controlId="content">
70 <Form.Label>Content</Form.Label>
71 <CKEditor
72 editor={ClassicEditor}
73 data={content || ""}
74 onInit={editor => {
75 if (edit) {
76 setContent(doc.document);
77 }
78 }}
79 onChange={(event, editor) => {
80 const data = editor.getData();
81 setContent(data);
82 setDirty(true);
83 }}
84 config={{
85 ckfinder: {
86 uploadUrl:
87 `${APIURL}/pdf/uploadImage`
88 }
89 }}
90 />
91 <div className="content-invalid-feedback">
92 {dirty && !content ? "Content is required" : null}
93 </div>
94 </Form.Group>
95 </Form.Row>
96 <Button type="submit" style={{ marginRight: 10 }}>
97 Save
98 </Button>
99 <Button type="button">Cancel</Button>
100 </Form>
101 )}
102 </Formik>
103 </>
104 );
105}
106export default observer(DocumentForm);
سپس From مربوط به React Bootstrap را درون کامپوننت Formik قرار میدهیم تا تابع مدیریت را از Formik دریافت کرده و مستقیماً در فیلدهای React Bootstrap مورد استفاده قرار دهیم.
این کار را در مورد CKEditor نمیتوانیم انجام دهیم، از این رو خودمان برخی «دستگیرههای فرم» (form handlers) را برای ادیتور متنی مینویسیم. میتوانیم prop به نام data را در CKEditor تنظیم کنیم تا مقدار ورودی ادیتور متنی تعیین شود. تابع onInit زمانی استفاده میشود که کاربران تلاش کنند یک سند موجود را ویرایش کنند، زیرا باید prop به نام data را با ادیتوری تنظیم کنیم که با اجرای دستور زیر مقداردهی میشود:
1setContent(doc.document);
prop به نام onChange تابع دستگیره برای تعیین content در زمان بهروزرسانی است و از این رو prop به نام data جدیدترین مقدار را خواهد داشت و در زمان کلیک کاربر روی Save تحویل میشود. ما از پلاگین CKFinder برای آپلود تصاویر استفاده میکنیم. برای این که آن را عملیاتی سازیم، باید URL آپلود تصویر را روی URL مسیر upload در بکاند تنظیم کنیم.
اسکیمای اعتبارسنجی فرم از سوی شیء schema در Yup عرضه شده است که در ابتدای کد ایجاد میشود. ما بررسی میکنیم آیا فیلد name وجود دارد یا نه.
تابع handleSubmit برای تحویل دادهها به بکاند مورد استفاده قرار میگیرد. ما هر دو شیء content و evt را بررسی میکنیم تا ببینیم آیا هر دو فیلد وجود دارند یا نه، زیرا نمیتوانیم از دستگیرههای فرم Formik مستقیماً در کامپوننت CKEditor استفاده کنیم.
اگر همه چیز معتبر باشد، بسته به این که prop به نام edit مقدار true دارد یا نه، یک سند جدید اضافه کرده یا سند موجود را ویرایش میکنیم. زمانی که ذخیرهسازی موفق باشد، getAllDocuments را فراخوانی میکنیم تا با دستور زیر جدیدترین سند را در استور MobX مقداردهی کنیم:
1documentStore.setDocuments(response.data);
سپس صفحه اصلی را با ایجاد فایل HomePage.js در پوشه src میسازیم:
1import React, { useState, useEffect } from "react";
2import { withRouter } from "react-router-dom";
3import DocumentForm from "./DocumentForm";
4import Modal from "react-bootstrap/Modal";
5import ButtonToolbar from "react-bootstrap/ButtonToolbar";
6import Button from "react-bootstrap/Button";
7import Table from "react-bootstrap/Table";
8import { observer } from "mobx-react";
9import { getDocuments, deleteDocument, generatePDF, APIURL } from "./request";
10function HomePage({ documentStore, history }) {
11 const [openAddModal, setOpenAddModal] = useState(false);
12 const [openEditModal, setOpenEditModal] = useState(false);
13 const [initialized, setInitialized] = useState(false);
14 const [doc, setDoc] = useState([]);
15 const openAddTemplateModal = () => {
16 setOpenAddModal(true);
17 };
18 const closeAddModal = () => {
19 setOpenAddModal(false);
20 setOpenEditModal(false);
21 };
22 const cancelAddModal = () => {
23 setOpenAddModal(false);
24 };
25 const cancelEditModal = () => {
26 setOpenEditModal(false);
27 };
28 const getAllDocuments = async () => {
29 const response = await getDocuments();
30 documentStore.setDocuments(response.data);
31 setInitialized(true);
32 };
33 const editTemplate = d => {
34 setDoc(d);
35 setOpenEditModal(true);
36 };
37 const onSave = () => {
38 cancelAddModal();
39 cancelEditModal();
40 };
41 const deleteSingleDocument = async id => {
42 await deleteDocument(id);
43 getAllDocuments();
44 };
45 const generateSinglePdf = async id => {
46 await generatePDF(id);
47 alert("PDF Generated");
48 getAllDocuments();
49 };
50 useEffect(() => {
51 if (!initialized) {
52 getAllDocuments();
53 }
54 });
55 return (
56 <div className="page">
57 <h1 className="text-center">Documents</h1>
58 <ButtonToolbar onClick={openAddTemplateModal}>
59 <Button variant="primary">Add Document</Button>
60 </ButtonToolbar>
61 <Modal show={openAddModal} onHide={closeAddModal}>
62 <Modal.Header closeButton>
63 <Modal.Title>Add Document</Modal.Title>
64 </Modal.Header>
65 <Modal.Body>
66 <DocumentForm
67 onSave={onSave.bind(this)}
68 cancelModal={cancelAddModal.bind(this)}
69 documentStore={documentStore}
70 />
71 </Modal.Body>
72 </Modal>
73 <Modal show={openEditModal} onHide={cancelEditModal}>
74 <Modal.Header closeButton>
75 <Modal.Title>Edit Document</Modal.Title>
76 </Modal.Header>
77 <Modal.Body>
78 <DocumentForm
79 edit={true}
80 doc={doc}
81 onSave={onSave.bind(this)}
82 cancelModal={cancelEditModal.bind(this)}
83 documentStore={documentStore}
84 />
85 </Modal.Body>
86 </Modal>
87 <br />
88 <Table striped bordered hover>
89 <thead>
90 <tr>
91 <th>Name</th>
92 <th>PDF</th>
93 <th>Generate PDF</th>
94 <th>Edit</th>
95 <th>Delete</th>
96 </tr>
97 </thead>
98 <tbody>
99 {documentStore.documents.map(d => {
100 return (
101 <tr key={d.id}>
102 <td>{d.name}</td>
103 <td>
104 <a href={`${APIURL}/${d.pdfPath}`} target="_blank">
105 Open
106 </a>
107 </td>
108 <td>
109 <Button
110 variant="outline-primary"
111 onClick={generateSinglePdf.bind(this, d.id)}
112 >
113 Generate PDF
114 </Button>
115 </td>
116 <td>
117 <Button
118 variant="outline-primary"
119 onClick={editTemplate.bind(this, d)}
120 >
121 Edit
122 </Button>
123 </td>
124 <td>
125 <Button
126 variant="outline-primary"
127 onClick={deleteSingleDocument.bind(this, d.id)}
128 >
129 Delete
130 </Button>
131 </td>
132 </tr>
133 );
134 })}
135 </tbody>
136 </Table>
137 </div>
138 );
139}
140export default withRouter(observer(HomePage));
ما یک جدول React Bootstrap برای لیستبندی سندها به همراه دکمههایی برای حذف یا ویرایش سندها داریم و یک دکمه نیز به تولید PDF اختصاص یافته است. ضمناً یک لینک ppen نیز برای باز کردن PDF در هر ردیف قرار دارد. همچنین دکمهای برای ایجاد PDF در بخش فوقانی جدول وجود دارد.
زمانی که صفحه بارگذاری میشود، getAllDocuments را فراخوانی میکنیم و آنها را در استور MobX مقداردهی میکنیم. سپس modal-های افزودن و ویرایش را به ترتیب با تابعهای openAddTemplateModal ،closeAddModal، cancelAddModal و cancelEditModal باز و بسته میکنیم. در ادامه فایل request.js را در پوشه src ایجاد کرده و کد زیر را به آن اضافه میکنیم:
1export const APIURL = "http://localhost:3000";
2const axios = require("axios");
3export const getDocuments = () => axios.get(`${APIURL}/pdf`);
4export const addDocument = data => axios.post(`${APIURL}/pdf`, data);
5export const editDocument = data => axios.put(`${APIURL}/pdf/${data.id}`, data);
6export const deleteDocument = id => axios.delete(`${APIURL}/pdf/${id}`);
7export const generatePDF = id => axios.get(`${APIURL}/pdf/generatePdf/${id}`);
بدین ترتیب تابعهای مورد نیاز برای ایجاد درخواست به مسیرهای بکاند اضافه میشوند. سپس دستور MobX را ایجاد میکنیم. به این منظور فایل store.js را در پوشه src ایجاد کرده و کد زیر را در آن قرار میدهیم:
1import { observable, action, decorate } from "mobx";
2class DocumentStore {
3 documents = [];
4setDocuments(documents) {
5 this.documents = documents;
6 }
7}
8DocumentStore = decorate(DocumentStore, {
9 documents: observable,
10 setDocuments: action
11});
12export { DocumentStore };
تابعی به نام setDocuments داریم که دادههای عکس را در استور قرار میدهد و در HomePage و DocumentForm مورد استفاده قرار میگیرد. ما آن را پیش از اکسپورت کردن، وهلهسازی میکنیم و از این رو تنها در یک محل باید با آن کار کنیم. قطعه کد زیر آرایه documents را در DocumentStore به صورت یک نهاد که تغییراتش میتواند از سوی کامپوننتها مورد نظارت قرار گیرد ایجاد میکند:
1DocumentStore = decorate(DocumentStore, {
2 documents: observable,
3 setDocuments: action
4});
تابع setDocuments به صورت تابعی ایجاد شده که میتواند برای تعیین آرایه documents در استور مورد استفاده قرار گیرد. سپس نوار فوقانی را با ایجاد فایل TopBar.js در پوشه src ایجاد کرده و کد زیر را در آن قرار میدهیم:
1import React from "react";
2import Navbar from "react-bootstrap/Navbar";
3import Nav from "react-bootstrap/Nav";
4import { withRouter } from "react-router-dom";
5function TopBar({ location }) {
6 return (
7 <Navbar bg="primary" expand="lg" variant="dark">
8 <Navbar.Brand href="#home">PDF App</Navbar.Brand>
9 <Navbar.Toggle aria-controls="basic-navbar-nav" />
10 <Navbar.Collapse id="basic-navbar-nav">
11 <Nav className="mr-auto">
12 <Nav.Link href="/" active={location.pathname == "/"}>
13 Home
14 </Nav.Link>
15 </Nav>
16 </Navbar.Collapse>
17 </Navbar>
18 );
19}
20export default withRouter(TopBar);
کد فوق شامل یک Navbar به صورت React Bootstrap است که نوار فوقانی را با لینکی به صفحه اصلی و نام اپلیکیشن نمایش میدهد. ما آن را تنها با token ارائه شده در حافظه لوکال نمایش میدهیم. سپس pathname را بررسی میکنیم تا لینکهای صحیح را با تعیین prop به نام active هایلایت کنیم. سپس در فایل index.html کد موجود را با کد زیر عوض میکنیم:
1<!DOCTYPE html>
2<html lang="en">
3 <head>
4 <meta charset="utf-8" />
5 <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
6 <meta name="viewport" content="width=device-width, initial-scale=1" />
7 <meta name="theme-color" content="#000000" />
8 <meta
9 name="description"
10 content="Web site created using create-react-app"
11 />
12 <link rel="apple-touch-icon" href="logo192.png" />
13 <!--
14 manifest.json provides metadata used when your web app is installed on a
15 user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
16 -->
17 <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
18 <!--
19 Notice the use of %PUBLIC_URL% in the tags above.
20 It will be replaced with the URL of the `public` folder during the build.
21 Only files inside the `public` folder can be referenced from the HTML.
22Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
23 work correctly both with client-side routing and a non-root public URL.
24 Learn how to configure a non-root public URL by running `npm run build`.
25 -->
26 <title>PDF App</title>
27 <link
28 rel="stylesheet"
29 href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
30 integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
31 crossorigin="anonymous"
32 />
33 </head>
34 <body>
35 <noscript>You need to enable JavaScript to run this app.</noscript>
36 <div id="root"></div>
37 <!--
38 This HTML file is a template.
39 If you open it directly in the browser, you will see an empty page.
40You can add webfonts, meta tags, or analytics to this file.
41 The build step will place the bundled scripts into the <body> tag.
42To begin the development, run `npm start` or `yarn start`.
43 To create a production bundle, use `npm run build` or `yarn build`.
44 -->
45 </body>
46</html>
بدین ترتیب Bootstrap CSS را اضافه میکنیم و عنوان را تغییر میدهیم. پس از نوشتن همه این کدها میتوانیم اپلیکیشن را اجرا کنیم. پیش از اجرای هر چیز، ابتدا nodemon را با اجرای دستور زیر نصب میکنیم:
npm i -g nodemon
بدین ترتیب دیگر لازم نیست در زمان تغییر یافتن فایلها، بکاند را به صورت دستی ریاستارت کنیم. سپس بکاند را با اجرای دستور npm start در پوشه backend و همچنین اجرای دستور npm start در پوشه frontend آغاز میکنیم. در ادامه اگر از شما سؤال شود میخواهید آن را از پورت متفاوتی اجرا کنید، گزینه yes را انتخاب کنید. بدین ترتیب نتیجهای مانند تصویر زیر به دست میآید:
بدین ترتیب به پایان این راهنما میرسیم.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی جاوا اسکریپت
- مجموعه آموزشهای برنامهنویسی
- آموزش Node.js — مجموعه مقالات مجله فرادرس
- استفاده از الگوی طراحی سلکتور در Node.js — از صفر تا صد
- آموزش Node.js: آشنایی با استریم ها و کار با MySQL — بخش دوازدهم
==