从零开始学前端 | 第四十一章:表单、提交与基础后端交互意识

从零开始学前端 | 第四十一章:表单、提交与基础后端交互意识
本章定位上一章我们已经把 Next.js 项目从“页面结构与数据展示”继续推进到了“项目结构整理与组件复用”的阶段。你已经开始理解这些很重要的工程意识页面组件和通用组件的职责不一样。组件复用的前提是边界清楚。样式文件、工具函数、常量配置也需要有组织地放置。项目越往后做越要重视结构分层。也就是说到现在为止你已经不只是会搭页面、拿数据、渲染内容也开始知道一个项目继续变大时应该怎样保持清楚。接下来我们要进入一个非常贴近真实项目的场景表单、提交与基础后端交互。表单非常常见。例如联系我们表单留言表单登录表单注册表单搜索表单反馈表单这些表单看起来只是几个输入框和一个按钮但真正写起来会牵涉很多问题用户输入的内容放在哪里提交前要不要校验正在提交时按钮要不要禁用提交成功后要给什么提示提交失败后页面应该怎么反馈数据最终是留在前端还是要发给后端这些问题说明表单并不是简单的“输入框堆叠”。它真正连接的是用户输入、页面状态、数据校验、接口请求和后端处理。这一章我们会从一个适合初学者理解的“联系表单”出发建立 Next.js 项目里最基础的表单提交和前后端交互意识。本章学习目标学完这一章后你应该能做到理解为什么表单体验会直接影响项目完成度。知道 Next.js 项目里表单仍然离不开 React 的受控组件基础。理解什么时候需要把组件标记为客户端组件。学会设计一个基础联系表单的数据结构。学会区分表单值、校验错误、提交状态和结果提示。理解提交前校验为什么很重要。学会为提交按钮增加禁用和加载中反馈。理解成功、失败、校验错误三种状态分别在表达什么。初步理解前端提交数据到后端的基础流程。知道后端也必须做校验不能只相信前端。初步认识 Next.js Route Handler 的用途。为下一章学习项目优化与上线基础做好准备。一、为什么表单是前端项目里非常重要的一关表单看起来很普通但它往往是用户和系统真正发生关系的地方。例如用户通过登录表单进入系统用户通过留言表单提交反馈用户通过联系表单发送需求用户通过订单表单完成购买也就是说表单不是单纯展示内容而是在收集用户输入。这就带来了一个重要区别展示页面更多是在“给用户看”表单页面则是在“让用户参与进来”。一旦用户参与进来页面就必须考虑更多细节用户有没有填错用户点了提交后有没有反馈请求失败时用户知不知道怎么办重复点击会不会提交多次后端收到的数据是不是可靠所以表单体验会非常直接地影响项目完成度。二、Next.js 表单仍然离不开 React 基础虽然现在我们已经进入 Next.js 阶段但表单的很多基础并没有变。你前面在 React 阶段已经学过受控组件 输入框的值来自状态输入变化时再更新状态。这个思路在 Next.js 里依然非常重要。例如const [userName, setUserName] useState(); input value{userName} onChange{function (event) { setUserName(event.target.value); }} /1. 为什么这不是重复学习因为 Next.js 是基于 React 的。你在 React 阶段学过的useState事件处理受控输入框表单提交条件渲染在 Next.js 表单里都会继续用到。2. Next.js 新增的重点是什么Next.js 阶段新增的重点在于表单不只是前端内部状态还可能要和后端接口发生交互。也就是说表单开始从“页面内部交互”走向“前后端协作”。三、什么时候需要客户端组件在 Next.js 的 App Router 中有一个对初学者很重要的概念客户端组件如果一个组件需要使用浏览器里的交互能力比如useStateonChangeonSubmit点击事件输入框实时变化通常就需要把它标记为客户端组件。写法是在文件最顶部加use client;1. 为什么表单组件经常是客户端组件因为表单通常需要记录用户输入响应输入变化处理提交事件显示提交中、成功、失败等状态这些都属于浏览器里的交互逻辑。2. 当前阶段先怎么理解就够了你可以先记住一句话需要用 React 状态和事件响应用户操作的组件通常会是客户端组件。后面你会逐步接触更细的客户端组件和服务端组件边界。四、先设计一个联系表单这一章我们用一个很常见的场景来练习联系表单它包含三个字段姓名邮箱留言内容这个表单不复杂但足够覆盖很多关键点输入值管理表单校验提交状态成功提示失败提示后端接收1. 先定义表单值类型interfaceContactFormValue{userName:string;email:string;message:string;}2. 再定义提交状态类型typeSubmitStatusidle|submitting|success|error;3. 为什么这两个类型值得一开始就定义因为它们分别表达两类不同问题ContactFormValue表达用户输入了什么SubmitStatus表达当前提交流程进行到哪一步这就是前面反复强调的状态一多时先分清它们各自在表达什么。五、先准备初始表单值一个表单通常都需要一个稳定的初始值。例如constinitialContactFormValue:ContactFormValue{userName:,email:,message:};1. 为什么不要到处手写空对象因为这个初始值后面会在多个地方用到。例如页面第一次加载提交成功后清空表单用户点击重置把它集中定义会让表单恢复默认状态时更清楚。2. 当前阶段最值得记住什么你可以先记住表单的默认值也是表单设计的一部分。它不只是随手写几个空字符串。六、联系表单里通常会有哪些状态一个稍微完整的表单页面通常至少会有下面这些状态const [formValue, setFormValue] useState(initialContactFormValue); const [errorMessage, setErrorMessage] useState(); const [submitStatus, setSubmitStatus] useStateSubmitStatus(idle);1.formValue表达什么它表达的是用户当前在表单里输入了什么。2.errorMessage表达什么它表达的是当前表单有什么需要提示用户的问题。3.submitStatus表达什么它表达的是当前提交流程处于空闲、提交中、成功还是失败。这三类状态分开以后页面会清楚很多。七、统一处理输入变化如果表单字段不多可以一个字段写一个onChange。但当字段稍微变多时更常见的是写一个统一更新函数。例如function updateFormValue(field: keyof ContactFormValue, value: string) { setFormValue(function (currentValue) { return { ...currentValue, [field]: value }; }); }1. 这段代码在做什么它的作用是根据字段名只更新表单对象里的某一个字段。2. 为什么这里用函数式更新因为它可以基于当前最新状态生成下一份状态。这在表单对象更新时是一个很稳的写法。八、把输入框接到状态上有了formValue和updateFormValue之后输入框就可以这样写input value{formValue.userName} onChange{function (event) { updateFormValue(userName, event.target.value); }} placeholder请输入姓名 /邮箱输入框也是类似的input value{formValue.email} onChange{function (event) { updateFormValue(email, event.target.value); }} placeholder请输入邮箱 /1. 这里最核心的仍然是什么仍然是受控组件输入框显示什么由状态决定用户输入变化再更新状态。2. 为什么这个基础不能丢因为后面的校验、提交、清空表单、禁用按钮都会围绕这份状态展开。九、提交前为什么要校验用户输入的数据不一定可靠。例如姓名为空邮箱格式不对留言太短用户只输入了空格如果不校验后面就会出现很多问题后端收到无效数据用户不知道哪里填错了页面看起来像提交了但实际没有意义所以提交前校验非常重要。1. 当前阶段先做基础校验就够了例如functionvalidateContactForm(value:ContactFormValue):string|null{if(!value.userName.trim()){return请填写姓名。;}if(!value.email.includes()){return请填写正确的邮箱地址。;}if(value.message.trim().length10){return留言内容至少需要 10 个字。;}returnnull;}2. 真实项目会怎么做真实项目里常常会使用更严格的校验工具例如Zod。但当前阶段先用基础函数理解校验流程会更容易建立主线。十、提交处理函数的主线是什么表单提交并不只是“点按钮”。它通常是一条流程阻止浏览器默认刷新清理旧提示校验表单进入提交中状态发送请求根据结果显示成功或失败成功后清空表单先看一个主线清楚的写法async function handleSubmit(event: React.FormEventHTMLFormElement) { event.preventDefault(); setErrorMessage(); const validationMessage validateContactForm(formValue); if (validationMessage) { setErrorMessage(validationMessage); return; } await submitContactForm(); }1. 为什么先不要把所有逻辑堆在这里因为handleSubmit最适合表达提交流程怎么走。而具体的请求逻辑可以继续单独放到submitContactForm里。2. 这和前面哪条原则一致这和前面讲过的函数只做一件事页面结构要清楚是一致的。十一、把请求提交逻辑单独封装出来下面是一段当前阶段可以理解的提交逻辑async function submitContactForm() { setSubmitStatus(submitting); try { await sendContactMessage(formValue); setSubmitStatus(success); setFormValue(initialContactFormValue); } catch { setSubmitStatus(error); setErrorMessage(提交失败请稍后再试。); } }1. 为什么这里有try / catch因为请求后端并不一定永远成功。可能会出现网络失败接口异常后端校验不通过所以提交逻辑一定要考虑失败情况。2. 成功后为什么要清空表单因为用户已经完成了一次有效提交。清空表单可以让页面回到一个稳定状态并告诉用户这次提交已经处理完了。十二、前端怎么把数据发给后端当前阶段你可以先用fetch理解最基础的请求提交。例如exportasyncfunctionsendContactMessage(value:ContactFormValue){constresponseawaitfetch(/api/contact,{method:POST,headers:{Content-Type:application/json},body:JSON.stringify(value)});if(!response.ok){thrownewError(提交失败);}}1. 这段代码在做什么它把表单数据通过POST请求发送到/api/contact2. 为什么要JSON.stringify因为请求体发送的是字符串形式的数据。所以要先把对象转成 JSON 字符串。这和前面你学过的本地存储数据转换思路很像。十三、提交按钮为什么要有禁用和加载中状态如果用户点了提交后页面没有任何变化他可能会连续点很多次。这会带来问题重复提交用户焦虑后端收到多份重复数据所以提交中状态非常重要。例如const isSubmitting submitStatus submitting; button typesubmit disabled{isSubmitting} {isSubmitting ? 提交中... : 提交留言} /button1. 这段代码解决了什么问题它解决的是用户点击提交后页面要清楚地告诉用户“正在处理”同时避免重复点击。2. 这是用户体验也是数据安全意识虽然它看起来只是按钮状态但背后其实是在降低重复提交用户误操作状态不明确这些风险。十四、成功、失败、校验错误分别是什么这一节非常重要。很多初学者会把所有提示都混成一类。但实际上它们表达的是不同问题。1. 校验错误校验错误通常发生在提交前。例如邮箱不合法留言太短必填项没填它表达的是当前输入本身不符合要求。2. 提交失败提交失败通常发生在请求后端时。例如网络失败后端返回错误服务器暂时不可用它表达的是输入可能没问题但提交过程没有成功完成。3. 提交成功提交成功表示后端已经正常接收并处理了这次提交。这三类状态一定要尽量分清楚。十五、如何给用户一个清楚的结果提示页面可以根据submitStatus和errorMessage显示不同提示。例如{errorMessage ? ( p classNameform-message form-message--error{errorMessage}/p ) : null} {submitStatus success ? ( p classNameform-message form-message--success 留言已提交我们会尽快联系你。 /p ) : null}1. 为什么提示文案很重要因为用户需要知道我哪里填错了我有没有提交成功失败了接下来该怎么办2. 好的提示应该是什么样至少应该做到说人话指出问题给出下一步建议例如“提交失败请稍后再试”就比什么都不显示好很多。十六、基础前后端交互流程是什么现在我们把整条链路串起来。一个最基础的表单提交流程可以理解成用户在浏览器里填写表单。前端收集输入值。前端做基础校验。前端通过请求把数据发送给后端。后端接收请求。后端再次校验数据。后端处理数据。后端返回成功或失败结果。前端根据结果更新页面提示。这条链路非常重要。因为它让你第一次真正看到前端不是只在浏览器里自娱自乐而是在和后端系统协作完成一件事。十七、为什么后端也必须校验这一点一定要尽早建立。很多初学者会以为前端已经校验过了后端是不是就不用管了不可以。因为前端校验主要是为了给用户及时反馈提升体验。但后端校验是为了保证真正进入系统的数据是可靠的。前端代码可以被绕过接口可以被直接调用用户也可能发送不符合预期的数据。所以真实项目里一定要记住前端校验提升体验后端校验保证底线。十八、Next.js 里后端接口可以先怎么理解在 Next.js 里我们可以通过 Route Handler 先理解最基础的接口。例如文件位置app/api/contact/route.ts它可以对应一个接口地址/api/contact当前阶段你先这样理解就够了route.ts可以用来接收前端发来的请求并返回响应。这就是 Next.js 提供的一种基础后端能力。十九、先看一个最基础的 Route Handler下面是一个非常简化的示意import{NextResponse}fromnext/server;exportasyncfunctionPOST(request:Request){constbodyawaitrequest.json();if(!body.email||!body.message){returnNextResponse.json({message:参数不完整},{status:400});}returnNextResponse.json({message:提交成功});}1. 这段代码在做什么它大致做了三件事接收前端提交的数据做最基础的字段检查返回成功或失败结果2. 为什么这只是入门示意因为真实项目还需要考虑更多事情例如更严格的数据校验数据库存储发送邮件防止垃圾提交日志记录当前阶段先理解“请求进来后端处理响应出去”这条主线就够了。二十、为什么不能把接口当成黑盒学前端时很多人会把接口想成后端给我一个地址我把数据发过去就行。这当然是前后端协作的一部分。但如果你完全不理解接口背后发生了什么后面会很容易在这些问题上卡住为什么接口返回 400为什么请求体格式不对为什么后端说缺字段为什么明明前端校验过后端还是报错所以当前阶段要建立的是基础后端交互意识而不是马上变成后端工程师。你只需要先明白前端发出的每一次提交后端都要接收、理解、校验和响应。二十一、一个联系表单页面可以怎样分层结合上一章的项目分层意识一个表单页面可以先这样组织src/ ├── app/ │ ├── contact/ │ │ └── page.tsx │ └── api/ │ └── contact/ │ └── route.ts ├── components/ │ └── contact-form.tsx ├── lib/ │ └── send-contact-message.ts └── types/ └── contact.ts1. 为什么这个结构比较清楚因为它把职责分开了页面入口放在app/contact/page.tsx表单界面放在components/contact-form.tsx请求函数放在lib/send-contact-message.ts类型定义放在types/contact.ts接口处理放在app/api/contact/route.ts2. 当前阶段一定要完全照这个结构吗不用。重点不是目录一模一样而是你能不能说清楚每个文件在负责什么。二十二、表单页面最容易踩的几个坑这一节建议你认真看。因为表单问题经常不是“完全不会写”而是细节缺失导致体验很差。1. 坑一只写输入框不做校验这样用户填错也不知道哪里错。2. 坑二提交时没有禁用按钮这样用户可能连续点击造成重复提交。3. 坑三只处理成功不处理失败这样一旦接口失败用户就不知道发生了什么。4. 坑四错误提示写得太抽象例如只写“错误”用户根本不知道怎么改。5. 坑五以为前端校验能替代后端校验前端校验不能保证数据安全只能提升用户体验。6. 坑六把请求逻辑、校验逻辑、表单 JSX 全部塞在一起短期能跑后面会越来越难维护。二十三、本章实践练习这一章的练习重点是把“输入、校验、提交、反馈”这条链路真正跑顺。1. 练习 1做一个基础联系表单请你做一个包含下面字段的表单姓名邮箱留言内容要求使用受控组件管理输入值点击提交时阻止默认刷新提交成功后清空表单这个练习会帮助你巩固最基础的表单主线。2. 练习 2补充基础校验请你为表单补充姓名不能为空邮箱必须包含留言不能少于 10 个字这个练习会帮助你理解提交前校验是表单体验的基础。3. 练习 3模拟提交成功、失败和校验错误请你分别模拟三种情况输入不合法显示校验错误接口返回失败显示失败提示提交成功显示成功提示这个练习会帮助你真正建立表单页面不只有一种状态。4. 练习 4给提交按钮增加禁用和加载中反馈请你做到提交时按钮不可点击按钮文字变成“提交中…”请求结束后恢复按钮状态这个练习会帮助你理解提交状态本身就是用户体验的一部分。二十四、学习重点提示这一章请你重点记住下面这些话表单是用户和系统真正发生交互的重要入口。Next.js 表单仍然离不开 React 的受控组件、状态和事件基础。需要使用useState、onChange、onSubmit的表单组件通常会是客户端组件。表单状态至少要考虑输入值、校验错误、提交状态和结果提示。提交前校验是为了提升体验但不能替代后端校验。提交按钮需要加载中和禁用状态避免重复提交。成功、失败、校验错误表达的是不同问题不要混成一类。前后端交互的基础流程是前端收集数据发送请求后端校验处理再把结果返回给前端。表单页面越真实越要重视状态反馈完整性。如果你只记一句话请记住这一章真正要建立的不只是“会写一个表单”而是“会让用户输入、提交状态、校验反馈和后端交互形成一条清楚的流程”。二十五、本章小结这一章我们正式把 Next.js 从“页面展示和项目结构整理”推进到了“用户输入与前后端交互”的阶段。你已经理解了为什么表单体验会直接影响项目完成度Next.js 表单为什么仍然离不开 React 的受控组件基础为什么交互表单通常需要客户端组件联系表单的基础状态应该如何设计为什么提交前要做校验为什么提交中要禁用按钮并显示加载反馈成功、失败、校验错误三种状态分别表达什么前端提交数据到后端的基础流程为什么后端也必须做校验Next.js Route Handler 可以怎样接收基础请求更重要的是你开始真正建立一种很关键的前后端协作意识表单不是几个输入框加一个按钮而是一条从用户输入到后端处理再回到页面反馈的完整链路。这一步非常关键。因为从这里开始你已经不只是在做静态内容展示而是在真正接触Next.js 项目里更接近真实业务交互的基础流程。二十六、课后思考题请你认真思考下面这些问题为什么说表单体验会直接影响项目完成度为什么 Next.js 表单仍然离不开 React 受控组件基础什么情况下一个表单组件通常需要写use client表单值、校验错误、提交状态和结果提示分别在表达什么为什么提交前校验不能替代后端校验为什么提交按钮需要禁用和加载中状态一个最基础的前后端表单提交流程可以拆成哪几步建议你把这些问题用自己的话写下来。只要你能把这些问题讲清楚说明你已经真正进入 Next.js 表单提交与基础后端交互的主线了。二十七、下一篇预告下一章我们会继续进入从零开始学前端 | 第四十二章项目优化与上线基础你会开始真正接触这些内容页面性能和首屏体验的基础意识图片、代码结构和配置项为什么需要优化环境变量是什么一个项目从开发到上线大致要经历哪些步骤也就是说下一章开始我们会从“表单、提交与基础后端交互意识”继续走到Next.js 项目完成度、优化意识和上线流程的下一步。