Как создавать документы Word с помощью Node.js
В этой статье мы создадим приложение, которое позволит пользователям вводить свой данные в текстовом редакторе и генерировать из него документ Word. Мы будем использовать Express для внутреннего интерфейса и React для внешнего интерфейса.
Back End
Мы начнем с серверной части. Для начала мы создадим папку проекта с папкой
внутри. Затем в папке backend
запустите, backend
чтобы создать приложение Express. Затем запустите npx express-generator
для установки пакетов. Далее мы устанавливаем наши собственные пакеты. Нам нужен Babel для запуска приложения с последней версией JavaScript, CORS для междоменных запросов, HTML-DOCX-JS для преобразования строк HTML в документы Word, Multer для загрузки файлов, Sequelize для ORM и SQLite3 для нашей базы данных.npm i
Мы устанавливаем все это, выполнив:
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
, чтобы создать код Sequelize.backend
В
добавьте: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 для таблицы
в первых 4 маршрутах. У нас есть GET для получения всех Documents
, POST для создания Documents
, PUT для обновления Document
по ID, DELETE для удаления Document
путем поиска по ID. У нас есть HTML в поле Document
для создания документа Word позже.document
Маршрут generate
для создания документа Word. Мы получаем идентификатор из URL, а затем используем пакет HTML-DOCX-JS для создания документа Word. Мы генерируем документ Word путем преобразования документа HTML в объект файлового потока с помощью пакета HTML-DOCX-JS, а затем записываем поток в файл и сохраняем путь к файлу в
с идентификатором в параметре URL.Document
У нас также есть маршрут
, позволяющий пользователю загружать изображения с помощью CKEditor с помощью плагина CKFinder. Плагин ожидает uploadImage
и 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 (
(
)}
/>
);
}
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 (
<>
{({
handleSubmit,
handleChange,
handleBlur,
values,
touched,
isInvalid,
errors
}) => (
Name
{errors.name}
Content
{
if (edit) {
setContent(doc.document);
}
}}
onChange={(event, editor) => {
const data = editor.getData();
setContent(data);
setDirty(true);
}}
config={{
ckfinder: {
uploadUrl:
`${APIURL}/document/uploadImage`
}
}}
/>
{dirty && !content ? "Content is required" : null}
)}
>
);
}
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 (
Documents
Add Document
Edit Document
Name
Document
Generate Document
Edit
Delete
{documentStore.documents.map(d => {
return (
{d.name}
Open
);
})}
);
}
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 (
Word App
);
}
export default withRouter(TopBar);
Это содержит Reac Bootstrap Navbar
чтобы показать верхнюю панель со ссылкой на главную страницу и имя приложения. Мы показываем ее только если существует token
. Также мы проверяем pathname
чтобы выделить правильные ссылки, установив параметр active
.
Далее в index.html
мы заменим существующий код на:
Word App
После написания всего этого кода мы можем запустить наше приложение. Прежде чем что-либо запускать, установите nodemon
, запустив, npm i -g nodemon
чтобы нам не приходилось перезагружать сервер при изменении файлов.
Затем запустите back end, запустив команду npm start
в папке backend
и npm start
в папке frontend
, затем выберите «yes», если вас попросят запустить его с другого порта.
Тогда вы получите: