使用React Email和Resend在Next.js中实现批量邮件发送
最近,我被指派负责设计和实施一个针对用户的群发邮件通知系统。然而,因为我之前只使用过SendGrid来创建对询问的生硬回复邮件,所以对实施过程中的设计概念一无所知。
因为从那时候的设想到最终的实施过程中,我进行了许多思考和摸索(或许有点夸张),所以我决定借此机会写一篇回顾文章!
目标读者
这主要是针对前端工程师的。
-
- 問い合わせ以外にメール配信サービスを活用したことがない方
-
- メールの本文をHTMLではなく、Reactコンポーネントで実装したい方
- これから実務で使おうと検討中の方
目标
-
- 本文が装飾された案内メールを一斉配信する。
- 本文は使い回ししやすいようテンプレート管理する。
任务的列举
在发送导航邮件之前,需要考虑如何装饰正文以及使用哪个邮件分发服务。
1. 文章的修饰
最终需要使用HTML、CSS等来装饰的事情我是知道的,但作为一个过于熟悉和喜爱React的人,我想尽可能地用React来实现。
所以,我开始寻找能否通过React组件来实现,结果发现了一个叫做“React Email”的库。
看起来使用起来也很方便,所以我决定在这里进行实现。
2 電子郵件投遞服務
React Email似乎也支持SendGrid。
看起来它还支持其他一些功能,但其中最方便的是“Resend”服务。
说明页面上写着以下英文:
当与其他服务集成时,您需要在发送之前将React模板转换为HTML。Resend会为您处理这些事情。
据说Resend就是可以直接使用,而无需将React转换为HTML,因为它是由React Email团队构建的,所以使用起来非常方便,因此我们立即采用。
※由于Resend与Vercel的兼容性很好,所以以后可能会成为默认选项吧?
回复邮件
使用React Email模板创建正文。
选择与要实施的引导邮件最接近的模板,并在借用的同时准备自己的原创模板似乎是个不错的选择。
安装React Email
如果你要在现有的仓库中进行设置,请使用这个手册来安装软件包。
基本上按照说明来进行操作,但由于邮件模板中还要使用@react-email/components,因此也要先安装它。
重新发送
接下来是重新发送设置。
大致需要进行以下步骤:
登录(建议使用GitHub认证)
创建API密钥
进行DNS记录设置。


如果设置正确,Resend仪表板上的状态将显示为“已验证”,如上方的两张图片所示。
重新安装Resend,设置APIKEY。
请使用以下命令进行安装:
npm install resend
yarn add resend
然后,在.env文件中将Resend的APIKEY设置如下:
RESEND_API_KEY=re_XXXXXXXXXX。
代码的实现
一旦到这里,就该开始编写代码了。
将本文的模板进行组件化
由于这次是试行,所以我们将直接使用Slack确认页面的模板。让我们直接将其组件化。
组件/邮件/Slack.tsx
import {
Body,
Container,
Column,
Head,
Heading,
Html,
Img,
Link,
Preview,
Row,
Section,
Text,
} from '@react-email/components';
import * as React from 'react';
interface SlackConfirmEmailProps {
validationCode?: string;
}
const baseUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: '';
export const SlackConfirmEmail = ({
validationCode = 'DJZ-TLX',
}: SlackConfirmEmailProps) => (
<Html>
<Head />
<Preview>Confirm your email address</Preview>
<Body style={main}>
<Container style={container}>
<Section style={logoContainer}>
<Img
src={`${baseUrl}/static/slack-logo.png`}
width="120"
height="36"
alt="Slack"
/>
</Section>
<Heading style={h1}>Confirm your email address</Heading>
<Text style={heroText}>
Your confirmation code is below - enter it in your open browser window
and we'll help you get signed in.
</Text>
<Section style={codeBox}>
<Text style={confirmationCodeText}>{validationCode}</Text>
</Section>
<Text style={text}>
If you didn't request this email, there's nothing to worry about - you
can safely ignore it.
</Text>
<Section>
<Row style={footerLogos}>
<Column style={{ width: '66%' }}>
<Img
src={`${baseUrl}/static/slack-logo.png`}
width="120"
height="36"
alt="Slack"
/>
</Column>
<Column>
<Row>
<Column>
<Link href="/">
<Img
src={`${baseUrl}/static/slack-twitter.png`}
width="32"
height="32"
alt="Slack"
style={socialMediaIcon}
/>
</Link>
</Column>
<Column>
<Link href="/">
<Img
src={`${baseUrl}/static/slack-facebook.png`}
width="32"
height="32"
alt="Slack"
style={socialMediaIcon}
/>
</Link>
</Column>
<Column>
<Link href="/">
<Img
src={`${baseUrl}/static/slack-linkedin.png`}
width="32"
height="32"
alt="Slack"
style={socialMediaIcon}
/>
</Link>
</Column>
</Row>
</Column>
</Row>
</Section>
<Section>
<Link
style={footerLink}
href="https://slackhq.com"
target="_blank"
rel="noopener noreferrer"
>
Our blog
</Link>
|
<Link
style={footerLink}
href="https://slack.com/legal"
target="_blank"
rel="noopener noreferrer"
>
Policies
</Link>
|
<Link
style={footerLink}
href="https://slack.com/help"
target="_blank"
rel="noopener noreferrer"
>
Help center
</Link>
|
<Link
style={footerLink}
href="https://slack.com/community"
target="_blank"
rel="noopener noreferrer"
data-auth="NotApplicable"
data-linkindex="6"
>
Slack Community
</Link>
<Text style={footerText}>
©2022 Slack Technologies, LLC, a Salesforce company. <br />
500 Howard Street, San Francisco, CA 94105, USA <br />
<br />
All rights reserved.
</Text>
</Section>
</Container>
</Body>
</Html>
);
export default SlackConfirmEmail;
const footerText = {
fontSize: '12px',
color: '#b7b7b7',
lineHeight: '15px',
textAlign: 'left' as const,
marginBottom: '50px',
};
const footerLink = {
color: '#b7b7b7',
textDecoration: 'underline',
};
const footerLogos = {
marginBottom: '32px',
paddingLeft: '8px',
paddingRight: '8px',
width: '100%',
};
const socialMediaIcon = {
display: 'inline',
marginLeft: '32px',
};
const main = {
backgroundColor: '#ffffff',
margin: '0 auto',
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
};
const container = {
maxWidth: '600px',
margin: '0 auto',
};
const logoContainer = {
marginTop: '32px',
};
const h1 = {
color: '#1d1c1d',
fontSize: '36px',
fontWeight: '700',
margin: '30px 0',
padding: '0',
lineHeight: '42px',
};
const heroText = {
fontSize: '20px',
lineHeight: '28px',
marginBottom: '30px',
};
const codeBox = {
background: 'rgb(245, 244, 245)',
borderRadius: '4px',
marginRight: '50px',
marginBottom: '30px',
padding: '43px 23px',
};
const confirmationCodeText = {
fontSize: '30px',
textAlign: 'center' as const,
verticalAlign: 'middle',
};
const text = {
color: '#000',
fontSize: '14px',
lineHeight: '24px',
};

