本篇依然来自于我们的 《前端周刊》 项目!
由团队成员 田八 翻译,欢迎大家 进群 持续追踪全球最新前端资讯!!
原文地址:css-tricks

四年后,我的"使用 WordPress REST API 进行无头表单提交"文章中的示例最终停止运行了。
这篇文章中包含了 CodePen
的嵌入,演示了如何使用流行的 WordPress
表单插件的 REST API
端点来捕获和显示验证错误和提交反馈,从而构建一个完全自定义的前端。这些代码演示依赖于我在后台运行的一个 WordPress
网站。但是在一次被迫的基础设施迁移过程中,该网站没能正确的被转移,更糟糕的是,我失去了对我的账户的访问权限。
当然,原本我是可以联系技术支持或者在其他地方恢复备份。但这种情况让我产生怀疑:如果这不是 WordPress
呢?如果它是一个我无法自托管或修复的第三方服务呢?有没有办法构建不会因为依赖的服务失败而中断的演示?我们如何确保教学示例尽可能长时间可用?
还是说这些是不可避免的?示例是否像网络上的其他一切一样,注定会停止运转?
与软件测试的相似之处
对于编写代码测试的人来说,长期以来他们一直都在与类似的问题作斗争,尽管表述方式有所不同。但核心问题是相同的。依赖关系,尤其是第三方依赖关系成为障碍,因为它们超出了控制范围。
毫不奇怪,消除外部依赖问题最靠谱的方法是将外部服务完全移除,从而有效地与其解耦。当然,怎么做到这一点,以及是否每次都是可行的,需要视具体情况而定。
事实上,在使示例能够快速还原这方面,处理依赖关系的技术同样有用。
为了更具体说明,我会使用前面提到的 CodePen
演示作为示例。同样的方法在很多其他的情况下也同样有效。
解耦 REST API
依赖关系
尽管有很多策略和技巧,但打破对 REST API
的依赖的两种最常见方法是:
- 在代码中模拟
HTTP
调用,不是执行真正的网络请求,而是返回备份的响应体 - 使用模拟
API
服务器作为真实服务的替代品,并以类似的方式提供预定义的响应
两者都有利弊,我们稍后再讨论。
使用拦截器模拟响应
在现代测试框架中,无论是用于单元测试还是端到端测试(例如Jest
或Playwright
),都提供内置的Mock
功能。
然而,我们并不一定需要这些,反正我们也不能在 CodePen
中使用它们。相反,我们可以对Fetch API
进行 monkey patch
来拦截请求并返回模拟响应。当无法更改原始源代码时使用 monkey patch
,我们可以通过覆盖现有函数来引入新的行为。
它实现起来如下:
javascript
const fetchWPFormsRestApiInterceptor = (fetch) => async (
resource,
options = {}
) => {
// 确保我们处理的是预期的数据
if (typeof resource !== "string" || !(options.body instanceof FormData)) {
return fetch(resource, options);
}
if (resource.match(/wp-json/contact-form-7/)) {
return contactForm7Response(options.body);
}
if (resource.match(/wp-json/gf/)) {
return gravityFormsResponse(options.body);
}
return fetch(resource, options);
};
window.fetch = fetchWPFormsRestApiInterceptor(window.fetch);
我们用自己的版本覆盖默认的fetch
,该版本在特定条件添加了自定义逻辑,否则保持原有的行为不变。
替换函数fetchWPFormsRestApiInterceptor
的作用类似于拦截器。拦截器是一种根据特定条件修改请求或响应的设计模式。
许多 HTTP
库,例如曾经风靡一时的axios,都提供了一个方便的 API
来添加拦截器,而无需依赖 monkey patching
。在管理多次重写覆盖时,很容易无意中产生细微的 bug
或引发冲突,因此应该谨慎使用monkey patching
。
有了拦截器,返回Mock
响应就像调用response
对象的静态JSON
方法一样简单:
ini
const contactForm7Response = (formData) => {
const body = {}
return Response.json(body);
};
根据需要,响应可以是纯文本
、Blob
或ArrayBuffer
的任何内容。还可以指定自定义状态代码,并包含其他响应标头。
对于 CodePen
的演示,构建的响应体的结构可能如下:
php
const contactForm7Response = (formData) => {
const submissionSuccess = {
into: "#",
status: "mail_sent",
message: "Thank you for your message. It has been sent.!",
posted_data_hash: "d52f9f9de995287195409fe6dcde0c50"
};
const submissionValidationFailed = {
into: "#",
status: "validation_failed",
message:
"One or more fields have an error. Please check and try again.",
posted_data_hash: "",
invalid_fields: []
};
if (!formData.get("somebodys-name")) {
submissionValidationFailed.invalid_fields.push({
into: "span.wpcf7-form-control-wrap.somebodys-name",
message: "This field is required.",
idref: null,
error_id: "-ve-somebodys-name"
});
}
// 或者以一种更彻底的方式检查电子邮件地址的有效性
if (!/^[^\s@]+@[^\s@]+.[^\s@]+$/.test(formData.get("any-email"))) {
submissionValidationFailed.invalid_fields.push({
into: "span.wpcf7-form-control-wrap.any-email",
message: "The email address entered is invalid.",
idref: null,
error_id: "-ve-any-email"
});
}
// 返回成功或验证失败的响应
const body = !submissionValidationFailed.invalid_fields.length
? submissionSuccess
: submissionValidationFailed;
return Response.json(body);
};
此时,任何使用fetch
调用的URL
如果包含wp-json/contact-form-7
,都会返回Mock
的成功或验证错误响应,具体取决于表单输入。
现在让我们将其与 Mock API
服务方法进行对比。
使用serverless
的 Mock API
服务
运行传统托管模式的 Mock API
服务器,会重新带来可用性、维护和成本方面的问题。尽管围绕serverless
功能的炒作已逐渐平息,但我们可以使用它们来规避这些问题。
而且由于DigitalOcean Functions慷慨的提供了免费套餐,创建 Mock API
实际上是免费的,只需要手动模拟即可。
对于简单的用例,所有操作都可以通过 Functions
控制面板完成,包括在内置编辑器中编写代码。观看这段简洁的演示视频,了解实际操作:
视频地址在当前平台不支持预览,请点击链接查看。
对于更复杂的需求,Functions
可以在本地开发,并使用 doctl(DigitalOcean 的 CLI)进行部署。
为了返回Mock
响应,若我们为每个端点单独创建一个Function
,这样操作会更简单,因为这样能避免添加不必要的条件判断。幸运的是,我们可以继续使用 JavaScript(Node.js)
,并且和 contactForm7Response
几乎相同的基础配置开始:
csharp
function main(event) {
const body = {};
return { body };
}
我们必须将处理函数命名为 main
,它会在端点被调用时触发。该函数的第一个参数是event
对象,其中包含请求的详细信息。同样,我们可以返回任何内容,如果需要返回我们需要的 JSON
响应,只需直接返回一个对象即可。
我们可以复用创建响应的相同代码。唯一的区别在于,需要我们自己从event
对象中提取表单输入数据:
kotlin
function main(event) {
// 如何从 event 中获取 FormData?
const formData = new FormData();
const submissionSuccess = {
// ...
};
const submissionValidationFailed = {
// ...
};
if (!formData.get("somebodys-name")) {
submissionValidationFailed.invalid_fields.push({
// ...
});
}
// 或者以一种更彻底的方式检查电子邮件地址的有效性
if (!/^[^\s@]+@[^\s@]+.[^\s@]+$/.test(formData.get("any-email"))) {
submissionValidationFailed.invalid_fields.push({
// ...
});
}
// 返回成功或验证失败的响应
const body = !submissionValidationFailed.invalid_fields.length
? submissionSuccess
: submissionValidationFailed;
return { body };
}
至于数据转换,serverless
函数通常期望接收 JSON
格式的输入,因此对于其他数据类型,需要额外的解析步骤。值得一提的是 CodePen
演示中的表单提交时使用的是 multipart/form-data
。
无需任何库,我们可以利用Response API
的功能将multipart/form-data
字符串转换为FormData
:
javascript
async function convertMultipartFormDataToFormData(data) {
const matches = data.match(/^\s*--(\S+)/);
if (!matches) {
return new FormData();
}
const boundary = matches[1];
return new Response(data, {
headers: {
"Content-Type": `multipart/form-data; boundary=${boundary}`
}
}).formData();
}
这段代码的主要侧重于提取boundary
变量。这通常是自动生成的,例如在浏览器中提交表单时。
提交的原始数据可以通过event.http.body
获得,但由于它是base64编码的,我们需要先对其进行解码:
csharp
async function main(event) {
const formData = await convertMultipartFormDataToFormData(
Buffer.from(event?.http?.body ?? "", "base64").toString("utf8")
);
// ...
const body = !submissionValidationFailed.invalid_fields.length
? submissionSuccess
: submissionValidationFailed;
return { body };
}
就这样,采用这种方法后,剩下的只需将对原始API
的调用替换为对Mock API
的调用即可。
总结
最终,这两种方法都有助于将演示示例与第三方API
依赖解耦。从工作量来看,至少在这个具体案例中,两者几乎相差无几。
手动模拟方法的优势在于几乎没有任何外部依赖,甚至没有我们能控制的依赖,所有功能都集成在项目内部。总体而言,在不了解具体细节的情况下,对于小型、独立的演示示例,我们完全有理由优先选择这种方法。
但使用 Mock API
服务也有其优势。Mock
服务不仅能支持演示,还能支持各类测试需求。对于更复杂的场景,专门负责Mock
服务的团队可能更喜欢使用 JavaScript
以外的编程语言,或是直接采用WireMock等工具,而非从头搭建。
就像所有事情一样,这取决于具体需求。除了前文提到的因素,还有许多其他标准需要权衡。
此外,我也不认为这种方法必须默认采用。毕竟,我 CodePen
上的演示已经运行了四年,没有任何问题。
关键在于,我们需要建立一套机制来监测演示示例何时失效(监控),并在失效发生时,我们有合适的工具来快速处理这种情况。