🔥 使用 React Flow 和 Resend 构建电子邮件自动化系统 🎉 TL;DR

2025-05-25

🔥 使用 React Flow 和 Resend 构建电子邮件自动化系统 🎉

TL;DR

TL;DR

在本教程中,您将学习如何构建电子邮件自动化系统,每 10 分钟向人们发送一系列消息。⏰

  • 使用 React Flow 构建表示电子邮件流程的客户端图表。⿳
  • 每 10 分钟根据流程重新发送电子邮件。📝

电子邮件


Novu——第一个开源通知基础设施

简单介绍一下我们。Novu 是一个开源通知基础设施。我们主要负责管理所有产品通知。通知可以是应用内通知(类似于开发者社区的Websockets中的铃铛图标)、电子邮件、短信等等。

我们实际上也在项目中实现了 ReactFlow 和 Resend

如果你能给我们一颗星,我会非常高兴!这会帮助我每周写更多文章🚀
https://github.com/novuhq/novu

诺武


ReactFlow 构建你的流程✅

ReactFlow 是一个易于使用的库,可用于构建从静态图表到数据可视化,甚至复杂的可视化编辑器等各种内容。它高度可定制,并默认提供各种内置功能,例如拖动节点、缩放和平移、选择多个节点和边等等。

在本文中,您将学习如何使用 ReactFlow 向您的 React 应用程序添加交互式图表,以及如何  通过构建电子邮件推广应用程序使用Resend发送电子邮件。

该应用程序通过 ReactFlow 中的节点接受各种电子邮件内容,并将它们作为电子邮件消息发送。

ReactFlow


让我们开始吧🔥

在这里,我将引导您安装该项目所需的包依赖项;使用 Next.js v12。



npx create-next-app@12 email-outreach-app


Enter fullscreen mode Exit fullscreen mode

运行下面的代码片段来安装 ReactFlow 和 Resend 包。



npm install reactflow resend


Enter fullscreen mode Exit fullscreen mode

最后,安装 React Redux 和 Redux Toolkit 包,以便我们管理应用程序内的状态。



npm install react-redux @reduxjs/toolkit


Enter fullscreen mode Exit fullscreen mode

放置基本页面布局📟

在这里,我们将创建一个表单,它接受一封电子邮件、一个主题以及一系列包含要发送给收件人的消息的节点。消息将以 30 分钟的间隔发送。

首先,将下面的代码片段复制到pages/index.js文件中。



import Head from "next/head";
import { useState } from "react";

export default function Home() {
    const [email, setEmail] = useState("");
    const [subject, setSubject] = useState("");

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log({ email, subject });
        setEmail("");
        setSubject("");
    };

    return (
        <>
            <Head>
                <title>Email Outreach - Resend & ReactFlow</title>
                <meta name='description' content='Generated by create next app' />
                <meta name='viewport' content='width=device-width, initial-scale=1' />
                <link rel='icon' href='/favicon.ico' />
            </Head>
            <main className='main'>
                <header className='header'>
                    <h1 style={{ marginBottom: "15px" }}>
                        Email Outreach with ReactFlow and Resend
                    </h1>
                </header>

                <form className='form' onSubmit={handleSubmit}>
                    <label htmlFor='email'>Email</label>
                    <input
                        type='email'
                        name='email'
                        id='email'
                        className='input'
                        value={email}
                        required
                        onChange={(e) => setEmail(e.target.value)}
                    />

                    <label htmlFor='subject'>Subject</label>
                    <input
                        type='text'
                        name='subject'
                        id='subject'
                        className='input'
                        value={subject}
                        required
                        onChange={(e) => setSubject(e.target.value)}
                    />
                    {/* --- 👉🏻 ReactFlow Component placeholder 👈🏼 --- */}
                    <button className='submitBtn'>START AUTOMATION</button>
                </form>
            </main>
        </>
    );
}


Enter fullscreen mode Exit fullscreen mode

