В этом пошаговом руководстве мы создадим приложение для управления отзывами, от бэкенда до внешнего интерфейса, используя ReactJS для внешнего интерфейса и NodeJS, Nodemailer и Gmail для внутреннего интерфейса.

Содержание

  • Что мы строим
  • Какие пакеты мы используем
  • Конфигурация Gmail
  • Давайте код (Back-end)
  • Давайте код (Front-end)
  • Запуск вашего внутреннего приложения на машине Linux с использованием pm2
  • Полный полный исходный код
  • Заключение

Что мы строим

  • Мы создадим приложение с формой, в которой пользователь может указать свое имя, адрес электронной почты и свой отзыв.
  • Данные формы будут отправлены в серверное приложение NodeJS через API.
  • Затем данные будут отформатированы в HTML-страницу, а затем повторно отправлены в учетную запись Gmail с помощью Nodemailer.
  • Чтобы сохранить нашу архитектуру кода чистой, мы сгруппируем каждую функциональность в отдельный файл.
  • Мы научимся управлять cors в отдельном файле со специальной конфигурационной папкой «/config».
  • Мы будем управлять проверками в другом файле: «validations/feedback.js».
  • Все наши функции, связанные с маршрутом «/feedback», также будут управляться в отдельном файле: «/controllers/feedbackController.js».
  • Для блокировки спама будет использоваться ограничитель скорости (5 отзывов в час): «/middlewares/rateLimiter.js».
  • Мы запустим нашу серверную часть на машине с Linux, используя pm2, это позволит нам перезапуститься в случае сбоя нашего приложения или перезагрузки сервера и вести журналы для различных событий.

Какие пакеты мы используем

Для задней части:

  • Мы будем использовать NodeJS и Express для управления маршрутом API.
  • Dotenv: для управления переменными окружения.
  • express-rate-limit: чтобы ограничить количество отзывов, которые пользователь может отправить в наш API (механизм защиты от спама).
  • cors: для управления cors в нашем внутреннем приложении.
  • Nodemailer: для отправки писем на учетную запись Gmail.

Для фронтенда:

  • Мы будем использовать ReactJS для создания простой формы связи с нами с полями форта: имя, электронная почта, тема, сообщение.

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

Конфигурация Gmail

Для настройки Gmail мы будем использовать концепцию менее безопасных приложений, которая означает, что любой, у кого есть ваш пользователь и пароль, может отправлять электронные письма с вашей учетной записи Gmail, чтобы включить менее безопасные приложения, вы можете перейти здесь.

Если вы активировали 2FA, вам нужно использовать другую концепцию: Пароль для конкретного приложения, чтобы включить его, перейдите здесь.

Давайте код (Back-end)

Мы начнем с нашего бэкэнда

Создайте новый каталог «back-end» и перейдите к нему

mkdir backend && cd backend

Инициализируйте npm, чтобы создать файл package.json для нашего внутреннего приложения, в качестве точки входа выберите: server.js

npm init

Теперь в файле «package.json» обновите раздел «скрипты».

  "scripts": {
    "start": "node server",
    "dev": "node --watch server"
  }

Это позволит нам запускать команду node v18.x с новым флагом «-watch», чтобы отслеживать любые изменения в нашем коде (например, в пакете nodemon).

Установка пакетов npm

npm i express dotenv express-rate-limit cors nodemailer

Прежде чем перейти к основному файлу приложения (server.js), я покажу вам структуру нашего приложения, а затем мы покажем вам, как реализовать каждый пакет: express, dotenv, cors, express-rate-limit и nodmailer.

- дотенв

После установки нашего пакета добавьте эту строку в «server.js»

require("dotenv").config();

Создайте новый файл на корневом уровне нашего приложения: «.env», мы поместим сюда наши секретные переменные.

PORT=3010
[email protected]
SUBJECTPREFIX=_MY_Website_
PASS=_your_gmail_password_

Не забудьте поместить сюда свои переменные и добавить этот файл в файл «.gitignore» на корневом уровне нашего приложения.

env
node_modules

Вы можете вызывать любые подобные переменные: например, PORT (в нашем случае это будет 3010).

const port =process.env.PORT

- кор

У нас есть два файла конфигурации:

  • разрешеноOrigins.js:
const allowedOrigins = [
  "http://localhost:3000",
  "http://127.0.0.1:3000",
  "http://server_address:3000",
];
module.exports = allowedOrigins;

Этот файл содержит список разрешенных источников, любые другие источники будут отклонены.

  • corsConfigs.js
const allowedOrigins = require("./allowedOrigins");

