ایجاد اسناد ورد با Node.js – از صفر تا صد


نرمافزار مایکروسافت ورد یک واژهپرداز قدرتمند است. این نرمافزار، فرمت رایجی برای ساخت و مبادله اسناد به صورت doc یا docx ارائه کرده است. یکی از قابلیتهای بسیاری از اپلیکیشنهای مرتبط با تولید و پردازش اسناد، امکان ایجاد اسناد ورد از انواع مختلف اسناد است. ایجاد اسناد ورد با Node.js با بهرهگیری از کتابخانههای شخص ثالث کار آسانی است.
در این مقاله اپلیکیشنی طراحی میکنیم که به کاربران امکان میدهد اسناد خود را در یک ویرایشگر متنی وارد کرده و از روی آن اسناد ورد بسازند. بکاند این اپلیکیشن با Express و فرانتاند آن با React طراحی میشود.
بخش بکاند
کار خود را از بکاند آغاز میکنیم.
برای شروع یک پوشه پروژه میسازیم که پوشه backend درون آن قرار دارد. سپس در پوشه backend دستور زیر را اجرا میکنیم تا یک اپلیکیشن اکسپرس ایجاد شود:
npx express-generator
سپس دستور npm i را اجرا میکنیم تا پکیجها نصب شوند و بعد پکیجهای خود را نصب میکنیم. ما به Babel برای اجرای اپلیکیشن با جدیدترین نسخه جاوا اسکریپت نیاز داریم، CORS برای درخواستهای بین دامنهای در فرانتاند، HTML-DOCX-JS برای تبدیل رشتههای HTML به اسناد ورد، Multer برای آپلود فایل، Sequelize برای ORM و در نهایت SQLite3 برای پایگاه داده مورد استفاده قرار خواهند گرفت.
همه این موارد را با اجرای دستور زیر نصب میکنیم:
npm i @babel/cli @babel/core @babel/node @babel/preset-env cors html-docx-js sequelize sqlite3 multer
پس از آن، بخش scripts فایل package.json را به صورت زیر تغییر میدهیم:
"start": "nodemon --exec npm run babel-node --./bin/www", "babel-node": "babel-node"
بدین ترتیب اپلیکیشن خود را به جای محیط اجرای معمول Node با استفاده از Babel اجرا میکنیم. سپس یک فایل .babelrc در پوشه backend ساخته و کد زیر را به آن اضافه میکنیم:
{ "presets": [ "@babel/preset-env" ] }
بدین ترتیب اپلیکیشنمان میتواند با آخرین نسخه از جاوا اسکریپت اجرا شود. سپس کد پایگاه داده خود را اضافه میکنیم. به این منظور دستور زیر را در پوشه backend اجرا کنید تا کد Sequelize ایجاد شود:
npx sequelize-cli init
اینک باید فایل config.js را در ساختار پروژه خود داشته باشیم. کد زیر را به این فایل اضافه میکنیم:
{ "development": { "dialect": "sqlite", "storage": "development.db" }, "test": { "dialect": "sqlite", "storage": "test.db" }, "production": { "dialect": "sqlite", "storage": "production.db" } }
کد فوق تعیین میکند که از SQLite به عنوان پایگاه داده استفاده خواهیم کرد. سپس مدل و migration را با اجرای دستور زیر ایجاد میکنیم. این دستور برای ایجاد یک مدل Document و جدول Documents استفاده میشود.
npx sequelize-cli model:create --name Document --attributes name:string,document:text,documentPath:string
برای ایجاد پایگاه داده دستور زیر را اجرا کنید:
npx sequelize-cli db:migrate
سپس مسیرها (routes) را ایجاد میکنیم. به این منظور یک فایل به نام document.js در پوشه routes ساخته و کد زیر را به آن اضافه کنید:
1var express = require("express");
2const models = require("../models");
3var multer = require("multer");
4const fs = require("fs");
5var router = express.Router();
6const htmlDocx = require("html-docx-js");
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("/generate/: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 converted = htmlDocx.asBlob(document.document);
45 const fileName = `${+new Date()}.docx`;
46 const documentPath = `${__dirname}/../files/${fileName}`;
47 await new Promise((resolve, reject) => {
48 fs.writeFile(documentPath, converted, err => {
49 if (err) {
50 reject(err);
51 return;
52 }
53 resolve();
54 });
55 });
56 const doc = await models.Document.update(
57 { documentPath: 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;
در 4 مسیر نخست، عملیات استاندارد CRUD را روی جدول Documents اجرا میکنیم. برای دریافت همه Document-ها عملیات GET را داریم. عملیات POST برای ایجاد یک Document از پارامترها استفاده میشود. عملیات PUT برای بهروزرسانی Document بر اساس ID مورد استفاده قرار میگیرد و عملیات DELETE برای حذف کردن یک Document بر اساس ID آن استفاده میشود. کد HTML را در فیلد document برای تولید سند ورد در ادامه خواهیم داشت.
مسیر generate برای تولید سند ورد استفاده میشود. بدین ترتیب ID از URL به دست میآید و سپس از پکیج HTML-DOCX-JS برای تولید سند ورد استفاده میشود. اسناد ورد با تبدیل سند HTML به یک شیء استریم فایل با پکیج HTML-DOCX-JS و سپس نوشتن استریم در یک فایل و ذخیره مسیر فایل در مدخل Document به همراه ID در پارامتر URL تولید میشوند.
همچنین یک مسیر uploadImage داریم که به کاربر امکان میدهد تا تصاویر را با افزونه CKEditor و CKFinder آپلود کند. این افزونه در پاسخ منتظر uploaded و url است و لذا این موارد را بازگشت میدهیم.
سپس باید یک پوشه به نام files به پوشه backend اضافه کنیم. در ادامه در فایل app.js کد موجود را با کد زیر عوض میکنیم:
1require("dotenv").config();
2var createError = require("http-errors");
3var express = require("express");
4var path = require("path");
5var cookieParser = require("cookie-parser");
6var logger = require("morgan");
7var cors = require("cors");
8var indexRouter = require("./routes/index");
9var documentRouter = require("./routes/document");
10var app = express();
11// view engine setup
12app.set("views", path.join(__dirname, "views"));
13app.set("view engine", "jade");
14app.use(logger("dev"));
15app.use(express.json());
16app.use(express.urlencoded({ extended: false }));
17app.use(cookieParser());
18app.use(express.static(path.join(__dirname, "public")));
19app.use(express.static(path.join(__dirname, "files")));
20app.use(cors());
21app.use("/", indexRouter);
22app.use("/document", documentRouter);
23// catch 404 and forward to error handler
24app.use(function(req, res, next) {
25 next(createError(404));
26});
27// error handler
28app.use(function(err, req, res, next) {
29 // set locals, only providing error in development
30 res.locals.message = err.message;
31 res.locals.error = req.app.get("env") === "development" ? err : {};
32// render the error page
33 res.status(err.status || 500);
34 res.render("error");
35});
36module.exports = app;
پوشه فایل را با دستور زیر عرضه میکنیم:
1app.use(express.static(path.join(__dirname, "files")));
و مسیر document را نیز با دستور زیر عرضه میکنیم:
1var documentRouter = require("./routes/document");
2app.use("/document", documentRouter);
بخش فرانتاند
اکنون کار ساخت بکاند پایان یافته است و میتوانیم به فرانتاند اپلیکیشن خود بپردازیم.
یک اپلیکیشن ریاکت با اجرای اسکریپت Create React App بسازید. دستور زیر را در پوشه 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.css کد موجود را با کد زیر عوض میکنیم:
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}
بدین ترتیب مقداری padding به صفحه اضافه میشود و پیام اعتبارسنجی برای ویرایشگر متنی استایلبندی میشود. همچنین رنگ 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}/document/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);
ما Form بوتاسترپ خود را درون یک کامپوننت Formik قرار میدهیم تا کارکرد مدیریت فرم را از طریق Formik به دست آوریم که مستقیماً در فیلدهای فرم بوتاسترپ ریاکت استفاده میکنیم. این کار را در مورد افزونه CKEditor نمیتوانیم انجام دهیم، از این رو «دستگیرههای فرم» (Form Handlers) خاص خود را برای ویرایشگر متنی مینویسیم. مقدار prop به نام data را برابر با مقدار ورودی از ویرایشگر متنی قرار میدهیم. تابع onInit زمانی استفاده میشود که کاربران تلاش میکنند، سند موجود را ویرایش کنند. از آنجا باید prop به نام data را با ویرایشگر تعیین کنیم، آن را با اجرای دستور زیر مقداردهی میکنیم:
1setContent(doc.document);
prop به نام onChange یک تابع handler برای تعیین content در زمانهای بهروزرسانی شدن است. از این رو prop به نام data آخرین مقدار را خواهد داشت که وقتی کاربر روی Save کلیک میکند تحویل میشود. از افزونه CKFinder برای آپلود تصاویر استفاده میکنیم. برای عملیاتی ساختن آن باید URL تصویر آپلود شونده را به URL مسیر آپلود در بکاند تنظیم کنیم. اسکیمای اعتبارسنجی فرم از سوی شیء schema در Yup عرضه میشود که در ابتدای کد ایجاد شده است. بررسی میکنیم که آیا فیلد name پر شده است یا نه.
تابع handleSubmit فرایند تحویل دادهها به بکاند را مدیریت میکند. هر دو شیء content و evt را بررسی میکنیم تا در مورد هر دو فیلد مطمئن شویم، زیرا نمیتوانیم از Formik مربوط به دستگیرههای فرم مستقیماً درون کامپوننت CKEditor استفاده کنیم. اگر همه چیز معتبر باشد، در این صورت میتوانیم به یک سند جدید اشاره کنیم و آن را بسته به این که prop به نام edit مقدار true یا false دارد، بهروزرسانی کنیم. سپس هنگامی که ذخیره سند موفق باشد، 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, generateDocument, 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([]);
15const openAddTemplateModal = () => {
16 setOpenAddModal(true);
17 };
18const closeAddModal = () => {
19 setOpenAddModal(false);
20 setOpenEditModal(false);
21 };
22const cancelAddModal = () => {
23 setOpenAddModal(false);
24 };
25const cancelEditModal = () => {
26 setOpenEditModal(false);
27 };
28const getAllDocuments = async () => {
29 const response = await getDocuments();
30 documentStore.setDocuments(response.data);
31 setInitialized(true);
32 };
33const editDocument = d => {
34 setDoc(d);
35 setOpenEditModal(true);
36 };
37const onSave = () => {
38 cancelAddModal();
39 cancelEditModal();
40 };
41const deleteSingleDocument = async id => {
42 await deleteDocument(id);
43 getAllDocuments();
44 };
45const generateSingleDocument = async id => {
46 await generateDocument(id);
47 alert("Document Generated");
48 getAllDocuments();
49 };
50useEffect(() => {
51 if (!initialized) {
52 getAllDocuments();
53 }
54 });
55return (
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>Document</th>
93 <th>Generate Document</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.documentPath}`} target="_blank">
105 Open
106 </a>
107 </td>
108 <td>
109 <Button
110 variant="outline-primary"
111 onClick={generateSingleDocument.bind(this, d.id)}
112 >
113 Generate Document
114 </Button>
115 </td>
116 <td>
117 <Button
118 variant="outline-primary"
119 onClick={editDocument.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 برای لیستبندی سندها به همراه دکمههایی برای ویرایش و حذف اسناد و تولید سند ورد داریم. ضمناً یک لینک Open برای باز کردن سند ورد در هر سطر وجود دارد. ما باید یک دکمه در ابتدای جدول ایجاد کنیم.
زمانی که صفحه بارگذاری میشود، getAllDocuments را فراخوانی کرده و موارد موجود را در استور MobX قرار میدهیم. کادرهای محاورهای add و edit را با تابعهای openAddTemplateModal, closeAddModal, cancelAddModal و cancelEditModal باز و بسته میکنیم. سپس فایل request.js را در پوشه src ایجاد کرده و کد زیر را به آن اضافه میکنیم:
1export const APIURL = "http://localhost:3000";
2const axios = require("axios");
3export const getDocuments = () => axios.get(`${APIURL}/document`);
4export const addDocument = data => axios.post(`${APIURL}/document`, data);
5export const editDocument = data => axios.put(`${APIURL}/document/${data.id}`, data);
6export const deleteDocument = id => axios.delete(`${APIURL}/document/${id}`);
7export const generateDocument = id => axios.get(`${APIURL}/document/generate/${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 اختصاص دارد که به عنوان مدخلی است که میتواند از سوی کامپوننتها تحت نظر قرار گیرد تا به تغییرها واکنش نشان دهند. تابع setDocuments به عنوان تابعی است که میتواند برای تعیین آرایه documents در store مورد استفاده قرار گیرد:
1DocumentStore = decorate(DocumentStore, {
2 documents: observable,
3 setDocuments: action
4});
سپس نوار فوقانی را با ایجاد فایل 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">Word 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 بوتاسترپ ریاکت برای نمایش نوار فوقانی به همراه لینکی به صفحه اصلی و نام اپلیکیشن است. ما آن را تنها زمانی نمایش میدهیم که token در local storage موجود باشد. 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>Word 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>
این کد CSS بوتاسترپ را اضافه میکند و عنوان را تغییر میدهد. پس از نوشتن همه این کدها میتوانیم اپلیکیشن خود را اجرا کنیم. پیش از اجرای اپلیکیشن باید nodemon را با اجرای دستور زیر نصب کنیم تا مجبور نباشیم در زمان تغییر یافتن فایلها، بکاند را به صورت دستی ریاستارت کنیم:
npm i -g nodemon
سپس بکاند را با اجرای دستور npm start در پوشه backend و دستور npm start در پوشه frontend فعال کنید و در ادامه اگر در مورد اجرای آن از پورت متفاوت سؤال شود، گزینه yes را انتخاب کنید. در نهایت اپلیکیشنی مانند تصاویر زیر به دست میآوریم:
بدین ترتیب به پایان این مقاله رسیدیم. اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- استریم و بافر در Node.js — به زبان ساده
- آموزش Node.js: آشنایی با استریم ها و کار با MySQL — بخش دوازدهم
- استفاده از الگوی طراحی سلکتور در Node.js — از صفر تا صد
==