将模板发送到服务器上。
服务器可以选择从API Routes、Route Handlers (Next.js App Router)或自定义后端中进行选择,但是此次选择API Routes(/api/send)。
如上图所示,在模板下方创建一个“发送消息”按钮,并在按钮点击时的onClick事件中向服务器发送模板。
下面是按钮点击时执行的函数代码(handleButtonClick)的示例。
在任意页面内的组件上。
// ボタンの制御用
const [isButtonClicked, setIsButtonClicked] = useState(false)
const handleButtonClick = (templateName: string, code?: string) => {
setIsButtonClicked(true)
// 宛先のメールアドレスの配列
const recipients = [
"hoge@gmail.com",
"hoge@yahoo.co.jp",
"hogehoge@gmail.com",
]
// APIルートへのフェッチ用
fetch("/api/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
react: templateName,
to: recipients,
}),
})
.then((response) => response.json())
.then((data) => {
console.log(data)
setIsButtonClicked(false)
})
.catch((error) => {
console.error("Error:", error)
setIsButtonClicked(false)
})
}
在这些参数中,我设置了一个叫recipients的参数,它是一个包含多个电子邮件地址的数组。
通过传递这个参数,可以实现批量发送邮件。
从服务器上进行批量邮件发送
请将以下内容用中文进行释义:api/send.ts。
/* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
/* eslint-disable import/no-anonymous-default-export */
import type { NextApiRequest, NextApiResponse } from "next"
import { Resend } from "resend"
import Slack from "../../components/mail/Slack"
const resend = new Resend(process.env.RESEND_API_KEY)
export default async (req: NextApiRequest, res: NextApiResponse) => {
try {
const { react, to: recipients } = req.body // ここでリクエストボディからデータを取得
// reactの値によってコンポーネントを選択
let selectedComponent
if (react === "Slack") selectedComponent = Slack()
else throw new Error("Invalid react value")
for (const recipient of recipients) {
const data = await resend.emails.send({
from: "demo-mail@hogehoge.com",
to: recipient,
subject: "テストメール",
html: "<strong>TEST</strong>",
react: selectedComponent,
})
}
res.status(200).json({ message: "Emails sent successfully." })
} catch (error) {
if (error instanceof Error) {
res.status(400).json({ message: error.message })
} else {
res.status(400).json({ message: "An unknown error occurred." })
}
}
}
另外,上述代码中的批量分发
for (const recipient of recipients) {
const data = await resend.emails.send({
from: "demo-mail@hogehoge.com",
to: recipient,
subject: "テストメール",
html: "<strong>TEST</strong>",
react: selectedComponent,
})
}
我在循环的部分进行了实现。

这样一来,暂时算是达到了这次的目标!
如果您想将每个用户的特殊信息嵌入正文中,……?
虽然到目前为止再说这个有些晚了,但是像促销等批量发送的邮件可以使用相同的模板发送给所有用户,但如果是像参考所用的Slack验证码模板一样,需要在正文中嵌入动态值并向个别用户发送。
(如果将模板示例用于Google规则更改通知就好了…)
在这种情况下,需要使用后端拥有的用户信息,因此根据项目的不同,最好使用自己的后端处理发送。比如Python等。
顺便提一下,重新发送似乎也可以在Python或Go中使用。
我打算以后试试这些方面。
结束
这次我们在调查的同时进行了实施,所以花了一些时间,但是一旦熟悉了,我认为就可以轻松地进行实施了。
对于追求效率的工程师来说,使用React组件可以无缝完成从文本创建到邮件发送的整个过程,这非常令人感激。
如果有机会,请尝试一下。
请从根源中获取灵感
请参考 React Email x Resend 体验文章,它更全面地覆盖了下一代邮件服务的内容。虽然我在写作中过于关注具体实务,并且内容有些零散,但还是希望您也能参考这篇文章。