У вас включен AdBlock или иной блокировщик рекламы.

Пожалуйста, отключите его, доход от рекламы помогает развитию сайта и появлению новых статей.

Спасибо за понимание.

В другой раз
DevGang блог о програмировании
Авторизоваться

Как создавать документы Word с помощью Node.js 

В этой статье мы создадим приложение, которое позволит пользователям вводить свой данные в текстовом редакторе и генерировать из него документ Word. Мы будем использовать Express для внутреннего интерфейса и React для внешнего интерфейса.

Back End

Мы начнем с серверной части. Для начала мы создадим папку проекта с папкой backend внутри. Затем в папке backend запустите, npx express-generator чтобы создать приложение Express. Затем запустите npm i для установки пакетов. Далее мы устанавливаем наши собственные пакеты. Нам нужен Babel для запуска приложения с последней версией JavaScript, CORS для междоменных запросов, HTML-DOCX-JS для преобразования строк HTML в документы Word, Multer для загрузки файлов, Sequelize для ORM и SQLite3 для нашей базы данных.

Мы устанавливаем все это, выполнив:

npm i @babel/cli @babel/core @babel/node @babel/preset-env cors html-docx-js sequelize sqlite3 multer

После этого мы изменим package.json чтобы добавить команды start и babel-node:

"start": "nodemon --exec npm run babel-node - ./bin/www", 
"babel-node": "babel-node"

Таким образом мы запускаем наше приложение с Babel вместо обычной среды выполнения Node.

Затем создайте файл .babelrc в папке backend и добавьте:

{
    "presets": [
        "@babel/preset-env"
    ]
}

чтобы указать, что мы запускаем наше приложение с последней версией JavaScript.

Далее мы добавляем код нашей базы данных. Запустите npx sequelize-cli init в папке backend, чтобы создать код Sequelize.

В config.js добавьте:

{
  "development": {
    "dialect": "sqlite",
    "storage": "development.db"
  },
  "test": {
    "dialect": "sqlite",
    "storage": "test.db"
  },
  "production": {
    "dialect": "sqlite",
    "storage": "production.db"
  }
}

Затем создайте нашу модель и выполните миграцию, запустив:

npx sequelize-cli model:create --name Document --attributes name:string,document:text,documentPath:string

создать модель Document и таблицу Documents.

Затем мы запускаем:

npx sequelize-cli db:migrate

Далее мы создаем наши маршруты. Создайте файл document.js в папке routes и добавьте:

var express = require("express");
const models = require("../models");
var multer = require("multer");
const fs = require("fs");
var router = express.Router();
const htmlDocx = require("html-docx-js");
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "./files");
  },
  filename: (req, file, cb) => {
    cb(null, `${file.fieldname}_${+new Date()}.jpg`);
  }
});
const upload = multer({
  storage
});
router.get("/", async (req, res, next) => {
  const documents = await models.Document.findAll();
  res.json(documents);
});
router.post("/", async (req, res, next) => {
  const document = await models.Document.create(req.body);
  res.json(document);
});
router.put("/:id", async (req, res, next) => {
  const id = req.params.id;
  const { name, document } = req.body;
  const doc = await models.Document.update(
    { name, document },
    { where: { id } }
  );
  res.json(doc);
});
router.delete("/:id", async (req, res, next) => {
  const id = req.params.id;
  await models.Document.destroy({ where: { id } });
  res.json({});
});
router.get("/generate/:id", async (req, res, next) => {
  const id = req.params.id;
  const documents = await models.Document.findAll({ where: { id } });
  const document = documents[0];
  const converted = htmlDocx.asBlob(document.document);
  const fileName = `${+new Date()}.docx`;
  const documentPath = `${__dirname}/../files/${fileName}`;
  await new Promise((resolve, reject) => {
    fs.writeFile(documentPath, converted, err => {
      if (err) {
        reject(err);
        return;
      }
      resolve();
    });
  });
  const doc = await models.Document.update(
    { documentPath: fileName },
    { where: { id } }
  );
  res.json(doc);
});
router.post("/uploadImage", upload.single("upload"), async (req, res, next) => {
  res.json({
    uploaded: true,
    url: `${process.env.BASE_URL}/${req.file.filename}`
  });
});
module.exports = router;

