Remix项目集成Resend与React Email遇stream动态导入不支持错误
Remix + Resend + React Email 密码重置问题解决
错误原因分析
“Dynamic require of ‘stream’ is not supported” 是因为React Email的渲染逻辑(如render/renderAsync)被意外在浏览器端执行,而stream是Node.js核心模块,浏览器环境不支持。Remix作为全栈框架,必须确保邮件渲染和发送逻辑仅在服务器端路由的loader/action中执行,不能在客户端组件里调用。
核心解决方案步骤
1. 确保邮件发送逻辑仅在服务器端运行
Remix的action/loader是服务器端专属执行环境,把邮件发送和模板渲染放在action里,不要在客户端表单的onSubmit等逻辑中调用。
密码重置页面示例
// app/routes/reset-password.tsx import { Form, useActionData } from "@remix-run/react"; export default function ResetPasswordPage() { const actionData = useActionData<typeof action>(); return ( <div> <h1>重置密码</h1> <Form method="post"> <input type="email" name="email" placeholder="输入邮箱" required /> <button type="submit">发送重置链接</button> {actionData?.error && <p style={{ color: "red" }}>{actionData.error}</p>} {actionData?.success && <p style={{ color: "green" }}>{actionData.success}</p>} </Form> </div> ); } // 服务器端action,仅在Node环境运行 export async function action({ request }: { request: Request }) { const formData = await request.formData(); const email = formData.get("email") as string; try { // 生成重置令牌(替换为你的令牌生成逻辑) const resetToken = crypto.randomBytes(32).toString("hex"); // 保存令牌到数据库(省略具体逻辑) // 调用邮件发送函数 await sendPasswordResetEmail(email, resetToken); return { success: "重置链接已发送到你的邮箱" }; } catch (error) { console.error(error); return { error: "发送邮件失败,请稍后重试" }; } }
2. 修复邮件发送辅助函数
确保用React Email的服务器端渲染方法,且不在客户端引入相关代码。
邮件发送工具函数示例
// app/utils/send-email.ts import { Resend } from "resend"; import { renderAsync } from "@react-email/render"; import PasswordResetEmail from "../emails/PasswordResetEmail"; const resend = new Resend(process.env.RESEND_API_KEY); export async function sendPasswordResetEmail(email: string, resetToken: string) { // 仅在服务器端渲染邮件模板 const html = await renderAsync( <PasswordResetEmail resetToken={resetToken} /> ); await resend.emails.send({ from: "你的应用 <noreply@yourdomain.com>", to: email, subject: "重置你的密码", html, }); }
3. 调整React Email模板
模板只做纯React组件渲染,不要包含服务器端专属代码,避免触发动态依赖加载。
密码重置邮件模板示例
// app/emails/PasswordResetEmail.tsx import { Html, Body, Container, Button, Text } from "@react-email/components"; type Props = { resetToken: string }; export default function PasswordResetEmail({ resetToken }: Props) { const resetUrl = `https://yourdomain.com/reset-password/${resetToken}`; return ( <Html> <Body style={{ fontFamily: "Arial, sans-serif" }}> <Container style={{ maxWidth: "600px", margin: "0 auto", padding: "20px" }}> <Text>你请求重置密码,请点击下方链接完成操作:</Text> <Button href={resetUrl} style={{ backgroundColor: "#0070f3", color: "white", padding: "10px 20px", textDecoration: "none", borderRadius: "4px", display: "inline-block", marginTop: "16px", }} > 重置密码 </Button> <Text style={{ marginTop: "16px" }}>如果不是你操作,请忽略此邮件。</Text> </Container> </Body> </Html> ); }
4. Remix打包配置优化(若问题仍存在)
如果错误持续,在remix.config.js中添加Node.js模块的polyfill,或排除客户端不需要的依赖:
// remix.config.js /** @type {import('@remix-run/dev').AppConfig} */ export default { ignoredRouteFiles: ["**/.*"], serverModuleFormat: "cjs", // 确保服务器端用CommonJS,避免ES模块兼容问题 browserNodeBuiltinsPolyfill: { modules: { stream: true, // 仅必要时为浏览器端polyfill stream模块 }, }, };
关键注意事项
- 禁止在客户端组件中调用
renderAsync或Resend的send方法,这些都是服务器端专属逻辑。 - 确保
RESEND_API_KEY仅存在于服务器端环境变量,不要暴露给客户端。 - 测试时使用Remix开发服务器,确认action在Node环境执行,而非浏览器。
内容的提问来源于stack exchange,提问作者Miles Donald
相关产品推荐
相关产品推荐

