SendGrid - отличный сервис от Twilio для отправки электронных писем. Вместо того, чтобы настраивать собственный почтовый сервер для отправки электронной почты с вашими приложениями, мы используем SendGrid для выполнения тяжелой работы за нас. Это также снижает вероятность попадания электронной почты в спам, поскольку это надежный сервис.
Он также имеет очень простые в использовании библиотеки для различных платформ для отправки электронных писем. Node.js - одна из поддерживаемых платформ.
Чтобы отправлять электронные письма с помощью SendGrid, установите пакет SDK SendGrid, запустив npm i @sendgrid/mail
. Затем в своем коде добавьте const sgMail = require(‘@sendgrid/mail’);
, чтобы импортировать установленный пакет.
Затем в своем коде вы отправляете электронное письмо по:
sgMail.setApiKey(process.env.SENDGRID_API_KEY); const msg = { to: email, from: '[email protected]', subject: 'Example Email', text: ` Dear user, Here is your email. `, html: ` <p>Dear user,</p> <p>Here is your email.</p> `, }; sgMail.send(msg);
где process.env.SENDGRID_API_KEY
- это API SendGrid, который следует сохранить как переменную среды, поскольку это секрет.
В этой статье мы создадим приложение, которое позволит пользователям вводить шаблоны электронной почты. Затем они могут использовать шаблоны для отправки писем на разные адреса электронной почты. Наше приложение будет состоять из серверной части и клиентской части. Передняя часть будет построена с помощью React, а задняя часть будет построена с помощью Express.
Back End
Для начала создадим папку проекта и внутри нее добавим папку backend
внутри папки проекта. Мы будем использовать экспресс-генератор для генерации кода для нашего проекта. Для этого запустите npx express-generator
в папке backend
. Затем запустите npm i
, чтобы установить пакеты, перечисленные в package.json
.
Затем мы устанавливаем наши собственные пакеты. Мы будем использовать Sequelize в качестве ORM, Babel для использования новейших функций JavaScript, Dotenv для хранения переменных среды, SendGrid Nodejs для отправки электронных писем, CORS для включения междоменных запросов с интерфейсом и SQLite3 для базы данных.
Для их установки запустите npm i @babel/cli @babel/core @babel/node @babel/preset-env @sendgrid/mail cors sendgrid-nodejs sequelize sqlite3
.
После их установки мы можем приступить к созданию серверной части. Сначала мы добавляем .babelrc
, чтобы разрешить Babel в нашем приложении запускать приложение с последней версией интерпретатора JavaScript. Для этого добавьте .babelrc
в папку backend
и добавьте:
{ "presets": [ "@babel/preset-env" ] }
Затем в package.json
замените существующий код следующим в разделе scripts
:
"start": "nodemon --exec npm run babel-node -- ./bin/www", "babel-node": "babel-node"
Это позволяет нам работать с последней версией среды выполнения Babel Node вместо обычной среды выполнения Node, поэтому мы можем использовать новейшие функции JavaScript, такие как import
Затем запустите Sequelize CLI, чтобы создать шаблонный код базы данных и выполнить миграцию для создания базы данных. Запустите npx sequelize-cli init
в папке backend
и вы получите config.json
. В config.json
измените существующий код на:
{ "development": { "dialect": "sqlite", "storage": "development.db" }, "test": { "dialect": "sqlite", "storage": "test.db" }, "production": { "dialect": "sqlite", "storage": "production.db" } }
Затем мы создаем нашу миграцию данных. Запустить:
npx sequelize-cli model:create --name EmailTemplate --attributes name:string,type:string,template:text,subject,previewText:string
для создания таблицы EmailTemplates
в базе данных SQLite. Вышеупомянутая команда также должна создать соответствующую модель для этой таблицы.
Затем запустите npx sequelize-cli db:migrate
, чтобы создать базу данных.
Теперь мы можем перейти к созданию наших маршрутов. Создайте файл с именем email.js
в папке routes
и добавьте:
var express = require("express"); const models = require("../models"); const sgMail = require("@sendgrid/mail"); sgMail.setApiKey(process.env.SENDGRID_API_KEY); var router = express.Router(); router.get("/templates", async (req, res, next) => { const templates = await models.EmailTemplate.findAll(); res.json(templates); }); router.get("/template/:id", async (req, res, next) => { const id = req.params.id; const templates = await models.EmailTemplate.findAll({ where: { id } }); res.json(templates[0]); }); router.post("/template", async (req, res, next) => { try { const template = await models.EmailTemplate.create(req.body); res.json(template); } catch (ex) { res.json(ex); } }); router.put("/template/:id", async (req, res, next) => { try { const id = req.params.id; const { name, description, template, subject } = req.body; const temp = await models.EmailTemplate.update( { name, description, template, subject }, { where: { id } } ); res.json(temp); } catch (ex) { res.json(ex); } }); router.delete("/template/:id", async (req, res, next) => { try { const id = req.params.id; await models.EmailTemplate.destroy({ where: { id } }); res.json({}); } catch (ex) { res.json(ex); } }); router.post("/send", async (req, res, next) => { try { const { template, variables, to, subject, from } = req.body; let html = template; Object.keys(variables).forEach(variable => { html = html.replace(`[[${variable}]]`, variables[variable]); }); const msg = { to, from, subject, html }; sgMail.send(msg); res.json({}); } catch (ex) { res.json(ex); } }); module.exports = router;
Это все способы сохранения наших шаблонов и отправки электронного письма с шаблоном. Маршрут GET templates
получает все сохраненные нами шаблоны. Маршрут templates/:id
получает шаблон по идентификатору. Мы используем findAll
с оператором where id = {id}
для получения результатов и получаем только первый результат, полученный по идентификатору.
Маршрут POST template
создает наш шаблон с функцией create
. Маршрут PUR template
использует функцию update
для обновления записи, найденной путем поиска по идентификатору. Второй аргумент имеет наше условие выбора. Маршрут DELETE template
удаляется путем поиска записи по идентификатору для удаления с помощью функции destroy
.
Маршрут send
вызывает API SendGrid для отправки электронной почты с переменными, объявленными в шаблоне электронной почты, заполненными значениями, установленными пользователем. В теле запроса есть поле variables
для отправки переменных со значениями, где ключ - это имя переменной, а значение имеет значение.
Sequelize предоставляет функции create
, findAll
, update
и destroy
как часть модели.
В app.js
мы заменяем существующий код на:
require('dotenv').config(); const createError = require("http-errors"); const express = require("express"); const path = require("path"); const cookieParser = require("cookie-parser"); const logger = require("morgan"); const cors = require("cors"); const indexRouter = require("./routes/index"); const emailRouter = require("./routes/email"); const 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(cors()); app.use("/", indexRouter); app.use("/email", emailRouter); // 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;
Мы включили CORS, добавив:
app.use(cors());
И мы добавили наши маршруты, добавив:
const emailRouter = require("./routes/email"); app.use("/email", emailRouter);
На этом завершается серверная часть нашего почтового приложения.
Внешний интерфейс
Далее мы переходим к передней части. Мы запускаем новый проект React в папке project
, запустив npx create-react-app frontend
.
Затем нам нужно установить несколько пакетов. Нам нужен Bootstrap для стилизации, MobX для управления состоянием, Axios для выполнения HTTP-запросов, Formik и Yup для обработки значений формы и проверки формы соответственно и React Router для маршрутизации URL-адресов на наши страницы.
Чтобы установить пакеты, запустите npm i 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 "./App.css"; import TopBar from "./TopBar"; import EmailPage from "./EmailPage"; import { EmailTemplateStore } from "./store"; const history = createHistory(); const emailTemplateStore = new EmailTemplateStore(); function App() { return ( <div className="App"> <Router history={history}> <TopBar /> <Route path="/" exact component={props => ( <HomePage {...props} emailTemplateStore={emailTemplateStore} /> )} /> <Route path="/email/:id" exact component={props => ( <EmailPage {...props} emailTemplateStore={emailTemplateStore} /> )} /> </Router> </div> ); } export default App;
Это входной компонент нашего приложения, и он содержит маршруты, которые мы добавим. Маршруты имеют emailTemplateStore
, это магазин MobX, который мы создадим.
В App.css
замените существующий код на:
.page { padding: 20px; }
чтобы добавить отступы на наши страницы.
Затем мы создаем форму для добавления и редактирования наших шаблонов электронной почты. Создайте файл с именем EmailForm.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 } from "formik"; import { addTemplate, getTemplates, editTemplate } from "./request"; const schema = yup.object({ name: yup.string().required("Name is required"), template: yup.string().required("Template is required"), subject: yup.string().required("Subject is required") }); function EmailForm({ emailTemplateStore, edit, onSave, template }) { const handleSubmit = async evt => { const isValid = await schema.validate(evt); if (!isValid) { return; } if (!edit) { await addTemplate(evt); } else { await editTemplate(evt); } getAllTemplates(); }; const getAllTemplates = async () => { const response = await getTemplates(); emailTemplateStore.setTemplates(response.data); onSave(); }; return ( <> <Formik validationSchema={schema} onSubmit={handleSubmit} initialValues={edit ? template : {}} > {({ 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.Group as={Col} md="12" controlId="description"> <Form.Label>Description</Form.Label> <Form.Control type="text" name="description" placeholder="Description" value={values.description || ""} onChange={handleChange} isInvalid={touched.description && errors.description} /> <Form.Control.Feedback type="invalid"> {errors.description} </Form.Control.Feedback> </Form.Group> <Form.Group as={Col} md="12" controlId="subject"> <Form.Label>Subject</Form.Label> <Form.Control type="text" name="subject" placeholder="Subject" value={values.subject || ""} onChange={handleChange} isInvalid={touched.subject && errors.subject} /> <Form.Control.Feedback type="invalid"> {errors.subject} </Form.Control.Feedback> </Form.Group> <Form.Group as={Col} md="12" controlId="template"> <Form.Label> Template - Enter Template in HTML, Put Variables Between Double Brackets. </Form.Label> <Form.Control as="textarea" rows="20" name="template" placeholder="Template" value={values.template || ""} onChange={handleChange} isInvalid={touched.template && errors.template} /> <Form.Control.Feedback type="invalid"> {errors.template} </Form.Control.Feedback> </Form.Group> </Form.Row> <Button type="submit" style={{ marginRight: 10 }}> Save </Button> <Button type="button">Cancel</Button> </Form> )} </Formik> </> ); } export default observer(EmailForm);
Это форма, в которую мы вводим шаблон электронного письма. У нас есть поле имени и шаблона в качестве обязательных полей и описание в качестве необязательного поля. Мы используем Formik для автоматического обновления значений формы и заполнения их в параметре evt
функции handleSubmit
. Это экономит нам много работы, избавляя от необходимости писать обработчикиonChange
для каждого поля самостоятельно.
Для проверки формы мы определяем объект schema
, созданный с помощью yup
, и передаем его компоненту Formik
. Проверка формы происходит автоматически, и ошибки отображаются, как только в компонент Form.Control.Feedback
вводятся недопустимые значения.
Компонент Form
предоставляется React Boostrap. Опора edit
сообщит, установили ли мы initialialValues
в компоненте Formik
. Мы устанавливаем его на опору template
, только если мы edit
это true
, потому что только тогда нам есть что редактировать.
Функция handleSubmit
вызывается при отправке формы, она имеет значения всех полей формы. Мы вызываем schema.validate
для проверки значений формы на соответствие схеме перед отправкой. Если они действительны, мы вызываем addTemplate
или editTemplate
в зависимости от того, хотите ли вы добавить или изменить шаблон. edit
отличит их друг от друга. В случае успеха мы вызываем getAllTemplates
, чтобы получить все шаблоны и разместить их в нашем магазине.
Мы оборачиваем observer
за пределы EmailForm
компонента, чтобы получить последние значения из нашего магазина MobX, как только он будет обновлен.
Далее мы добавляем страницу для отправки писем. Добавьте файл с именем EmailPage.js
в папку src
и добавьте:
import React, { useState, useEffect } from "react"; import { withRouter } from "react-router-dom"; import { getTemplate, sendEmail } from "./request"; import * as yup from "yup"; import Form from "react-bootstrap/Form"; import Col from "react-bootstrap/Col"; import Button from "react-bootstrap/Button"; import { Formik } from "formik"; function EmailPage({ match: { params } }) { const [template, setTemplate] = useState({}); const [schema, setSchema] = useState(yup.object({})); const [variables, setVariables] = useState([]); const [initialized, setInitialized] = useState(false); const handleSubmit = async evt => { const isValid = await schema.validate(evt); if (!isValid) { return; } let data = { variables: {} }; data.template = evt.template; variables.forEach(v => { const variable = v.replace("[[", "").replace("]]", ""); data.variables[variable] = evt[variable]; }); data.to = evt.to; data.from = evt.from; data.subject = evt.subject; await sendEmail(data); alert("Email sent"); }; const getSingleTemplate = async () => { const response = await getTemplate(params.id); setTemplate(response.data); const placeholders = response.data.template.match(/\[\[(.*?)\]\]/g); setVariables(placeholders); let schemaObj = {}; placeholders.forEach(p => { p = p.replace("[[", "").replace("]]", ""); schemaObj[p] = yup.string().required("This field is required"); }); let newSchema = yup.object(schemaObj); setSchema(newSchema); setInitialized(true); }; useEffect(() => { if (!initialized) { getSingleTemplate(); } }); return ( <div className="page"> <h1 className="text-center">Send Email</h1> <Formik validationSchema={schema} onSubmit={handleSubmit} enableReinitialize={true} initialValues={template} > {({ handleSubmit, handleChange, handleBlur, values, touched, isInvalid, errors }) => ( <Form noValidate onSubmit={handleSubmit}> {variables.map((v, i) => { const variable = v.replace("[[", "").replace("]]", ""); return ( <Form.Row key={i}> <Form.Group as={Col} md="12" controlId="name"> <Form.Label>Variable - {variable}</Form.Label> <Form.Control type="text" name={variable} value={values[variable] || ""} onChange={handleChange} isInvalid={touched[variable] && errors[variable]} /> <Form.Control.Feedback type="invalid"> {errors[variable]} </Form.Control.Feedback> </Form.Group> </Form.Row> ); })} <Form.Row> <Form.Group as={Col} md="12" controlId="from"> <Form.Label>From Email</Form.Label> <Form.Control type="text" name="from" placeholder="From Email" value={values.from || ""} onChange={handleChange} isInvalid={touched.from && errors.from} /> <Form.Control.Feedback type="invalid"> {errors.from} </Form.Control.Feedback> </Form.Group> </Form.Row> <Form.Row> <Form.Group as={Col} md="12" controlId="to"> <Form.Label>To Email</Form.Label> <Form.Control type="text" name="to" placeholder="To Email" value={values.to || ""} onChange={handleChange} isInvalid={touched.to && errors.to} /> <Form.Control.Feedback type="invalid"> {errors.to} </Form.Control.Feedback> </Form.Group> </Form.Row> <Form.Row> <Form.Group as={Col} md="12" controlId="subject"> <Form.Label>Subject</Form.Label> <Form.Control type="text" name="subject" placeholder="Subject" value={values.subject || ""} onChange={handleChange} isInvalid={touched.subject && errors.subject} /> <Form.Control.Feedback type="invalid"> {errors.subject} </Form.Control.Feedback> </Form.Group> </Form.Row> <Form.Row> <Form.Group as={Col} md="12" controlId="template"> <Form.Label>Template</Form.Label> <Form.Control as="textarea" rows="20" name="template" placeholder="Template" value={values.template || ""} onChange={handleChange} isInvalid={touched.template && errors.template} readOnly /> <Form.Control.Feedback type="invalid"> {errors.template} </Form.Control.Feedback> </Form.Group> </Form.Row> <Button type="submit" style={{ marginRight: 10 }}> Send </Button> </Form> )} </Formik> </div> ); } export default withRouter(EmailPage);
В этом компоненте мы получаем шаблон электронной почты по идентификатору и извлекаем переменные из текста шаблона в функции getSingleTemplate
. Мы также создаем схему проверки с помощью Yup с помощью:
let schemaObj = {}; placeholders.forEach(p => { p = p.replace("[[", "").replace("]]", ""); schemaObj[p] = yup.string().required("This field is required"); }); let newSchema = yup.object(schemaObj);
Мы перебираем ключи поля variable
объекта response.data
и динамически строим схему проверки формы Yup.
В компонент Formik
мы помещаем опору:
enableReinitialize={true}
так что мы можем заполнить текст шаблона в поле формы с помощью name
prop template
.
В компоненте Form
у нас есть:
{variables.map((v, i) => { const variable = v.replace("[[", "").replace("]]", ""); return ( <Form.Row key={i}> <Form.Group as={Col} md="12" controlId="name"> <Form.Label>Variable - {variable}</Form.Label> <Form.Control type="text" name={variable} value={values[variable] || ""} onChange={handleChange} isInvalid={touched[variable] && errors[variable]} /> <Form.Control.Feedback type="invalid"> {errors[variable]} </Form.Control.Feedback> </Form.Group> </Form.Row> ); })}
чтобы динамически перебирать variables
, который мы установили в функции getSingleTemplate
, и отображать поле формы с сообщением проверки формы. Мы устанавливаем для параметра name
значение variable
, чтобы отображалось правильное сообщение.
Нам нужно обернуть функцию withRouter
за пределы EmailPage
, чтобы мы могли получить опору match
, чтобы получить идентификатор шаблона электронной почты из URL-адреса.
Затем мы создаем домашнюю страницу, в которой будет таблица для отображения списка сохраненных шаблонов и кнопки в каждой строке таблицы, позволяющие пользователям отправлять электронные письма с шаблоном или удалять или редактировать шаблон. Также будет кнопка, позволяющая пользователю добавить шаблон электронного письма.
import React, { useState, useEffect } from "react"; import { withRouter } from "react-router-dom"; import EmailForm from "./EmailForm"; 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 { getTemplates, deleteTemplate } from "./request"; function HomePage({ emailTemplateStore, history }) { const [openAddModal, setOpenAddModal] = useState(false); const [openEditModal, setOpenEditModal] = useState(false); const [initialized, setInitialized] = useState(false); const [template, setTemplate] = useState([]); const openAddTemplateModal = () => { setOpenAddModal(true); }; const closeAddModal = () => { setOpenAddModal(false); setOpenEditModal(false); }; const cancelAddModal = () => { setOpenAddModal(false); }; const cancelEditModal = () => { setOpenEditModal(false); }; const getAllTemplates = async () => { const response = await getTemplates(); emailTemplateStore.setTemplates(response.data); setInitialized(true); }; const editTemplate = template => { setTemplate(template); setOpenEditModal(true); }; const onSave = () => { cancelAddModal(); cancelEditModal(); }; const deleteSelectedTemplate = async id => { await deleteTemplate(id); getAllTemplates(); }; const sendEmail = template => { history.push(`/email/${template.id}`); }; useEffect(() => { if (!initialized) { getAllTemplates(); } }); return ( <div className="page"> <h1 className="text-center">Templates</h1> <ButtonToolbar onClick={openAddTemplateModal}> <Button variant="primary">Add Template</Button> </ButtonToolbar> <Modal show={openAddModal} onHide={closeAddModal}> <Modal.Header closeButton> <Modal.Title>Add Template</Modal.Title> </Modal.Header> <Modal.Body> <EmailForm onSave={onSave.bind(this)} cancelModal={cancelAddModal.bind(this)} emailTemplateStore={emailTemplateStore} /> </Modal.Body> </Modal> <Modal show={openEditModal} onHide={cancelEditModal}> <Modal.Header closeButton> <Modal.Title>Edit Template</Modal.Title> </Modal.Header> <Modal.Body> <EmailForm edit={true} template={template} onSave={onSave.bind(this)} cancelModal={cancelEditModal.bind(this)} emailTemplateStore={emailTemplateStore} /> </Modal.Body> </Modal> <br /> <Table striped bordered hover> <thead> <tr> <th>Name</th> <th>Description</th> <th>Subject</th> <th>Send Email</th> <th>Edit</th> <th>Delete</th> </tr> </thead> <tbody> {emailTemplateStore.templates.map(t => ( <tr key={t.id}> <td>{t.name}</td> <td>{t.description}</td> <td>{t.subject}</td> <td> <Button variant="outline-primary" onClick={sendEmail.bind(this, t)} > Send Email </Button> </td> <td> <Button variant="outline-primary" onClick={editTemplate.bind(this, t)} > Edit </Button> </td> <td> <Button variant="outline-primary" onClick={deleteSelectedTemplate.bind(this, t.id)} > Delete </Button> </td> </tr> ))} </tbody> </Table> </div> ); } export default withRouter(observer(HomePage));
У нас есть Table
, предоставленный React Boostrap. Опять же, мы оборачиваем функцию withRouter
за пределы компонента HomePage
внизу, чтобы получить объект history
в наших реквизитах, что позволяет нам вызывать history.push
для перенаправления на нашу страницу электронной почты.
У нас есть функции openAddTemplateModal
, closeAddModal
, cancelAddModal
, cancelEditModal
и onSave
для открытия или закрытия модальных окон добавления или редактирования. onSave
используется EmailForm
компонентом, передавая его как опору в EmailForm
. У нас есть getAllTemplates
функция для получения сохраненных нами шаблонов. Мы используем функцию обратного вызова useEffect
, чтобы получить шаблоны при первой загрузке, проверяя переменную initialized
, если она ложна, то загружаем getAllTemplates
и устанавливаем initialized
на false
, чтобы он больше не загружался.
Компонент Modal
предоставляется React Bootstrap. И в модальных окнах добавления и редактирования мы используем один и тот же EmailForm
для сохранения шаблонов. Мы можем сказать, добавляет ли пользователь или редактирует, используя свойство edit
.
Затем создайте файл с именем requests.js
и добавьте:
const APIURL = "http://localhost:3000"; const axios = require("axios"); export const getTemplates = () => axios.get(`${APIURL}/email/templates`); export const getTemplate = id => axios.get(`${APIURL}/email/template/${id}`); export const addTemplate = data => axios.post(`${APIURL}/email/template`, data); export const editTemplate = data => axios.put(`${APIURL}/email/template/${data.id}`, data); export const deleteTemplate = id => axios.delete(`${APIURL}/email/template/${id}`); export const sendEmail = data => axios.post(`${APIURL}/email/send`, data);
чтобы позволить использовать HTTP-запросы к нашей серверной части.
Затем мы создаем store.js
в src
и помещаем:
import { observable, action, decorate } from "mobx"; class EmailTemplateStore { templates = []; setTemplates(templates) { this.templates = templates; } } EmailTemplateStore = decorate(EmailTemplateStore, { templates: observable, setTemplates: action }); export { EmailTemplateStore };
чтобы позволить нам хранить массив шаблонов в центральной локализации для легкого доступа для всех компонентов. Мы передаем экземпляр этого в наши компоненты через свойство emailTemplateStore
компонентов, чтобы вызвать функцию setTemplates
, чтобы установить шаблоны и получить доступ к шаблонам с помощью emailTemplateStore.templates
. Вот почему мы оборачиваем observable
функцию вокруг наших компонентов. Нам нужны последние значения, поскольку они обновляются здесь. Также мы обозначили templates
как observable
в функции decorate
, чтобы поле templates
могло иметь последнее значение при каждом доступе. setTemplates
обозначен как action
, чтобы мы могли вызывать его для управления магазином.
Затем создайте TopBar.js
и добавьте:
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">Email 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);
для создания верхней панели с помощью компонента Navbar
, предоставляемого React Boostrap. Мы проверяем pathname
, чтобы выделить правильные ссылки, установив active
prop.
Наконец, в 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>Email 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>
чтобы добавить CSS Bootstrap и изменить заголовок приложения.
Как только все это будет сделано, перейдите в папку backend
и запустите npm start
, чтобы запустить серверную часть, перейдите в папку frontend
и выполните ту же команду. Ответьте yes
, если вас спросят, хотите ли вы запускать приложение с другого порта.
В итоге получаем: