前端请求进化史 | 从 Form 到 Server Actions
小麦2025年04月11日1868 字
大家好,我是小麦,今天我们来聊一个大家日常都会接触到的话题:前端请求。
发请求这件事情看似简单,但在长达 30 多年的 Web 发展史中,它的变化可谓是非常大的。
我们知道请求和响应的本质,是客户端和服务端之间交换 HTTP 报文和数据。
前端要做到这一点,需要借助运行时(比如浏览器、Node.js)来完成。
不同历史时期,在代码中完成这个操作的方式是有所不同的。
今天我们顺着标准的发展,从古早的表单提交开始,聊聊整个前端发请求的历史,以及最后你可能还不太了解的 Server Actions 技术。
在前后端还未分离的 1990 年代,Web 开发者主要是通过 Form 表单发送请求。
action 和 method 属性对应 HTTP 请求的地址和方法,要发送的数据通过 input 元素设置,最后由用户点击 submit 按钮完成请求发送。
表单提交也有许多高级封装,玩过 ASP.NET Web Forms 的朋友一定见过这样的前后端同构代码。
这是一种不需要 JavaScript 代码,发送同步请求的方法,同时伴随整个页面刷新。
虽然体验不是很好,但这也足够满足那个年代对于 Web 页面的需求。
随着人们对于网页操作体验的要求越来越高,异步请求技术很快就来了。
1999 年,微软提出了全新的 XHR(XMLHttpRequest)API,允许使用 JavaScript 发送异步请求,实现在不刷新页面的情况下,与服务器交换数据并更新网页内容。
它写起来像这样。
原始的 XHR 在易用性、数据解析和错误处理方面存在劣势,对开发者来说并不友好。
早期的 Web 开发者一定写过这样的回调地狱代码,看着就十分难受。
为了解决 XHR 的问题,5 年之后(2005),AJAX 技术横空出世,与其说 AJAX 是一种新技术,不如说它是对 XHR 的进一步封装。
最典型的代表就是 jQuery 的 ajax,它写起来像这样。
相比 XHR 的写法它更加人性化,不需要开发者手动创建 XHR 对象,判断响应状态以及解析响应体。
但是 AJAX 封装依然不能解决回调地狱问题,比如开发者时常写出这样的代码。
在 JavaScript 回调范式的约束下,我们虽然可以通过重构来优化代码,但很难从根本上解决问题。
不同的 AJAX 封装回调写法也不尽相同。
为了规范异步编程,Promise/A+ 标准于 2012 年提出。
Promise 标准将一个异步操作分为 pending、fulfilled 和 rejected 三种状态,并规定使用 then 方法获取 resolve 出来的结果,使用 catch 方法获取 reject 出来的结果。
在这个时期,基于 XHR 并符合 Promise 规范的请求库百花齐放,比较著名的有 superagent(2011) 和 axios(2014) 等。
axios 使得开发者可以这样编写请求代码。
虽然这看起来和 jQuery 的 ajax 没什么两样,但拥抱 Promise 规范为后来支持 async/await 语法奠定了基础。
此时市面上已经有相当多的请求库了,但由于都依赖 XHR,也从底层锁死了它们的功能。
这个时候开发者也犯难了,发个请求还要从一大堆库里挑一个,各有各的用法,总归不太方便,不如咱们定一个标准,大家用一个好了。
于是在 2015 年,Fetch API 出现了,它是 XHR 的正式继任者,完善了异步请求需要考虑的方方面面。
相较于 XHR,Fetch API 支持 Promise 标准,可以处理流式响应,具备自由度更高的 cache 配置等等。
它的用法看起来像这样。
虽然和那些三方库大差不差,但重点在于 Fetch API 的标准化,以及直接由运行时提供。
这意味着我们不再需要安装依赖,也几乎不需要考虑环境差异。
两年后的 2017 年,ES8 准标发布,带来了 async/await 语法。
它规定使用 async 关键字将函数标记为异步函数,使用 await 关键字等待 Promise 对象完成。
至此,Promise 和 async/await 成功会师,开发者终于可以按照扁平的顺序结构书写异步代码,彻底摆脱了回调地狱的障碍。
如今 8 年过去了,发请求这件事又有哪些变化呢?
首先,Fetch API 已经成为 Web 请求的事实标准,
axios 在 2024 年 5 月内置了 Fetch Adapter。
Node.js 从 v21 版本也开始正式支持 Fetch API。
在框架领域,React 在 v18 版本中提出了一个叫做 Server Actions 的新概念,随后 Next.js 在 v14 版本中稳定了该功能。
它写起来像这样,客户端点击按钮调用了服务端的 increment 方法,随后拿到返回值,非常自然和符合直觉。
整个代码看起来没有任何发请求的操作,实际上框架内部基于 Fetch API 做了深度定制。
让我们检查浏览器网络请求,可以看到这是一个发往页面所在路由的 POST 请求。
请求头中带有 Next-Action,表示请求具体哪个服务端方法。
在响应头中可以看到 Content-Type 为 text/x-component,这是框架自定义的一种专用响应格式。
在这个例子中具体长这样,对应服务端 increment 方法的返回值 1。
那么这个请求是如何发起的呢?
从 Initiator 下的 Request call stack 中跟踪调用代码,可以看到 await fetch("") 字样,显然就是使用了 Fetch API。
Server Actions 的优势在于完全消除了请求样板代码,特别适合基于 JavaScript 的全栈应用开发。
劣势在于和框架严格绑定,不具备通用性;新方案也缺少最佳实践,容易乱写。
前端发送异步请求这件事,从 XHR 时代过渡到使用 async/await 的 Fetch 标准,花了近 20 年的时间。
经历了从语言本身的迭代、三方库的洗礼,到 Web 标准的变迁。
可以毫不夸张地说,发请求的历史也是 JS 生态和异步编程范式的发展史。
每个时代都有它对应的技术特色,技术没有银弹,选择最适合自己的才是最好的。
那么这就是本期视频的全部内容了,如果觉得好的话记得给个一键三连。
我是小麦,一位热爱分享的前端工程师,期待和你分享更多硬核、有用的内容。