Мы выполняем стандартные операции CRUD для таблицы Documents в первых 4 маршрутах. У нас есть GET для получения всех Documents, POST для создания Document, PUT для обновления Document по ID, DELETE для удаления Document путем поиска по ID. У нас есть HTML в поле document для создания документа Word позже.

Маршрут generate для создания документа Word. Мы получаем идентификатор из URL, а затем используем пакет HTML-DOCX-JS для создания документа Word. Мы генерируем документ Word путем преобразования документа HTML в объект файлового потока с помощью пакета HTML-DOCX-JS, а затем записываем поток в файл и сохраняем путь к файлу в Document с идентификатором в параметре URL.

У нас также есть маршрут uploadImage, позволяющий пользователю загружать изображения с помощью CKEditor с помощью плагина CKFinder. Плагин ожидает uploaded и url в ответе, поэтому мы возвращаем их.

Затем нам нужно добавить папку files в backend.

Далее в app.js мы заменим существующий код на:

require("dotenv").config();
var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
var cors = require("cors");
var indexRouter = require("./routes/index");
var documentRouter = require("./routes/document");
var app = express();
// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use(express.static(path.join(__dirname, "files")));
app.use(cors());
app.use("/", indexRouter);
app.use("/document", documentRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
  res.status(err.status || 500);
  res.render("error");
});
module.exports = app;

Мы открываем папку с файлом:

app.use(express.static(path.join(__dirname, "files")));

и устанавливаем маршрут document с помощью:

var documentRouter = require("./routes/document");
app.use("/document", documentRouter);

Внешний интерфейс

Теперь наше API готово, мы можем перейти к работе с интерфейсом. Создайте приложение React с помощью команды «Create React App». Запускаем 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 на:

import React from "react";
import HomePage from "./HomePage";
import { Router, Route } from "react-router-dom";
import { createBrowserHistory as createHistory } from "history";
import TopBar from "./TopBar";
import { DocumentStore } from "./store";
import "./App.css";
const history = createHistory();
const documentStore = new DocumentStore();
function App() {
  return (
    <div className="App">
      <Router history={history}>
        <TopBar />
        <Route
          path="/"
          exact
          component={props => (
            <HomePage {...props} documentStore={documentStore} />
          )}
        />
      </Router>
    </div>
  );
}
export default App;

добавить наш верхний бар и маршрут к домашней странице.

В App.css, мы заменим существующий код на:

.page {
  padding: 20px;
}
.content-invalid-feedback {
  width: 100%;
  margin-top: 0.25rem;
  font-size: 80%;
  color: #dc3545;
}
nav.navbar {
  background-color: green !important;
}

добавить некоторые отступы на нашу страницу и стилизовать сообщение проверки для редактора Rich text, а также изменить цвет navbar.

Далее мы создаем форму для добавления и редактирования документов. Создайте файл DocumentForm.js в src и добавьте:

import React from "react";
import * as yup from "yup";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { observer } from "mobx-react";
import { Formik, Field } from "formik";
import { addDocument, editDocument, getDocuments, APIURL } from "./request";
import CKEditor from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
const schema = yup.object({
  name: yup.string().required("Name is required")
});
function DocumentForm({ documentStore, edit, onSave, doc }) {
  const [content, setContent] = React.useState("");
  const [dirty, setDirty] = React.useState(false);
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid || !content) {
      return;
    }
    const data = { ...evt, document: content };
    if (!edit) {
      await addDocument(data);
    } else {
      await editDocument(data);
    }
    getAllDocuments();
  };
  const getAllDocuments = async () => {
    const response = await getDocuments();
    documentStore.setDocuments(response.data);
    onSave();
  };
  return (
    <>
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
        initialValues={edit ? doc : {}}
      >
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="name">
                <Form.Label>Name</Form.Label>
                <Form.Control
                  type="text"
                  name="name"
                  placeholder="Name"
                  value={values.name || ""}
                  onChange={handleChange}
                  isInvalid={touched.name && errors.name}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.name}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="content">
                <Form.Label>Content</Form.Label>
                <CKEditor
                  editor={ClassicEditor}
                  data={content || ""}
                  onInit={editor => {
                    if (edit) {
                      setContent(doc.document);
                    }
                  }}
                  onChange={(event, editor) => {
                    const data = editor.getData();
                    setContent(data);
                    setDirty(true);
                  }}
                  config={{
                    ckfinder: {
                      uploadUrl:
                        `${APIURL}/document/uploadImage`
                    }
                  }}
                />
                <div className="content-invalid-feedback">
                  {dirty && !content ? "Content is required" : null}
                </div>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: 10 }}>
              Save
            </Button>
            <Button type="button">Cancel</Button>
          </Form>
        )}
      </Formik>
    </>
  );
}
export default observer(DocumentForm);

Мы обертываем нашу React Bootstrap Form в компоненту Formik, чтобы получить функцию обработки формы от Formik, которую мы используем непосредственно в полях формы React Bootstrap. Мы не можем сделать то же самое с CKEditor, поэтому мы пишем свои собственные обработчики форм для редактора форматированного текста. Мы устанавливаем data в, CKEditor чтобы установить значение ввода редактора форматированного текста. Эта функция onInit используется, когда пользователи пытаются редактировать существующий документ, поскольку мы должны установить параметр data с помощью редактора, который инициализируется при запуске setContent(doc.document);. Метод onChange является функция обработчика для установки content всякий раз, когда он обновляется, так что параметр data будет иметь последнее значение, которое мы будем представлять, когда пользователь нажимает кнопку Сохранить.

Мы используем плагин CKFinder для загрузки изображений. Чтобы это работало, мы устанавливаем URL загрузки изображения на URL маршрута загрузки в нашем бэкэнде.

Схема проверки формы предоставляется объектом Yup schema, который мы создаем в верхней части кода. Мы проверяем, заполнено ли поле name.

Функция handleSubmit предназначена для обработки представления данных в заднюю часть. Мы проверяем оба объекта content и evt, чтобы проверить оба поля, поскольку мы не можем включить обработчики форм Formik непосредственно в CKEditor компонент.

Если все верно, то мы добавляем новый документ или обновляем его в зависимости от того, является ли реквизит верным или нет.

Затем, когда сохранение getAllDocuments прошло успешно, мы вызываем, чтобы заполнить последние документы в нашем хранилище MobX, запустив documentStore.setDocuments(response.data);.

Далее мы делаем нашу домашнюю страницу, создав HomePage.js в папке src и добавив:

import React, { useState, useEffect } from "react";
import { withRouter } from "react-router-dom";
import DocumentForm from "./DocumentForm";
import Modal from "react-bootstrap/Modal";
import ButtonToolbar from "react-bootstrap/ButtonToolbar";
import Button from "react-bootstrap/Button";
import Table from "react-bootstrap/Table";
import { observer } from "mobx-react";
import { getDocuments, deleteDocument, generateDocument, APIURL } from "./request";
function HomePage({ documentStore, history }) {
  const [openAddModal, setOpenAddModal] = useState(false);
  const [openEditModal, setOpenEditModal] = useState(false);
  const [initialized, setInitialized] = useState(false);
  const [doc, setDoc] = useState([]);
const openAddTemplateModal = () => {
    setOpenAddModal(true);
  };
const closeAddModal = () => {
    setOpenAddModal(false);
    setOpenEditModal(false);
  };
const cancelAddModal = () => {
    setOpenAddModal(false);
  };
const cancelEditModal = () => {
    setOpenEditModal(false);
  };
const getAllDocuments = async () => {
    const response = await getDocuments();
    documentStore.setDocuments(response.data);
    setInitialized(true);
  };
const editDocument = d => {
    setDoc(d);
    setOpenEditModal(true);
  };
const onSave = () => {
    cancelAddModal();
    cancelEditModal();
  };
const deleteSingleDocument = async id => {
    await deleteDocument(id);
    getAllDocuments();
  };
const generateSingleDocument = async id => {
    await generateDocument(id);
    alert("Document Generated");
    getAllDocuments();
  };
useEffect(() => {
    if (!initialized) {
      getAllDocuments();
    }
  });
return (
    <div className="page">
      <h1 className="text-center">Documents</h1>
      <ButtonToolbar onClick={openAddTemplateModal}>
        <Button variant="primary">Add Document</Button>
      </ButtonToolbar>
<Modal show={openAddModal} onHide={closeAddModal}>
        <Modal.Header closeButton>
          <Modal.Title>Add Document</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <DocumentForm
            onSave={onSave.bind(this)}
            cancelModal={cancelAddModal.bind(this)}
            documentStore={documentStore}
          />
        </Modal.Body>
      </Modal>
<Modal show={openEditModal} onHide={cancelEditModal}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Document</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <DocumentForm
            edit={true}
            doc={doc}
            onSave={onSave.bind(this)}
            cancelModal={cancelEditModal.bind(this)}
            documentStore={documentStore}
          />
        </Modal.Body>
      </Modal>
      <br />
      <Table striped bordered hover>
        <thead>
          <tr>
            <th>Name</th>
            <th>Document</th>
            <th>Generate Document</th>
            <th>Edit</th>
            <th>Delete</th>
          </tr>
        </thead>
        <tbody>
          {documentStore.documents.map(d => {
            return (
              <tr key={d.id}>
                <td>{d.name}</td>
                <td>
                  <a href={`${APIURL}/${d.documentPath}`} target="_blank">
                    Open
                  </a>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={generateSingleDocument.bind(this, d.id)}
                  >
                    Generate Document
                  </Button>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={editDocument.bind(this, d)}
                  >
                    Edit
                  </Button>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={deleteSingleDocument.bind(this, d.id)}
                  >
                    Delete
                  </Button>
                </td>
              </tr>
            );
          })}
        </tbody>
      </Table>
    </div>
  );
}
export default withRouter(observer(HomePage));

