使用 React 和 NodeJS 的杂货店应用程序的通知系统

2025-06-10

使用 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>
Enter fullscreen mode Exit fullscreen mode

我们的标记其实很简单。我们有一个输入框,用于指定需要添加的杂货商品和商品列表。最后,我们还有一个输入框,用于指定想要提醒的日期。

我们将使用一个名为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;}
}
Enter fullscreen mode Exit fullscreen mode

现在我们有了外观,我们需要功能。所以我们也添加一些 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))
  })
})
Enter fullscreen mode Exit fullscreen mode

在我们的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
Enter fullscreen mode Exit fullscreen mode

我们在这里所做的是创建 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"
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

我们稍后将从 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}`);
})
Enter fullscreen mode Exit fullscreen mode

在我们的示例中,app.js我们有一个端点:/grocery-schedule。我们将在这里处理在电子邮件中收到的通知。使用 Novu 非常简单,只需Novu从模块中获取构造函数并创建一个新的 Novu 实例即可。您可以API_KEYNovu 帐户仪表板获取它。

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>

Enter fullscreen mode Exit fullscreen mode

这段代码非常简单。在 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
}
Enter fullscreen mode Exit fullscreen mode

最后,我们需要更新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}`);
});
Enter fullscreen mode Exit fullscreen mode

基本上,我们在这里所做的就是创建预定日期值sendAt。其值为上午 9 点scheduledGroceryDate。例如,如果我们提供的值为2023 年 3 月 12 日,则该sendAt值将为2023 年 3 月 12 日 上午 9:00实例中的名称本身对应的trigger方法将触发我们的通知。它接受触发器 ID作为第一个参数,您可以从 Novu 获取该 ID。novu

图片描述

在对象中,to我们提供来自的SUBSCRIBER_IDEMAILconfig.js,并在payload对象中提供sendAtdategroceryItems。请记住,sendAtdate字段必须采用 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
PREV
Firebase 云消息传递 (FCM) 终极指南
NEXT
如何使用 React、Websockets 和 Novu 构建 dev.to 通知中心🔥