const corsConfigs = {
  origin: (origin, callback) => {
    if (allowedOrigins.indexOf(origin) !== -1 || !origin) {
      // remove ||!origin to block postman request

      callback(null, true);
    } else {
      callback(new Error("Origin not allowed by Cors"));
    }
  },
  optionsSuccessStatus: 200,
};
module.exports = corsConfigs;

Это файл конфигурации пакета cors (), он содержит функцию обратного вызова, которая проверяет наш файл «allowedOrigins» на наличие запроса, и если он не существует, он выдает ошибку с сообщением: «Происхождение не разрешено Cors».

« || !origin» разрешает запросы «без происхождения» из таких приложений, как postman.

Теперь включите эти две строки в наш «server.js», чтобы вызвать пакет cors.

const cors = require("cors");
const corsConfigs = require("./config/corsConfigs");

Затем передайте эти коры на наш экспресс-сервер в качестве промежуточного программного обеспечения с параметром «corsConfigs».

app.use(cors(corsConfigs));

- выражать

В нашем приложении мы создадим простой API с одним маршрутом: «http://ваш-сервер-адрес/feedback».

В наш файл «server.js» добавьте следующие строки:

const app = express();
const port = process.env.PORT || 3010;

.....

app.use(express.json());
app.use("/feedback", require("./routes/feedbackRoutes"));
app.listen(port, () => {
  console.log(`✅ Application running on port: ${port}`);
});
  • У нас есть запрос экспресс-пакета в первую очередь
  • вызывая промежуточное ПО express.json(), при этом наше приложение может управлять форматом JSON в объекте запроса.
  • Затем мы направляем все запросы из «/feedback» в наши файлы «./routes/feedbackRoutes», этот файл содержит все функции, которые управляют этим маршрутом.
  • Теперь прослушайте номер порта из «.env» с 3010 по умолчанию.
  • Если все пойдет хорошо, напечатайте в терминале сообщение console.log: `✅ Приложение работает на порту: ${port}`.

Наш файл «server.js» будет выглядеть так

require("dotenv").config();
const express = require("express");
const cors = require("cors");
const corsConfigs = require("./config/corsConfigs");
const app = express();
const port = process.env.PORT || 3500;

app.use(cors(corsConfigs));
app.use(express.json());
app.use("/feedback", require("./routes/feedbackRoutes"));
app.listen(port, () => {
  console.log(`✅ Application running on port: ${port}`);
});

Теперь давайте объясним файл «./routes/feedbackRoutes».

const express = require("express");
const router = express.Router();
const feedbackController = require("../controllers/feedbackController");
const rateLimiter = require("../middlewares/rateLimiter");
const feedbackValidation = require("../validation/feedback");

router
  .route("/")
  .post(rateLimiter, feedbackValidation, feedbackController.feedback);

module.exports = router;

В этом файле

  • мы будем управлять маршрутом «/feedback» для нашего API с помощью метода: «POST»
  • Мы будем использовать промежуточное программное обеспечение «rateLimiter», с ограничением количества отзывов до 5 в час.

Если это число превышено, будет отправлен запрос об ошибке n с сообщением: «Слишком много отзывов с этого IP, попробуйте позже через 1 час».

Для этого воспользуемся пакетом «express-rate-limit».

const rateLimit = require("express-rate-limit");

const rateLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, //1 hour limit
  max: 5, //5 request limit
  message: {
    message: "Too many feedbacks from this IP, please try later after 1 hour",
  },
  handler: (req, res, next, options) => {
    res.status(options.statusCode).send(options.message);
  },
  standardHeaders: true, //return rate limit info in the "RateLimit-*" headers
  legacyHeaders: false, //Disable the "X-RateLimit-* headers
});
module.exports = rateLimiter;
  • И промежуточное ПО «feedbackValidation.validate»

Мы будем управлять всеми нашими проверками в этом файле, для этого мы будем использовать пакет «express-validator».

const { body } = require("express-validator");

const validate = [
  body("name").notEmpty().withMessage("You must enter a valide Name").trim(),
  body("email")
    .notEmpty()
    .isEmail()
    .withMessage("You must enter a valide email")
    .normalizeEmail(),
  body("subject")
    .notEmpty()
    .withMessage("You must enter a valide subject")
    .trim(),
  body("text")
    .notEmpty()
    .withMessage("You must enter a valide text")
    .isLength({ min: 10 })
    .withMessage("Text must be at least 10 characters long")
    .trim(),
];
module.exports = validate;

Мы проверим, не пусты ли поля имени, темы и текста, действителен ли адрес электронной почты и содержит ли текстовое поле не менее 10 символов.

Если произойдет какая-либо из этих ошибок, в качестве ответа конечному пользователю будет возвращен массив с соответствующими сообщениями.

  • Теперь «./controllers/feedbackController.js»
const  {validationResult}  = require("express-validator");
const nodemailer = require("nodemailer");

// @desc FEEDBACK
// @Route POST /feedback
// @Access Public
const feedback = async (req, res) => {
  console.log(req.body);
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json({ errors: errors.array() });
  }
  // NodeMailer proccessing ...

  let transporter = nodemailer.createTransport({
    service: "gmail",
    auth: {
      user: process.env.EMAIL,
      pass: process.env.PASS,
    },
  });
  let mailOptions = {
    from: req.body.email,
    to: process.env.EMAIL,
    subject: `${process.env.SUBJECTPREFIX}: ${req.body.subject}`,
    html: 
    `<div>
    <p>From: ${req.body.email}</p>
    <p>Subject: ${req.body.subject}</p>
    <p>${req.body.text}</p>
    </div>`,
  };
  transporter.sendMail(mailOptions, function (error, info) {
    if (error) {
      console.log(error);
      return res.json({ message: "Error: Message is not sent" });
    } else {
      console.log("Email sent: " + info);
      return res.json({ message: "Message sent with success" });
    }
  });
};

module.exports = { feedback };

Сначала мы проверим наличие ошибок с помощью и отправим ответ с кодом состояния 422 и массивом, если какая-либо ошибка существует.

const  validationResult  = require("express-validator");
....
const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json({ errors: errors.array() });
  }

Затем мы начнем использовать наш пакет Nodemailer.

. Настройте транспортер с именем пользователя и паролем из файла «.env».

let transporter = nodemailer.createTransport({
    service: "gmail",
    auth: {
      user: process.env.EMAIL,
      pass: process.env.PASS,
    },
  });

. Настройте электронное письмо для отправки, для этого мы будем использовать информацию из файла «req.body» и из файла «.env».

  let mailOptions = {
    from: req.body.email,
    to: process.env.EMAIL,
    subject: `${process.env.SUBJECTPREFIX}: ${req.body.subject}`,
    html: 
    `<div>
    <p>From: ${req.body.email}</p>
    <p>Subject: ${req.body.subject}</p>
    <p>${req.body.text}</p>
    </div>`,
  };

Наконец отправить письмо

  transporter.sendMail(mailOptions, function (error, info) {
    if (error) {
      console.log(error);
      return res.statuscode(500).json({ message: "Error: Message is not sent" });
    } else {
      console.log("Email sent: " + info);
      return res.json({ message: "Message sent with success" });
    }
  });
};

Если все пойдет хорошо, отправьте ответ OK, в противном случае отправьте ответ об ошибке с кодом 500: Internal Server Error.

Запуск вашего внутреннего приложения на машине Linux с использованием pm2

PM2 — это менеджер производственных процессов для приложений Node.js со встроенным балансировщиком нагрузки. Это позволяет поддерживать приложения в рабочем состоянии вечно, перезагружать их без простоев и облегчать выполнение общих задач системного администратора.

Теперь мы будем запускать наше приложение в фоновом режиме, используя pm2, мы будем использовать флаг «-watch», чтобы перезапускать наше приложение при изменении кода нашего приложения.

pm2 start server.js --watch

После запуска вашего приложения вы можете легко управлять своими приложениями, запустив:

pm2 list

Мониторинг журналов

pm2 monit

Подробнее о менеджере пакетов pm2 можно прочитать здесь

Давайте код (Front-end)

Объяснив внутреннюю часть нашего приложения с полным стеком, мы не будем переходить к внешней части.

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

Мы построим форму с контролируемыми входами fort (с хуками useStat):

  • имя: имя отправителя отзыва.
  • электронная почта: электронная почта отправителя отзыва.
  • subject: тема отзыва.
  • text: текст или сообщение обратной связи.

Наше приложение будет содержать один компонент Form, содержащий форму и функцию отправки: handleSubmit().

- index.js

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

- App.js

import "./styles.css";
import Form from "./Form";
export default function App() {

  return <Form/>;
}

- Форма.js