上面的代码片段创建了一个简单的表单,用于接收收件人的电子邮件地址和邮件主题。在接下来的部分中,我们将添加 ReactFlow 组件。

管理 ReactFlow 组件内的状态

在导入 ReactFlow 组件之前,让我们先设置状态管理库 - Redux Toolkit。

💡 PS:您不需要状态管理库即可使用 ReactFlow。

我们使用 Redux 来追踪组件内的输入并相应地更新应用程序的状态。此外, 您也可以轻松添加 ReactFlow 组件

因此,创建一个redux包含nodes.js和一个store.js文件的文件夹。



mkdir redux
cd redux
touch nodes.js store.js


Enter fullscreen mode Exit fullscreen mode

将下面的代码片段复制到redux/nodes.js文件中。



import { createSlice } from "@reduxjs/toolkit";

const addNode = (object) => {
    const newNode = {
        id: `${Number(object.id) + 1}`,
        type: "task",
        position: { x: 0, y: object.position.y + 120 },
        data: { value: "" },
    };
    return newNode;
};

const addEdge = (object) => {
    const newEdge = {
        id: `${object.id}->${Number(object.id) + 1}`,
        source: `${object.id}`,
        target: `${Number(object.id) + 1}`,
    };
    return newEdge;
};


Enter fullscreen mode Exit fullscreen mode

上面的代码片段包含两个函数,它们接受一个对象(节点数组中的最后一个元素)并返回另一个包含上述值的对象。

接下来,在同一个文件中,在函数下方添加代码片段。



//below the functions (within the same file)
//---- 👉🏻 functions 👈🏼---

export const nodeSlice = createSlice({
    name: "nodes",
    initialState: {
        nodes: [
            {
                id: "1",
                type: "task",
                position: { x: 0, y: 0 },
                data: { value: "" },
            },
        ],
        edges: [],
    },
    reducers: {
        setNodes: (state, action) => {
            let nodes = state.nodes;
            state.nodes = [...state.nodes, addNode(nodes[nodes.length - 1])];
            state.edges = [...state.edges, addEdge(nodes[nodes.length - 1])];
        },
        updateNodeValue: (state, action) => {
            let nodes = [...state.nodes];
            let objectIndex = nodes.findIndex((obj) => obj.id === action.payload.id);
            if (objectIndex !== -1) {
                state.nodes[objectIndex] = {
                    ...nodes[objectIndex],
                    data: { value: action.payload.value },
                };
            }
        },
    },
});

// Action creators are generated for each case reducer function
export const { setNodes, updateNodeValue } = nodeSlice.actions;

export default nodeSlice.reducer;


Enter fullscreen mode Exit fullscreen mode
  • 从上面的代码片段来看,
    • 我们创建了两个状态nodesedges数组。nodes状态中只有一个元素,代表图中的初始节点。
    • ReducersetNodes更新nodesandedges数组。当用户点击Add button每个图表节点内的 时,它会执行。
    • 减速器updateNodeValue跟踪图表中每个节点内的输入,并使用新值更新正确的节点。

调整大小

将节点减速器添加到store.js文件中。



import { configureStore } from "@reduxjs/toolkit";
import nodeReducer from "./nodes";

export const store = configureStore({
    reducer: {
        nodes: nodeReducer,
    },
});


Enter fullscreen mode Exit fullscreen mode

最后,通过更新文件使整个应用程序可以使用该存储_app.js



import { store } from "../redux/store";
import "../styles/globals.css";
import { Provider } from "react-redux";

export default function App({ Component, pageProps }) {
    return (
        <Provider store={store}>
            <Component {...pageProps} />
        </Provider>
    );
}


Enter fullscreen mode Exit fullscreen mode

恭喜!您已设置好图表所需的状态。接下来,让我们将其添加到应用中。

添加 ReactFlow 组件

由于我们对图中的每个节点使用自定义组件,因此创建一个components包含Task.js文件的文件夹。



mkdir components
cd components
touch Task.js


