如何在Next.js中使用Server Action POST发送数据至端点
问题描述
我有一个包含多步骤流程的父组件,其中一步使用了Array.from()静态方法。目前正在学习Server Action,希望通过POST请求将数据发送到指定端点。我已经把相关函数传递给子组件,也再次用到了数组方法,但不知道怎么把数据发送到目标端点。相关代码如下:
if (prev.step === 2) { const singleGuests = Array.from( { length: prev.tickets.single }, (_, i) => ({ firstName: formData.get(`single_firstName_${i}`), lastName: formData.get(`single_lastName_${i}`), email: formData.get(`single_email_${i}`), phonenumber: formData.get(`single_phonenumber_${i}`), }) ); const vipGuests = Array.from({ length: prev.tickets.vip }, (_, i) => ({ firstName: formData.get(`vip_firstName_${i}`), lastName: formData.get(`vip_lastName_${i}`), email: formData.get(`vip_email_${i}`), phonenumber: formData.get(`vip_phonenumber_${i}`), })); return { ...prev, step: prev.step + 1, guests: { single: singleGuests, vip: vipGuests }, }; }
子组件代码:
import { useState } from "react"; import { motion } from "framer-motion"; import { Caesar_Dressing } from "next/font/google"; const ceasarDressing = Caesar_Dressing({ subsets: ["latin"], weight: "400", display: "swap", }); export default function ContactInfo({ tickets, formAction }) { const [isFormValid, setIsFormValid] = useState(false); const handleInputChange = (e) => { const inputs = e.target.closest("form").querySelectorAll("input"); const allValid = Array.from(inputs).every((input) => input.checkValidity()); setIsFormValid(allValid); }; return ( <motion.form initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} transition={{ duration: 0.3 }} className="text-white rounded-lg bg-gradient-to-tl border border-gray-500 from-customBlack_2 to-customBlack p-4 relative z-0" onChange={handleInputChange} > <fieldset className="grid gap-6 mb-6 md:grid-cols-2"> <legend className={`${ceasarDressing.className} block mb-2 text-3xl`}> PERSONLIG INFORMATION </legend> {Array.from({ length: tickets.single }, (_, i) => ( <ContactForm key={i} i={i} ticketType="single" /> ))} <div className="relative z-10 group rounded-xl inline-block p-[2px] overflow-hidden shadow-lg"> <span className="absolute inset-[-1000%] animate-[spin_3s_linear_infinite] bg-[conic-gradient(from_90deg_at_50%_50%,#EC2783_0%,#141415_50%,#EC2783_100%)]" /> <div className="relative bg-gradient-to-tl from-customBlack_2 to-customBlack z-0 rounded-xl"> <div className="rounded-xl overflow-hidden"> {Array.from({ length: tickets.vip }, (_, i) => ( <ContactForm key={i} i={i} ticketType="vip" /> ))} </div> </div> </div> </fieldset> <div className="flex"> <button className={`${ isFormValid ? "font-bold px-8 py-2 my-8 ml-auto text-xl bg-gradient-to-bl from-customPink text-white to-customOrange text-transparent hover:transform" : "bg-gray-500 px-8 py-2 my-8 ml-auto text-xl font-bold text-gray-300 cursor-not-allowed" }`} formAction={formAction} type="submit" disabled={!isFormValid} > Næste </button> </div> </motion.form> ); } function ContactForm({ i, ticketType }) { // 使用staggerChildren给每个子元素添加延迟动画 // 在父组件中定义子组件可继承的变体状态 // 给每个子输入框定义inputSpring动画效果 const staggerInputs = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.2, }, }, }; const inputSpring = { hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0, transition: { type: "spring", stiffness: 100 }, }, }; return ( <motion.div initial="hidden" animate="visible" variants={staggerInputs} className="p-2" > {/* 根据ticketType判断是single还是vip票,进行条件渲染 */} {ticketType === "single" && ( <motion.h2 className="font-bold text-xl" variants={inputSpring}> Enkelt Billet </motion.h2> )} {ticketType === "vip" && ( <motion.h2 variants={inputSpring} className="font-bold text-xl bg-gradient-to-r from-customPink via-customRed to-customOrange bg-clip-text text-transparent" > VIP Billet </motion.h2> )} <motion.div className="mb-2.5" variants={inputSpring}> <label htmlFor={`${ticketType}_firstName_${i}`} className="block text-sm font-medium text-white" > Fornavn </label> <input name={`${ticketType}_firstName_${i}`} type="text" placeholder="Joe" autoComplete="name" required className="bg-gray-100 border border-gray-200 text-gray-900 text-sm rounded-md w-full p-2.5 focus:outline-none focus:ring-2 valid:[&:not(:placeholder-shown):not(:focus)]:bg-green-50 valid:[&:not(:placeholder-shown):not(:focus)]:border-green-500 valid:[&:not(:placeholder-shown):not(:focus)]:focus:ring-green-500 invalid:[&:not(:placeholder-shown):not(:focus)]:focus:ring-red-500 invalid:[&:not(:placeholder-shown):not(:focus)]:bg-red-50 invalid:[&:not(:placeholder-shown):not(:focus)]:border-red-400" /> </motion.div> <motion.div className="mb-2.5" variants={inputSpring}> <label htmlFor={`${ticketType}_lastName_${i}`} className="block text-sm font-medium text-white" > Efternavn </label> <input name={`${ticketType}_lastName_${i}`} type="text" placeholder="Doe" autoComplete="name" required className="bg-gray-100 border border-gray-200 text-gray-900 text-sm rounded-md w-full p-2.5 focus:outline-none focus:ring-2 valid:[&:not(:placeholder-shown):not(:focus)]:bg-green-50 valid:[&:not(:placeholder-shown):not(:focus)]:border-green-500 valid:[&:not(:placeholder-shown):not(:focus)]:focus:ring-green-500 invalid:[&:not(:placeholder-shown):not(:focus)]:focus:ring-red-500 invalid:[&:not(:placeholder-shown):not(:focus)]:bg-red-50 invalid:[&:not(:placeholder-shown):not(:focus)]:border-red-400" /> </motion.div> <motion.div className="mb-2.5" variants={inputSpring}> <label htmlFor={`${ticketType}_email_${i}`}>Email</label> <input className="bg-gray-100 border border-gray-200 text-gray-900 text-sm rounded-md w-full p-2.5 focus:outline-none focus:ring-2 valid:[&:not(:placeholder-shown):not(:focus)]:bg-green-50 valid:[&:not(:placeholder-shown):not(:focus)]:border-green-500 valid:[&:not(:placeholder-shown):not(:focus)]:focus:ring-green-500 invalid:[&:not(:placeholder-shown):not(:focus)]:focus:ring-red-500 invalid:[&:not(:placeholder-shown):not(:focus)]:bg-red-50 invalid:[&:not(:placeholder-shown):not(:focus)]:border-red-400" name={`${ticketType}_email_${i}`} type="email" placeholder="joedoe@example.com" autoComplete="email" required /> </motion.div> <motion.div className="mb-2.5" variants={inputSpring}> <label htmlFor={`${ticketType}_phonenumber_${i}`}>Mobilnummer</label> <input name={`${ticketType}_phonenumber_${i}`} type="tel" placeholder="12 34 56 78" autoComplete="tel" required className="bg-gray-100 border border-gray-200 text-gray-900 text-sm rounded-md w-full p-2.5 focus:outline-none focus:ring-2 valid:[&:not(:placeholder-shown):not(:focus)]:bg-green-50 valid:[&:not(:placeholder-shown):not(:focus)]:border-green-500 valid:[&:not(:placeholder-shown):not(:focus)]:focus:ring-green-500 invalid:[&:not(:placeholder-shown):not(:focus)]:focus:ring-red-500 invalid:[&:not(:placeholder-shown):not(:focus)]:bg-red-50 invalid:[&:not(:placeholder-shown):not(:focus)]:border-red-400" /> </motion.div> </motion.div> ); }
解决方案
1. 创建Server Action文件
在Next.js项目中新建actions.js文件,定义处理表单提交的Server Action:
"use server"; export async function submitGuestData(formData) { // 解析表单中携带的票数量 const ticketCounts = JSON.parse(formData.get("ticketCounts")); // 解析嘉宾数据,复用原有的Array.from逻辑 const singleGuests = Array.from( { length: ticketCounts.single }, (_, i) => ({ firstName: formData.get(`single_firstName_${i}`), lastName: formData.get(`single_lastName_${i}`), email: formData.get(`single_email_${i}`), phonenumber: formData.get(`single_phonenumber_${i}`), }) ); const vipGuests = Array.from( { length: ticketCounts.vip }, (_, i) => ({ firstName: formData.get(`vip_firstName_${i}`), lastName: formData.get(`vip_lastName_${i}`), email: formData.get(`vip_email_${i}`), phonenumber: formData.get(`vip_phonenumber_${i}`), }) ); // 发送POST请求到目标端点 try { const response = await fetch("你的目标端点URL", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ singleGuests, vipGuests }), }); if (!response.ok) throw new Error("数据发送失败"); const result = await response.json(); return { success: true, data: result }; } catch (error) { return { success: false, error: error.message }; } }
2. 修改父组件传递formAction的逻辑
在父组件中导入Server Action,将票数量作为隐藏字段传入表单,并处理提交结果:
// 父组件中导入Server Action import { submitGuestData } from "@/actions"; // 渲染ContactInfo组件时传递formAction <ContactInfo tickets={tickets} formAction={async (formData) => { // 将票数量添加到formData中 formData.append('ticketCounts', JSON.stringify(tickets)); // 调用Server Action const result = await submitGuestData(formData); // 根据结果更新流程步骤或处理错误 if (result.success) { setStep(prev => prev + 1); } else { console.error("提交失败:", result.error); // 可添加错误提示逻辑 } }} />
3. 修正子组件表单配置
给子组件的表单添加POST方法,确保符合Server Action的提交要求:
// ContactInfo组件中的form标签修改为: <motion.form initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} transition={{ duration: 0.3 }} className="text-white rounded-lg bg-gradient-to-tl border border-gray-500 from-customBlack_2 to-customBlack p-4 relative z-0" onChange={handleInputChange} method="POST" >
关键说明
- Server Action必须标记
"use server",确保在服务器端执行,避免暴露敏感信息。 - 票数量需要通过表单传递给Server Action,否则无法正确解析对应数量的嘉宾数据。
- 请求发送时要设置
Content-Type: application/json,将数据序列化为JSON格式。 - 必须处理请求的成功和失败状态,以便更新流程或给用户反馈。
内容的提问来源于stack exchange,提问作者ktj_jcbsn
相关产品推荐
相关产品推荐