У нас есть таблица React Bootstrap для перечисления документов с кнопками для редактирования, удаления документов и создания документа Word. Кроме того, в каждой строке есть ссылка Open для открытия документа Word. У нас есть кнопка создания в верхней части таблицы.

Когда страница загружается, мы вызываем getAllDocuments и заполняем их в хранилище MobX. 

Далее создайте request.js в папке src и добавьте:

export const APIURL = "http://localhost:3000";
const axios = require("axios");

export const getDocuments = () => axios.get(`${APIURL}/document`);

export const addDocument = data => axios.post(`${APIURL}/document`, data);
export const editDocument = data => axios.put(`${APIURL}/document/${data.id}`, data);

export const deleteDocument = id => axios.delete(`${APIURL}/document/${id}`);
export const generateDocument = id => axios.get(`${APIURL}/document/generate/${id}`);

Добавив функции для отправки запросов к нашим маршрутам в серверной части.

Затем мы создаем наше хранилище MobX. Создайте store.js в папке src и добавьте:

import { observable, action, decorate } from "mobx";

class DocumentStore {
  documents = [];
  setDocuments(documents) {
    this.documents = documents;
  }
}

DocumentStore = decorate(DocumentStore, {
  documents: observable,
  setDocuments: action
});

export { DocumentStore };

Мы имеем функцию, setDocuments чтобы поместить данные в хранилище, который мы использовали в HomePage и DocumentForm и мы инстанцировали его перед экспортом , так что мы должны сделать это только в одном месте.

Этот блок:

DocumentStore = decorate(DocumentStore, {
  documents: observable,
  setDocuments: action
});

обозначает массив documents в DocumentStore в качестве объекта, который может отслеживаться компонентами на предмет изменений. setDocuments обозначается как функция , которая может быть использована для установки массива documents в хранилище.

Затем мы создаем верхнюю панель, создав файл TopBar.js в папке src и добавив:

import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import { withRouter } from "react-router-dom";

function TopBar({ location }) {
  return (
    <Navbar bg="primary" expand="lg" variant="dark">
      <Navbar.Brand href="#home">Word App</Navbar.Brand>
      <Navbar.Toggle aria-controls="basic-navbar-nav" />
      <Navbar.Collapse id="basic-navbar-nav">
        <Nav className="mr-auto">
          <Nav.Link href="/" active={location.pathname == "/"}>
            Home
          </Nav.Link>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}

export default withRouter(TopBar);

Это содержит Reac Bootstrap Navbar чтобы показать верхнюю панель со ссылкой на главную страницу и имя приложения. Мы показываем ее только если существует token. Также мы проверяем pathname чтобы выделить правильные ссылки, установив параметр active.

Далее в index.html мы заменим существующий код на:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Word App</title>
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

После написания всего этого кода мы можем запустить наше приложение. Прежде чем что-либо запускать, установите nodemon, запустив, npm i -g nodemon чтобы нам не приходилось перезагружать сервер при изменении файлов.

Затем запустите back end, запустив команду npm start в папке backend и npm start в папке frontend, затем выберите «yes», если вас попросят запустить его с другого порта.

Тогда вы получите:

#JavaScript #NodeJS #React
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться