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

📅 2026/7/23 18:36:11 👁️ 阅读次数 📝 编程学习
从零开始学前端 | 第四十一章:表单、提交与基础后端交互意识

本章定位

上一章,我们已经把 Next.js 项目从“页面结构与数据展示”继续推进到了“项目结构整理与组件复用”的阶段。

你已经开始理解这些很重要的工程意识:

  1. 页面组件和通用组件的职责不一样。
  2. 组件复用的前提是边界清楚。
  3. 样式文件、工具函数、常量配置也需要有组织地放置。
  4. 项目越往后做,越要重视结构分层。

也就是说,到现在为止,你已经不只是会搭页面、拿数据、渲染内容,也开始知道:

一个项目继续变大时,应该怎样保持清楚。

接下来,我们要进入一个非常贴近真实项目的场景:

表单、提交与基础后端交互。

表单非常常见。

例如:

  • 联系我们表单
  • 留言表单
  • 登录表单
  • 注册表单
  • 搜索表单
  • 反馈表单

这些表单看起来只是几个输入框和一个按钮,但真正写起来会牵涉很多问题:

  1. 用户输入的内容放在哪里?
  2. 提交前要不要校验?
  3. 正在提交时按钮要不要禁用?
  4. 提交成功后要给什么提示?
  5. 提交失败后页面应该怎么反馈?
  6. 数据最终是留在前端,还是要发给后端?

这些问题说明,表单并不是简单的“输入框堆叠”。

它真正连接的是:

用户输入、页面状态、数据校验、接口请求和后端处理。

这一章,我们会从一个适合初学者理解的“联系表单”出发,建立 Next.js 项目里最基础的表单提交和前后端交互意识。

本章学习目标

学完这一章后,你应该能做到:

  1. 理解为什么表单体验会直接影响项目完成度。
  2. 知道 Next.js 项目里表单仍然离不开 React 的受控组件基础。
  3. 理解什么时候需要把组件标记为客户端组件。
  4. 学会设计一个基础联系表单的数据结构。
  5. 学会区分表单值、校验错误、提交状态和结果提示。
  6. 理解提交前校验为什么很重要。
  7. 学会为提交按钮增加禁用和加载中反馈。
  8. 理解成功、失败、校验错误三种状态分别在表达什么。
  9. 初步理解前端提交数据到后端的基础流程。
  10. 知道后端也必须做校验,不能只相信前端。
  11. 初步认识 Next.js Route Handler 的用途。
  12. 为下一章学习项目优化与上线基础做好准备。

一、为什么表单是前端项目里非常重要的一关

表单看起来很普通,但它往往是用户和系统真正发生关系的地方。

例如:

  • 用户通过登录表单进入系统
  • 用户通过留言表单提交反馈
  • 用户通过联系表单发送需求
  • 用户通过订单表单完成购买

也就是说,表单不是单纯展示内容,而是在收集用户输入。

这就带来了一个重要区别:

展示页面更多是在“给用户看”,表单页面则是在“让用户参与进来”。

一旦用户参与进来,页面就必须考虑更多细节:

  1. 用户有没有填错?
  2. 用户点了提交后有没有反馈?
  3. 请求失败时用户知不知道怎么办?
  4. 重复点击会不会提交多次?
  5. 后端收到的数据是不是可靠?

所以表单体验会非常直接地影响项目完成度。

二、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 中,有一个对初学者很重要的概念:

客户端组件

如果一个组件需要使用浏览器里的交互能力,比如:

  • useState
  • onChange
  • onSubmit
  • 点击事件
  • 输入框实时变化

通常就需要把它标记为客户端组件。

写法是在文件最顶部加:

"use client";

1. 为什么表单组件经常是客户端组件

因为表单通常需要:

  1. 记录用户输入
  2. 响应输入变化
  3. 处理提交事件
  4. 显示提交中、成功、失败等状态

这些都属于浏览器里的交互逻辑。

2. 当前阶段先怎么理解就够了

你可以先记住一句话:

需要用 React 状态和事件响应用户操作的组件,通常会是客户端组件。

后面你会逐步接触更细的客户端组件和服务端组件边界。

四、先设计一个联系表单

这一章我们用一个很常见的场景来练习:

联系表单

它包含三个字段:

  1. 姓名
  2. 邮箱
  3. 留言内容

这个表单不复杂,但足够覆盖很多关键点:

  • 输入值管理
  • 表单校验
  • 提交状态
  • 成功提示
  • 失败提示
  • 后端接收

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. 为什么这里用函数式更新

因为它可以基于当前最新状态生成下一份状态。

这在表单对象更新时是一个很稳的写法。

八、把输入框接到状态上

有了formValueupdateFormValue之后,输入框就可以这样写:

<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. 后端收到无效数据
  2. 用户不知道哪里填错了
  3. 页面看起来像提交了,但实际没有意义

所以提交前校验非常重要。

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

但当前阶段先用基础函数理解校验流程,会更容易建立主线。

十、提交处理函数的主线是什么

表单提交并不只是“点按钮”。

它通常是一条流程:

  1. 阻止浏览器默认刷新
  2. 清理旧提示
  3. 校验表单
  4. 进入提交中状态
  5. 发送请求
  6. 根据结果显示成功或失败
  7. 成功后清空表单

先看一个主线清楚的写法:

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/contact

2. 为什么要JSON.stringify

因为请求体发送的是字符串形式的数据。

所以要先把对象转成 JSON 字符串。

这和前面你学过的本地存储数据转换思路很像。

十三、提交按钮为什么要有禁用和加载中状态

如果用户点了提交后页面没有任何变化,他可能会连续点很多次。

这会带来问题:

  • 重复提交
  • 用户焦虑
  • 后端收到多份重复数据

所以提交中状态非常重要。

例如:

const isSubmitting = submitStatus === "submitting"; <button type="submit" disabled={isSubmitting}> {isSubmitting ? "提交中..." : "提交留言"} </button>

1. 这段代码解决了什么问题

它解决的是:

用户点击提交后,页面要清楚地告诉用户“正在处理”,同时避免重复点击。

2. 这是用户体验,也是数据安全意识

虽然它看起来只是按钮状态,但背后其实是在降低:

  • 重复提交
  • 用户误操作
  • 状态不明确

这些风险。

十四、成功、失败、校验错误分别是什么

这一节非常重要。

很多初学者会把所有提示都混成一类。

但实际上它们表达的是不同问题。

1. 校验错误

校验错误通常发生在提交前。

例如:

  • 邮箱不合法
  • 留言太短
  • 必填项没填

它表达的是:

当前输入本身不符合要求。

2. 提交失败

提交失败通常发生在请求后端时。

例如:

  • 网络失败
  • 后端返回错误
  • 服务器暂时不可用

它表达的是:

输入可能没问题,但提交过程没有成功完成。

3. 提交成功

提交成功表示:

后端已经正常接收并处理了这次提交。

这三类状态一定要尽量分清楚。

十五、如何给用户一个清楚的结果提示

页面可以根据submitStatuserrorMessage显示不同提示。

例如:

{errorMessage ? ( <p className="form-message form-message--error">{errorMessage}</p> ) : null} {submitStatus === "success" ? ( <p className="form-message form-message--success"> 留言已提交,我们会尽快联系你。 </p> ) : null}

1. 为什么提示文案很重要

因为用户需要知道:

  • 我哪里填错了
  • 我有没有提交成功
  • 失败了接下来该怎么办

2. 好的提示应该是什么样

至少应该做到:

  1. 说人话
  2. 指出问题
  3. 给出下一步建议

例如“提交失败,请稍后再试”就比什么都不显示好很多。

十六、基础前后端交互流程是什么

现在我们把整条链路串起来。

一个最基础的表单提交流程可以理解成:

  1. 用户在浏览器里填写表单。
  2. 前端收集输入值。
  3. 前端做基础校验。
  4. 前端通过请求把数据发送给后端。
  5. 后端接收请求。
  6. 后端再次校验数据。
  7. 后端处理数据。
  8. 后端返回成功或失败结果。
  9. 前端根据结果更新页面提示。

这条链路非常重要。

因为它让你第一次真正看到:

前端不是只在浏览器里自娱自乐,而是在和后端系统协作完成一件事。

十七、为什么后端也必须校验

这一点一定要尽早建立。

很多初学者会以为:

前端已经校验过了,后端是不是就不用管了?

不可以。

因为前端校验主要是为了:

给用户及时反馈,提升体验。

但后端校验是为了:

保证真正进入系统的数据是可靠的。

前端代码可以被绕过,接口可以被直接调用,用户也可能发送不符合预期的数据。

所以真实项目里一定要记住:

前端校验提升体验,后端校验保证底线。

十八、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. 这段代码在做什么

它大致做了三件事:

  1. 接收前端提交的数据
  2. 做最基础的字段检查
  3. 返回成功或失败结果

2. 为什么这只是入门示意

因为真实项目还需要考虑更多事情,例如:

  • 更严格的数据校验
  • 数据库存储
  • 发送邮件
  • 防止垃圾提交
  • 日志记录

当前阶段先理解“请求进来,后端处理,响应出去”这条主线就够了。

二十、为什么不能把接口当成黑盒

学前端时,很多人会把接口想成:

后端给我一个地址,我把数据发过去就行。

这当然是前后端协作的一部分。

但如果你完全不理解接口背后发生了什么,后面会很容易在这些问题上卡住:

  1. 为什么接口返回 400?
  2. 为什么请求体格式不对?
  3. 为什么后端说缺字段?
  4. 为什么明明前端校验过,后端还是报错?

所以当前阶段要建立的是基础后端交互意识,而不是马上变成后端工程师。

你只需要先明白:

前端发出的每一次提交,后端都要接收、理解、校验和响应。

二十一、一个联系表单页面可以怎样分层

结合上一章的项目分层意识,一个表单页面可以先这样组织:

src/ ├── app/ │ ├── contact/ │ │ └── page.tsx │ └── api/ │ └── contact/ │ └── route.ts ├── components/ │ └── contact-form.tsx ├── lib/ │ └── send-contact-message.ts └── types/ └── contact.ts

1. 为什么这个结构比较清楚

因为它把职责分开了:

  • 页面入口放在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:做一个基础联系表单

请你做一个包含下面字段的表单:

  • 姓名
  • 邮箱
  • 留言内容

要求:

  1. 使用受控组件管理输入值
  2. 点击提交时阻止默认刷新
  3. 提交成功后清空表单

这个练习会帮助你巩固最基础的表单主线。

2. 练习 2:补充基础校验

请你为表单补充:

  1. 姓名不能为空
  2. 邮箱必须包含@
  3. 留言不能少于 10 个字

这个练习会帮助你理解:

提交前校验是表单体验的基础。

3. 练习 3:模拟提交成功、失败和校验错误

请你分别模拟三种情况:

  1. 输入不合法,显示校验错误
  2. 接口返回失败,显示失败提示
  3. 提交成功,显示成功提示

这个练习会帮助你真正建立:

表单页面不只有一种状态。

4. 练习 4:给提交按钮增加禁用和加载中反馈

请你做到:

  1. 提交时按钮不可点击
  2. 按钮文字变成“提交中…”
  3. 请求结束后恢复按钮状态

这个练习会帮助你理解:

提交状态本身就是用户体验的一部分。

二十四、学习重点提示

这一章请你重点记住下面这些话:

  1. 表单是用户和系统真正发生交互的重要入口。
  2. Next.js 表单仍然离不开 React 的受控组件、状态和事件基础。
  3. 需要使用useStateonChangeonSubmit的表单组件,通常会是客户端组件。
  4. 表单状态至少要考虑输入值、校验错误、提交状态和结果提示。
  5. 提交前校验是为了提升体验,但不能替代后端校验。
  6. 提交按钮需要加载中和禁用状态,避免重复提交。
  7. 成功、失败、校验错误表达的是不同问题,不要混成一类。
  8. 前后端交互的基础流程是:前端收集数据,发送请求,后端校验处理,再把结果返回给前端。
  9. 表单页面越真实,越要重视状态反馈完整性。

如果你只记一句话,请记住:

这一章真正要建立的,不只是“会写一个表单”,而是“会让用户输入、提交状态、校验反馈和后端交互形成一条清楚的流程”。

二十五、本章小结

这一章,我们正式把 Next.js 从“页面展示和项目结构整理”推进到了“用户输入与前后端交互”的阶段。

你已经理解了:

  • 为什么表单体验会直接影响项目完成度
  • Next.js 表单为什么仍然离不开 React 的受控组件基础
  • 为什么交互表单通常需要客户端组件
  • 联系表单的基础状态应该如何设计
  • 为什么提交前要做校验
  • 为什么提交中要禁用按钮并显示加载反馈
  • 成功、失败、校验错误三种状态分别表达什么
  • 前端提交数据到后端的基础流程
  • 为什么后端也必须做校验
  • Next.js Route Handler 可以怎样接收基础请求

更重要的是,你开始真正建立一种很关键的前后端协作意识:

表单不是几个输入框加一个按钮,而是一条从用户输入到后端处理再回到页面反馈的完整链路。

这一步非常关键。

因为从这里开始,你已经不只是在做静态内容展示,而是在真正接触:

Next.js 项目里更接近真实业务交互的基础流程。

二十六、课后思考题

请你认真思考下面这些问题:

  1. 为什么说表单体验会直接影响项目完成度?
  2. 为什么 Next.js 表单仍然离不开 React 受控组件基础?
  3. 什么情况下一个表单组件通常需要写"use client"
  4. 表单值、校验错误、提交状态和结果提示分别在表达什么?
  5. 为什么提交前校验不能替代后端校验?
  6. 为什么提交按钮需要禁用和加载中状态?
  7. 一个最基础的前后端表单提交流程可以拆成哪几步?

建议你把这些问题用自己的话写下来。

只要你能把这些问题讲清楚,说明你已经真正进入 Next.js 表单提交与基础后端交互的主线了。

二十七、下一篇预告

下一章我们会继续进入:

从零开始学前端 | 第四十二章:项目优化与上线基础

你会开始真正接触这些内容:

  • 页面性能和首屏体验的基础意识
  • 图片、代码结构和配置项为什么需要优化
  • 环境变量是什么
  • 一个项目从开发到上线,大致要经历哪些步骤

也就是说,下一章开始,我们会从“表单、提交与基础后端交互意识”,继续走到:

Next.js 项目完成度、优化意识和上线流程的下一步。