使用 React 和 NodeJS 的杂货店应用程序的通知系统
TL;DR
在本指南中,我们将使用 Node 和 Express 制作一个简单的购物清单 Web 应用。然后,我们将使用 Novu 作为开源通知系统,在我们需要购买食品杂货的当天发送电子邮件提醒。
购买一些杂货
我承认,在记录事情方面,我不太擅长。有一半的时候,我去买东西的时候,都会漏掉一些完全忘记的东西。虽然我可以轻松地把事情写下来,但我也是一名开发者,喜欢给自己制造一些困难。所以,我们一起来做点什么吧!

在本文中,我们将介绍使用 Node.js 创建的简单购物清单,以及如何使用 Novu 平台从我们的 API 发送电子邮件的一些指南。
设置我们的项目
如果您想查看完整的项目 Github,可以在这里查看。
当然,我们需要以某种方式启动我们的项目。我们需要做的第一步是创建一个项目文件夹,然后添加一个基本网页。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./styles.css">
<link rel="stylesheet" href="https://unicons.iconscout.com/release/v4.0.0/css/line.css">
<script src="https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.0/dist/index.umd.min.js"></script>
<title>Grocery Notification</title>
</head>
<body>
<div class="container">
<div class="input">
<input name="Enter new item" id="grocery-input" placeholder="Enter new grocery item"></inp>
<i class="uil uil-notes notes-icon"></i>
</div>
<div class="datepicker-container">
<input id="datepicker" placeholder="Schedule Grocery Date" type="text"/>
</div>
<h1 class="title">Grocery Items</h1>
<ul class="grocery-list">
<li class="grocery-list-item" >
<span class="grocery-item">Eggs</span>
<i class="uil uil-trash delete-icon"></i>
</li>
</ul>
<button class="submit" type="button">Schedule</button>
</div>
<div class="error notification"></div>
<div class="success notification"></div>
<script src="./script.js"></script>
</body>
</html>
我们的标记其实很简单。我们有一个输入框,用于指定需要添加的杂货商品和商品列表。最后,我们还有一个输入框,用于指定想要提醒的日期。
我们将使用一个名为easepick的第三方日期选择器库。它是一个非常简单的库,可以添加到我们的应用中,并且我们会将它与Unicons 中的图标一起使用。
我们还将为我们的网站使用一些样式,您可以在这里找到:
styles.css
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,600;1,300&family=Roboto&display=swap');
* {
font-family: 'Roboto', sans-serif;
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #eee;
height: 100vh;
padding: 0;
}
.container {
position: relative;
max-width: 500px;
width: 100%;
background-color: #fff;
box-shadow: 0 3px 5px rgba(0,0,0,0.1);
padding: 30px;
margin: 85px auto;
border-radius: 6px;
}
.container .input {
position: relative;
height: 70px;
width: 100%
}
.container .datepicker-container {
margin-top: 15px;
display: flex;
justify-content: flex-end;
}
.container input#datepicker {
padding: 8px;
outline: none;
border-radius: 6px;
border: 1px solid #cecece;
}
input#grocery-input {
height: 100%;
width: 100%;
outline: none;
border-radius: 6px;
padding: 25px 18px 18px 18px;
font-size: 16px;
font-weight: 400;
resize: none;
}
.notes-icon {
position: absolute;
top: 50%;
font-size: 15px;
right: 20px;
transform: translateY(-50%);
font-size: 18px;
color: #828282;
}
.title {
text-align: center;
margin-top: 25px;
margin-bottom: 0px;
}
.grocery-list {
margin-top: 30px;
}
.grocery-list .grocery-list-item {
list-style: none;
display: flex;
align-items: center;
width: 100%;
background-color: #eee;
padding: 15px;
border-radius: 6px;
position: relative;
margin-top: 15px;
}
.grocery-list .grocery-item {
margin-left: 15px;
}
.grocery-list .delete-icon {
position: absolute;
right: 15px;
cursor: pointer;
}
button.submit {
margin-top: 40px;
padding: 12px;
border-radius: 6px;
outline: none;
border: none;
width: 100%;
background-color: #0081C9;
color: white;
cursor: pointer;
}
.notification {
position: absolute;
top: 5%;
left: 50%;
transform: translate(-50%,-50%);
width: 250px;
height: auto;
padding: 5px;
margin-top: 5px;
border-radius: 6px;
color: white;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
}
.error {
background-color: #FF5733;
}
.success {
background-color: #0BDA51;
}
.notification.show {
opacity: 1;
-webkit-animation: fadein 0.5s, fadeout .5s 1.5s;
animation: fadein 0.5s, fadeout .5s 1.5s;
}
@keyframes fadein {
from {top: 0; opacity: 0;}
to {top: 5%; opacity: 1; }
}
@keyframes fadeout {
from {top: 5%; opacity: 1;}
to {top: 0; opacity: 0;}
}
现在我们有了外观,我们需要功能。所以我们也添加一些 JavaScript:
script.js
window.addEventListener('DOMContentLoaded', () => {
const ulElement = document.querySelector('.grocery-list');
const submitElement = document.querySelector('.submit');
const inputElement = document.querySelector('#grocery-input');
const errorElement = document.querySelector('.error');
const successElement = document.querySelector('.success');
let dateSelected = null;
const showNotificationMessage = (element, errorMessage) => {
if (element.classList.contains('show')) {
return;
}
element.textContent = errorMessage;
element.classList.add('show');
setTimeout(() => {
element.classList.remove('show');
}, 2000)
}
const datePicker = new easepick.create({
element: '#datepicker',
css: [
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.0/dist/index.css"
],
zIndex: 10,
setup(picker) {
picker.on('select', (e) => {
dateSelected = e.detail.date;
})
}
});
ulElement.addEventListener('click', (e) => {
if (e.target.tagName === 'I') {
ulElement.removeChild(e.target.closest('li'));
}
})
inputElement.addEventListener('keyup', (e) => {
const value = e.target.value;
if (e.keyCode === 13 && value.trim()) {
const li = document.createElement('li');
li.classList.add('grocery-list-item');
const span = document.createElement('span');
span.classList.add('grocery-item');
span.textContent = value;
const icon = document.createElement('i');
icon.classList.add('uil', 'uil-trash', 'delete-icon');
li.appendChild(span);
li.appendChild(icon);
ulElement.appendChild(li);
inputElement.value = '';
}
});
submitElement.addEventListener('click', (e) => {
const groceryItems = [...document.querySelectorAll('span.grocery-item')].map(element => ({
item: element.textContent
}));
if (!dateSelected) {
return showNotificationMessage(errorElement, 'Please select the grocery date.');
}
const date2DaysBefore = new Date(dateSelected.setDate(dateSelected.getDate() - 1));
if (new Date() > date2DaysBefore) {
return showNotificationMessage(errorElement, 'Please select a date two days or more after this day.');
}
if (!groceryItems.length) {
return showNotificationMessage(errorElement, 'Please add grocery items.');
}
fetch('http://localhost:3000/grocery-schedule', {
method: 'POST',
body: JSON.stringify({
scheduledGroceryDate: dateSelected.toISOString(),
groceryItems
}),
headers: {
'content-type': 'application/json'
},
mode: 'cors'
})
.then(resp => resp.json())
.then((resp) => {
while (ulElement.lastChild) {
ulElement.removeChild(ulElement.lastChild);
}
showNotificationMessage(successElement, resp.message);
})
.catch(e => console.log(e))
})
})
在我们的script.js
文件中,我们:
- 初始化我们的 easepick 实例,并在
setup
方法中监听事件select
,以便获取日期值。 - 因为
ulElement
我们正在监听事件click
,并且在回调中,我们正在检查I
元素,因为这是我们无序列表的删除按钮。 - 我们
inputElement
正在监听keyup
事件,以便我们可以在无序列表元素中添加新项目。 - 最后,
submitElement
监听click
此元素的事件并将请求发送到我们的 API 以触发调度。
我们将在预定日期的前一天触发电子邮件,以便提前提醒我们。
这是 Web 应用程序在前端显示的一个杂货商品的样子。
创建我们的 API
现在我们需要使用 Node 和 Express 创建后端。具体来说,我们需要Node 版本 18.12.0。我们可以使用命令行实用程序切换到所需的版本。
mkdir api && cd api && npm init -y && npm install express cors @novu/node
我们在这里所做的是创建 API 文件夹,然后npm init -y
使用默认配置的命令初始化我们的 Node 项目。之后,我们安装了将要使用的库。我们不会将数据保存在数据库中,但稍后我们会讲到。我们还安装了 Novu@novu/node
包,以便可以通过 API 进行通知。
package.json
{
"name": "grocery-notify-app",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "node app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@novu/node": "^0.11.0",
"cors": "^2.8.5",
"express": "^4.18.2"
},
"type": "module"
}
config.js
const API_KEY = 'API_KEY'; // Novu Dashboard -> Settings -> Api Keys Tab
const SUBSCRIBER_ID = 'SUBSCRIBER_ID'; // subscriber id created by the sdk or from the workflow
const EMAIL = 'TO_EMAIL'; // your EMAIL to receive the notification
const PORT = 3000;
export {
API_KEY,
EMAIL,
SUBSCRIBER_ID,
PORT
}
我们稍后将从 Novu 仪表板API_KEY
获取。SUBSCRIBER_ID
app.js
import express from "express";
import cors from "cors";
import { API_KEY, SUBSCRIBER_ID, EMAIL, PORT } from "./config.js";
import { Novu } from "@novu/node";
const novu = new Novu(API_KEY);
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.post('/grocery-schedule', async (req, res) => {
const { scheduledGroceryDate, groceryItems } = req.body;
try {
res.status(200).send({ message: "Grocery Reminder Scheduled." })
} catch (e) {
console.log(e);
res.status(500).send({
message: 'Something went wrong, when scheduling the grocery reminder.'
})
}
});
app.listen(PORT, () => {
console.log(`server listening at port ${PORT}`);
})
在我们的示例中,app.js
我们有一个端点:/grocery-schedule
。我们将在这里处理在电子邮件中收到的通知。使用 Novu 非常简单,只需Novu
从模块中获取构造函数并创建一个新的 Novu 实例即可。您可以API_KEY
从Novu 帐户仪表板获取它。
Novu 是什么?
简单来说,Novu 是一个用于在应用程序中实现、配置和管理通知的平台。它可以管理:
- 电子邮件
- 短信
- 聊天
- 推送通知
一切尽在一个平台上!Novu 让构建实时应用程序变得更容易,我们只需在代码中配置并触发通知即可。您可以点击此处了解更多关于 Novu 的信息。您可以使用您的 GitHub 帐户轻松创建 Novu 帐户,这可能是本教程中最快的方式。
当您第一次看到 Novu 仪表板时,您将看到仪表板。
正如我之前所说,我们可以在仪表板中获取 API 密钥,具体来说是在“设置”→“Api 密钥”中。
之后,我们需要创建一个将在 API 中触发的通知模板。单击“通知”,然后单击最右侧的“新建”按钮。
输入通知模板的必要详细信息,例如通知名称、通知标识符和通知描述。在创建工作流之前,我们先来看看“集成 商店”选项卡。
正如我之前所说,Novu 提供了一系列通知集成选项,例如电子邮件、短信、聊天和推送通知。在这里,您可以将我们之前制作的通知 模板集成到特定的通知提供商中。我们将使用Mailjet作为电子邮件提供商,将购物提醒发送到特定的电子邮件。
首先,你的邮箱必须有一个有效的域名才能使用此功能。我将使用我的工作邮箱。
您可以在帐户设置→ Rest API → API 密钥管理(主帐户和子帐户)中获取您的 Mailjet API 密钥和密钥。
复制 API 密钥和密钥,然后前往Novu 控制面板→集成*商店 → Mailjet 提供商并粘贴。请确保包含您用于 Mailjet 的邮箱地址。
之后,我们需要编辑通知的工作流编辑器。在 Novu 仪表板中,前往“通知”,然后点击之前创建的通知。我的是grocery-notification
。
完成后,单击Workflow Editor
选项卡,然后在编辑器中单击触发器组件下方带有加号的圆圈,然后Delay
在右侧进行选择。
有两种类型Delay
:常规和预定。为此,我们将使用预定类型。我们还需要指定 Novu 将用于电子邮件的字段名称。在我的示例中,我将使用 字段sendAt
。
然后在Delay
组件下方再次单击带有加号的圆圈并选择Email
。
我们的工作流程应该是这样的:
最后,我们还需要配置电子邮件模板。点击“电子邮件属性”下方右侧的“编辑模板”按钮。重定向到“编辑 电子邮件 模板”界面后,您在此处看到的电子邮件就是之前在 Mailjet 配置中使用的电子邮件。将电子邮件主题更新为“Grocery Reminder”,然后点击“自定义代码”,并复制以下 Handlebars 代码:
<div>
<h1>Hi, It's time for you to buy your grocery items.</h1>
<h2>{{dateFormat date 'MM/dd/yyyy'}}</h2>
{{#each groceryItems}}
<li>{{item}}</li>
{{/each}}
</div>
这段代码非常简单。在 h2 部分,我们将date
值格式化为月/日/年格式,在使用 的部分,#each
我们只是在groceryItems
数组中进行迭代。我们还使用了列表项元素中每次迭代获得的 属性,并且需要在迭代后item
指定。/each
之后,点击Update
右上角的按钮。
完成我们的 API
完成所有这些后,就该完成我们应用的 API 了。config.js
使用之前从 Novu 仪表盘获取的 API 密钥、SUBSCRIBER_ID以及仪表盘中“通知”页面的邮箱地址来更新文件。EMAIL是可选的,因为SUBSCRIBER_ID会使用相同的值,但为了方便举例,我们还是提供了它。
我们的更新config.js
文件:
const API_KEY = 'YOUR_API_KEY_FROM_THE_SETTINGS_TAB'; // Novu Dashboard -> Settings -> Api Keys Tab
const SUBSCRIBER_ID = '63dd93575fd0df47313ee933';
const EMAIL = 'mac21macky@gmail.com';
const PORT = 3000;
export {
API_KEY,
EMAIL,
SUBSCRIBER_ID,
PORT
}
最后,我们需要更新app.js
文件以触发 Novu 来安排我们的通知:
import express from "express";
import cors from "cors";
import { API_KEY, SUBSCRIBER_ID, EMAIL, PORT } from "./config.js";
import { Novu } from "@novu/node";
const novu = new Novu(API_KEY);
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.post('/grocery-schedule', async (req, res) => {
const { scheduledGroceryDate, groceryItems } = req.body;
try {
// sendAt - 9 am on the `scheduledGroceryDate`
const sendAt = new Date(new Date(scheduledGroceryDate).setHours(9, 0, 0, 0)).toISOString();
await novu.trigger('grocery-notification', {
to: {
subscriberId: SUBSCRIBER_ID,
email: EMAIL
},
payload: {
sendAt,
date: new Date(scheduledGroceryDate).toISOString(),
groceryItems
}
});
res.status(200).send({ message: "Grocery Reminder Scheduled." })
} catch (e) {
console.log(e);
res.status(500).send({
message: 'Something went wrong, when scheduling the grocery reminder.'
})
}
});
app.listen(PORT, () => {
console.log(`server listening at port ${PORT}`);
});
基本上,我们在这里所做的就是创建预定日期值sendAt
。其值为上午 9 点scheduledGroceryDate
。例如,如果我们提供的值为2023 年 3 月 12 日,则该sendAt
值将为2023 年 3 月 12 日 上午 9:00。实例中的名称本身对应的trigger
方法将触发我们的通知。它接受触发器 ID作为第一个参数,您可以从 Novu 获取该 ID。novu
在对象中,to
我们提供来自的SUBSCRIBER_ID和EMAILconfig.js
,并在payload
对象中提供sendAt
、date
和groceryItems
。请记住,sendAt
和date
字段必须采用 ISO 字符串格式。
好的!让我们测试一下我们的应用。我要在购物清单中添加 5 个新商品:牛奶、面包、水、黄油和葡萄。
按下“安排”按钮,如果成功,将会弹出一条消息,告诉我们已安排好。
回到 Novu,在活动源中,您可以查看该特定提醒是否已安排。
当您单击第一个时,您可以了解该通知的详细信息。
此通知的执行被延迟,因为我们Delay
之前将其指定为类型,并且您还可以看到此通知的执行时间。
如果一切按计划进行,您将收到一封示例电子邮件。
如果您想扩展此项目,可以向我们的项目添加一个用于保存杂货商品的数据库。然后,在我们的 Web 应用的另一个页面上显示已安排的购物日期列表。
关闭购物清单
到目前为止,我们已经创建了一个应用,它使用一些简单的 Node.js 和 Express 来制作一个简单的购物清单。然后,我们利用 Novu 的强大功能来安排通知并发送电子邮件提醒自己。表面上看,它非常简单,但功能上还有很大的提升空间。您可以设置用户帐户并与家人共享,或者还可以添加短信通知!
Novu 是一款出色的开源通知系统,可用于将通知集成到您的应用程序中。它完全免费,并且在合适的人手中会是一款非常强大的工具。
如果您想查看完整的项目 Github,可以在这里查看。
鏂囩珷鏉ユ簮锛�https://dev.to/novu/notification-system-for-a-grocery-app-with-react-and-nodejs-234g