import "./styles.css";
import { useEffect, useRef, useState } from "react";
export default function Form() {
  const nameRef = useRef();
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [subject, setSubject] = useState("");
  const [text, setText] = useState("");
  const [result, setResult] = useState("");
  const handleSubmit = async (e) => {
    setResult("");
    e.preventDefault();
    // validating data
    if (!name || !email || !subject || text?.length < 10) {
      setResult("Please verify your inputs ...");
      return null;
    }
    const data = { name, email, subject, text };
   fetch("http://localhost:3010/feedback", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    })
      .then((response) => {
        return response.json();
      })
      .then((json) => {
        console.log(json?.message);
        setResult(`Result: ${json?.message}`);
      })
      .catch((error) => {
        console.log(error?.message);
        setResult(`Error: ${error?.message}`);
      });
  };
  useEffect(() => {
    nameRef.current.focus();
  }, []);

  return (
    <div className="App">
      <h1>Fomr2Email - Feedback</h1>
      <form onSubmit={handleSubmit} className="form__container">
        <div className="form__controls">
          <label htmlFor="name">Name</label>
          <input
            ref={nameRef}
            type="text"
            id="name"
            value={name}
            onChange={(e) => setName(e.target.value)}
          />
        </div>
        <div className="form__controls">
          <label htmlFor="email">Email</label>
          <input
            id="email"
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div className="form__controls">
          <label htmlFor="subject">Subject</label>
          <input
            id="subject"
            className="input__subject"
            type="text"
            value={subject}
            onChange={(e) => setSubject(e.target.value)}
          />
        </div>
        <div className="form__controls">
          <label htmlFor="text">Text</label>
          <textarea
            rows="5"
            id="text"
            type="text"
            value={text}
            onChange={(e) => setText(e.target.value)}
          />
        </div>
        <div className="form__controls">
          <button className="button">Send Feedback</button>
        </div>
      </form>
      <p>{result}</p>
    </div>
  );
}

Как вы можете видеть в функции handleSubmit()

  • Сброс результата
setResult("");
  • Простая проверка данных
// validating data
    if (!name || !email || !subject || text?.length < 10) {
      setResult("Please verify your inputs ...");
      return null;
    }
  • Получение данных
const data = { name, email, subject, text };
    fetch("http://localhost:3010/feedback", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    })
      .then((response) => {
        return response.json();
      })
      .then((json) => {
        console.log(json?.message);
        setResult(`Result: ${json?.message}`);
      })
      .catch((error) => {
        console.log(error?.message);
        setResult(`Error: ${error?.message}`);
      });
  };

Результат выборки будет сохранен в статистике «результат» с «setResult», а затем показан в конце формы.

<p>{result}</p>

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

const nameRef = useRef();

...

useEffect(() => {
    nameRef.current.focus();
  }, []);

Когда наш компонент монтируется, ввод имени получает фокус.

- стили.css

.App {
  font-family: sans-serif;
  text-align: center;
  display: flex;
  flex: 1;
  flex-direction: column;
}
.button {
  height: 40px;
  width: 150px;
  font-size: 1.2rem;
  border-radius: 5px;
  cursor: pointer;
  transition: 0.1s ease-in-out;
  margin: auto;
}
.button:hover {
  background-color: #84c586;
  color: whitesmoke;
  border-color: gray;
  transform: scale(1.1);
}
.form__controls {
  display: flex;
  flex-direction: row;
  justify-content: flex-start;
  align-items: flex-start;
  margin: 10px;
  padding: 2px;
}
.form__controls > label {
  margin-right: 10px;
  width: 100px;
  height: 30px;
}

.form__controls > input {
  width: 200px;
  height: 30px;
  border-radius: 5px;
  border: 1px solid gray;
  transition: 0.1s ease-in-out;
}
.form__controls > textarea {
  width: 300px;
  border-radius: 5px;
  border: 1px solid gray;
  transition: 0.1s ease-in-out;
}
.form__controls > input:focus,
textarea:focus {
  outline: 2px solid #84c586;
  outline-offset: 1px;
  transform: scale(1.05);
}

.form__container {
  border: 1px solid #ccc;
  border-radius: 5px;
  margin: auto;
  padding: 10px;
  width: 450px;
}
@media only screen and (max-width: 480px) {
  .form__container {
    width: 290px;
  }
  .form__controls > input {
    width: 150px;
  }
  .form__controls > textarea {
    width: 280px;
  }
}

Стилизация формы

Полный полный исходный код



Заключение

С Form2Email вам больше никогда не понадобятся сторонние веб-сервисы для включения форм обратной связи или обратной связи.

Его легко реализовать, с любым внешним рабочим процессом, событием для внутреннего интерфейса, это крошечный скрипт (всего 26,1 МБ памяти), который вы можете запустить на старом ПК с любой системой (способной запускать NodeJS).

Спасибо за прочтение.

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .

Заинтересованы в масштабировании запуска вашего программного обеспечения? Ознакомьтесь с разделом Схема.