Enter fullscreen mode Exit fullscreen mode

将以下代码复制到Task.js文件中。Task 组件代表图中的每个节点。



import { useState } from "react";
import { Handle, Position } from "reactflow";
import { useSelector, useDispatch } from "react-redux";
import { setNodes, updateNodeValue } from "../redux/nodes";

export default function Task({ id }) {
    const initialNodes = useSelector((state) => state.nodes.nodes);
    const [value, setValue] = useState("");
    const dispatch = useDispatch();

    return (
        <>
            <Handle type='target' position={Position.Top} />
            <div
                style={{
                    padding: "10px",
                    backgroundColor: "#F5F5F5",
                    borderRadius: "5px",
                }}
            >
                <input
                    className='textInput'
                    type='text'
                    required
                    onChange={(e) => {
                        setValue(e.target.value);
                        dispatch(updateNodeValue({ id, value: e.target.value }));
                    }}
                    value={value}
                />
                {Number(id) === initialNodes.length && (
                    <button onClick={() => dispatch(setNodes())} className='addBtn'>
                        ADD NODE
                    </button>
                )}
            </div>

            <Handle type='source' position={Position.Bottom} id='a' />
        </>
    );
}


Enter fullscreen mode Exit fullscreen mode
  • 从上面的代码片段来看,
    •  顶部和底部渲染的Handle 组件将 每个节点连接到另一个节点。它有一个 type 属性,用于确定节点是源节点还是目标节点。
    • Add Node按钮触发setNodes减速器。
    • 当用户更新输入字段中的内容时,updateNodeValue也会触发 Reducer 使用输入值更新所选注释。
    • 图中的每个节点都有一个data和一个id包含该节点详细信息的道具。

接下来,将以下导入添加到pages/index.js文件中。



import { useState, useCallback, useMemo, useEffect } from "react";
import ReactFlow, {
    useNodesState,
    useEdgesState,
    getIncomers,
    getOutgoers,
    addEdge,
    getConnectedEdges,
} from "reactflow";
import "reactflow/dist/style.css";
import Task from "../components/Task";
import { useSelector } from "react-redux";


Enter fullscreen mode Exit fullscreen mode

在文件的 Home 组件中添加下面的代码片段pages/index.js



const initialNodes = useSelector((state) => state.nodes.nodes);
const initialEdges = useSelector((state) => state.nodes.edges);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const nodeTypes = useMemo(() => ({ task: Task }), []);

useEffect(() => {
    setNodes(initialNodes);
    setEdges(initialEdges);
}, [initialNodes, setNodes, initialEdges, setEdges]);

const onConnect = useCallback(
    (params) => setEdges((eds) => addEdge(params, eds)),
    [setEdges]
);


Enter fullscreen mode Exit fullscreen mode
  • 从上面的代码片段来看,
    • 使用ReactFlow提供的useNodesState和钩子将 Redux 状态中的节点和边设置为图的节点和边useEdgesState
    • 变量nodeTypes使我们能够定制每个节点。Task是我们的自定义组件。
    • onConnect当您添加新节点时,该函数就会执行。
    • useEffect当边和节点发生变化时,钩子就会运行

最后,将ReactFlow组件添加到用户界面,如下所示。



return (
    <form>
        {/*---👉🏻 other form elements 👈🏼---*/}
        <div style={{ height: "60vh", width: "100%", marginTop: "20px" }}>
            <ReactFlow
                nodes={nodes}
                edges={edges}
                onNodesChange={onNodesChange}
                onEdgesChange={onEdgesChange}
                onConnect={onConnect}
                nodeTypes={nodeTypes}
            />
        </div>
        <button className='submitBtn'>START AUTOMATION</button>
    </form>
);


Enter fullscreen mode Exit fullscreen mode

恭喜,您已成功将图表添加到您的应用程序中。


使用 Resend.com 发送您的电子邮件 📜

在本节中,您将学习如何使用 Resend 发送电子邮件,通过将每个节点中的输入发送到表单上提供的电子邮件。

