当第三方 API 失效时,如何保留文章演示示例

本篇依然来自于我们的 《前端周刊》 项目!

由团队成员 田八 翻译,欢迎大家 进群 持续追踪全球最新前端资讯!!

原文地址:css-tricks

四年后,我的"使用 WordPress REST API 进行无头表单提交"文章中的示例最终停止运行了。

这篇文章中包含了 CodePen 的嵌入,演示了如何使用流行的 WordPress 表单插件的 REST API 端点来捕获和显示验证错误和提交反馈,从而构建一个完全自定义的前端。这些代码演示依赖于我在后台运行的一个 WordPress 网站。但是在一次被迫的基础设施迁移过程中,该网站没能正确的被转移,更糟糕的是,我失去了对我的账户的访问权限。

当然,原本我是可以联系技术支持或者在其他地方恢复备份。但这种情况让我产生怀疑:如果这不是 WordPress 呢?如果它是一个我无法自托管或修复的第三方服务呢?有没有办法构建不会因为依赖的服务失败而中断的演示?我们如何确保教学示例尽可能长时间可用?

还是说这些是不可避免的?示例是否像网络上的其他一切一样,注定会停止运转?

与软件测试的相似之处

对于编写代码测试的人来说,长期以来他们一直都在与类似的问题作斗争,尽管表述方式有所不同。但核心问题是相同的。依赖关系,尤其是第三方依赖关系成为障碍,因为它们超出了控制范围。

毫不奇怪,消除外部依赖问题最靠谱的方法是将外部服务完全移除,从而有效地与其解耦。当然,怎么做到这一点,以及是否每次都是可行的,需要视具体情况而定。

事实上,在使示例能够快速还原这方面,处理依赖关系的技术同样有用。

为了更具体说明,我会使用前面提到的 CodePen 演示作为示例。同样的方法在很多其他的情况下也同样有效。

解耦 REST API 依赖关系

尽管有很多策略和技巧,但打破对 REST API 的依赖的两种最常见方法是:

  1. 在代码中模拟 HTTP 调用,不是执行真正的网络请求,而是返回备份的响应体
  2. 使用模拟 API 服务器作为真实服务的替代品,并以类似的方式提供预定义的响应

两者都有利弊,我们稍后再讨论。

使用拦截器模拟响应

在现代测试框架中,无论是用于单元测试还是端到端测试(例如JestPlaywright),都提供内置的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);
};

根据需要,响应可以是纯文本BlobArrayBuffer的任何内容。还可以指定自定义状态代码,并包含其他响应标头。

对于 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 服务方法进行对比。

使用serverlessMock API 服务

运行传统托管模式的 Mock API 服务器,会重新带来可用性、维护和成本方面的问题。尽管围绕serverless功能的炒作已逐渐平息,但我们可以使用它们来规避这些问题。

而且由于DigitalOcean Functions慷慨的提供了免费套餐,创建 Mock API 实际上是免费的,只需要手动模拟即可。

对于简单的用例,所有操作都可以通过 Functions 控制面板完成,包括在内置编辑器中编写代码。观看这段简洁的演示视频,了解实际操作:

YouTuBe

视频地址在当前平台不支持预览,请点击链接查看。

对于更复杂的需求,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 上的演示已经运行了四年,没有任何问题。

关键在于,我们需要建立一套机制来监测演示示例何时失效(监控),并在失效发生时,我们有合适的工具来快速处理这种情况。

相关推荐
掘金者阿豪19 分钟前
把业务数据变成共享仪表盘:Metabase可视化与远程访问实践
前端·后端
kyriewen39 分钟前
折腾了半年 AI 编程工作流,最后发现效率瓶颈是桌上那块屏幕
前端·javascript·ai编程
蜗牛前端1 小时前
codex 全流程开发上线的高颜值礼簿小程序
前端·微信小程序
大龄秃头程序员2 小时前
我在图文流 App 里落地双层缓存、弱网降级与 OOM 治理
前端
老王以为2 小时前
React Renderer 分离的多平台架构
前端·react native·react.js
hunterandroid2 小时前
Kotlin Coroutines 与 Flow:让异步任务更清晰
前端
Bigger2 小时前
从零搭建 AI 代码审查服务:一份前端也能看懂的 Python 学习笔记
前端·ci/cd·ai编程
lichenyang4533 小时前
JSAPI、NAPI、Biz、Imp:ASCF Demo 如何真正调用系统能力和 C++ 能力
前端
自由路飞3 小时前
RAG 混合检索深挖:BM25 和向量分数为什么不能直接相加?
面试
lichenyang4533 小时前
IPC、JSVM、UIThread、libuv:ASCF 架构图里最容易混的几个词
前端