从零开始学前端 | 第四十一章:表单、提交与基础后端交互意识
本章定位
上一章,我们已经把 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. 再定义提交状态类型
typeSubmitStatus="idle"|"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] = useState<SubmitStatus>("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().length<10){return"留言内容至少需要 10 个字。";}returnnull;}2. 真实项目会怎么做
真实项目里,常常会使用更严格的校验工具,例如Zod。
但当前阶段先用基础函数理解校验流程,会更容易建立主线。
十、提交处理函数的主线是什么
表单提交并不只是“点按钮”。
它通常是一条流程:
- 阻止浏览器默认刷新
- 清理旧提示
- 校验表单
- 进入提交中状态
- 发送请求
- 根据结果显示成功或失败
- 成功后清空表单
先看一个主线清楚的写法:
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) { 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){constresponse=awaitfetch("/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 type="submit" disabled={isSubmitting}> {isSubmitting ? "提交中..." : "提交留言"} </button>1. 这段代码解决了什么问题
它解决的是:
用户点击提交后,页面要清楚地告诉用户“正在处理”,同时避免重复点击。
2. 这是用户体验,也是数据安全意识
虽然它看起来只是按钮状态,但背后其实是在降低:
- 重复提交
- 用户误操作
- 状态不明确
这些风险。
十四、成功、失败、校验错误分别是什么
这一节非常重要。
很多初学者会把所有提示都混成一类。
但实际上它们表达的是不同问题。
1. 校验错误
校验错误通常发生在提交前。
例如:
- 邮箱不合法
- 留言太短
- 必填项没填
它表达的是:
当前输入本身不符合要求。
2. 提交失败
提交失败通常发生在请求后端时。
例如:
- 网络失败
- 后端返回错误
- 服务器暂时不可用
它表达的是:
输入可能没问题,但提交过程没有成功完成。
3. 提交成功
提交成功表示:
后端已经正常接收并处理了这次提交。
这三类状态一定要尽量分清楚。
十五、如何给用户一个清楚的结果提示
页面可以根据submitStatus和errorMessage显示不同提示。
例如:
{errorMessage ? ( <p className="form-message form-message--error">{errorMessage}</p> ) : null} {submitStatus === "success" ? ( <p className="form-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}from"next/server";exportasyncfunctionPOST(request:Request){constbody=awaitrequest.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.ts
2. 当前阶段一定要完全照这个结构吗
不用。
重点不是目录一模一样,而是:
你能不能说清楚每个文件在负责什么。
二十二、表单页面最容易踩的几个坑
这一节建议你认真看。
因为表单问题经常不是“完全不会写”,而是细节缺失导致体验很差。
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 项目完成度、优化意识和上线流程的下一步。