Resend 是一个电子邮件 API,可让您轻松发送文本、附件和电子邮件模板。借助 Resend,您可以大规模构建、测试和发送事务性电子邮件。

其最佳功能之一是您的邮件不会进入收件人的垃圾邮件箱,而是进入收件人的收件箱。

我们已经在本教程开始时安装了 Resend。因此,请转到 注册页面并创建一个帐户

重新发送

创建 API 密钥并将其保存到.env.localNext.js 项目中的文件中。



RESEND_API_KEY=<place_your_API_key>


Enter fullscreen mode Exit fullscreen mode

接下来,在文件夹中创建一个send.js文件pages/api,并将下面的代码复制到该文件中。



//👇🏻 within the send.js file
import { Resend } from "resend";

// initiate the resend instance
const resend = new Resend(process.env.RESEND_API_KEY);

const timer = (time) => {
    return new Promise((res) => {
        setTimeout(() => res(true), time);
    });
}

export default async function handler(req, res) {
    const { subject, email, tasks } = req.body;
    if (!subject || !tasks || !email) {
        res.status(400).json({invalid: true});
    }

    for (const task of tasks) {
        await resend.emails.send({
            from: "name@yourcompany.dev",
            to: [email],
            subject,
            text: task,
        });

        // Wait 10 minutes
        await timer(600000);
    }

    res.status(200).json({invalid: false});
}


Enter fullscreen mode Exit fullscreen mode

上面的代码片段从请求中接收主题、收件人和电子邮件内容,并通过Resend向收件人发送电子邮件 

请注意,电子邮件之间会有 10 分钟的延迟。

这将无法部署到 Vercel,因为他们的免费包支持每个请求最多 10 秒。

您绝对可以在本地机器上对其进行测试。

在生产中,这样的事情需要进入一个队列,每隔 X 次发送电子邮件一次。

在文件中添加以下函数pages/index.js




const sendEmail = (index) => {
    fetch("/api/send", {
        method: "POST",
        body: JSON.stringify({
            email,
            subject,
            tasks: nodes.map(data => data.value), // map all nodes to a string array
        }),
        headers: {
            "Content-Type": "application/json",
        },
    })
        .then((data) => {
            alert(`Sent to processing`);
        })
        .catch((err) => {
            alert(`Encountered an error when message${index} ❌`);
            console.error(err);
        });
};


Enter fullscreen mode Exit fullscreen mode

上述函数循环遍历 ReactFlow 图中的各个节点,并定期向收件人发送包含节点值的电子邮件。

最后,当用户提交表单时执行该函数。



const handleSubmit = (e) => {
    e.preventDefault();
    sendEmail();//👈🏼 Send to server
    setEmail(""); // Reset the input
    setSubject(""); // Reset the input
};


Enter fullscreen mode Exit fullscreen mode

让我们总结一下🎁

到目前为止,您已经学习了如何使用 ReactFlow 向应用程序添加交互式图表以及如何使用 Resend 发送电子邮件。

ReactFlow 是一个 流行的开源库 ,它使我们能够构建交互式且可自定义的流程图和图表。如果您想构建一个需要拖放功能和可自定义图形 UI 元素的应用程序,那么应该考虑使用 ReactFlow。

本教程的源代码可以在这里找到:   https://github.com/novuhq/blog/tree/main/email-outreach-with-reactflow-and-resend

感谢您的阅读!🎉


帮帮我!

如果您觉得这篇文章帮助您更好地理解了电子邮件自动化!请给我们一颗星,我会非常高兴!也请在评论区告诉我❤️
https://github.com/novuhq/novu

帮助

文章来源:https://dev.to/novu/building-an-email-automation-system-with-react-flow-and-resend-17b5
PREV
使用 Puppeteer 和 React 构建交互式屏幕共享应用程序🤯
NEXT
💬 Building a real-time chat with Websockets, Novel and Clerk 🚀🚀 TL;DR