前端演示老挂掉,咋整?两种骚操作让代码演示稳如老狗

4,029字
17–26 分钟
in

写代码演示最怕啥?怕它过段时间自己就挂了,尤其是那种依赖外部接口的玩意儿。之前搞了一堆WordPress表单提交的CodePen演示,跑了四年,结果托管环境一迁移,后台数据全没了,连账号都登不上去。这玩意儿就跟网上那些教程里的Demo一样,背后依赖的服务一抽风,整个演示就变成一堆红字报错,巨尴尬。那会儿就在想,如果依赖的不是自己能折腾的WordPress,而是个第三方商业服务,那岂不是直接抓瞎?所以得琢磨个法子,让这些演示别那么脆皮,就算背后的真家伙歇菜了,演示本身还能照常转。

目录

为啥演示会变“一次性用品”

但凡代码演示里拽了外部API,就等于把命根子交到了别人手里。那些表单提交的例子,背后得有个正经的服务器在跑着,接收数据、返回成功或失败的消息。一旦这个服务器没了,或者接口地址变了,前端那堆代码就成了没头的苍蝇。这跟软件测试里头的依赖问题是一模一样的道理,外部依赖不受控,就容易出幺蛾子。最靠谱的解决思路就是把这些外部服务从演示里头“摘”出去,让演示自己跟自己玩,不依赖外部网络环境。

两招搞定外部API依赖

要让演示摆脱对外部API的依赖,通常就两条路:要么在代码层面动手脚,把网络请求给“调包”;要么单独搞个假的API服务器,让演示去跟这个假的通信。两条路各有各的玩法,咱们就以前面提到的那堆WordPress表单演示为例,具体看看咋操作。

劫持请求,狸猫换太子

这招的核心是在浏览器里头搞个拦截器,在fetch请求发出去之前或者发出去之后,把它给截住。如果是打给WordPress表单API的请求,就直接伪造一个回应返回去,根本不往外面发。这招在测试框架里头很常见,叫“猴子补丁”,就是直接替换掉浏览器原来的fetch函数。

// 定义一个拦截函数,把原来的fetch包一层
const 请求拦截器 = (原始fetch) => async (请求地址, 请求配置 = {}) => {
  // 只处理表单提交的请求,其他的放行
  if (typeof 请求地址 !== "string" || !(请求配置.body instanceof FormData)) {
    return 原始fetch(请求地址, 请求配置);
  }

  // 匹配WordPress表单插件的接口地址
  if (请求地址.match(/wp-json/contact-form-7/)) {
    return 伪造ContactForm7回应(请求配置.body);
  }

  if (请求地址.match(/wp-json/gf/)) {
    return 伪造GravityForms回应(请求配置.body);
  }

  return 原始fetch(请求地址, 请求配置);
};

// 直接把全局fetch替换掉
window.fetch = 请求拦截器(window.fetch);

这么一搞,页面里任何发往那些特定地址的fetch请求,都会被拐到咱们自己的函数里头。在伪造回应的函数里,可以根据表单填的内容,返回成功或者失败的信息。比如,如果名字没填,就返回一个验证失败的假回应。

const 伪造ContactForm7回应 = (表单数据) => {
  // 预设一个成功回应的结构
  const 成功回应 = {
    into: "#",
    status: "mail_sent",
    message: "感谢留言,已成功发送!",
    posted_data_hash: "d52f9f9de995287195409fe6dcde0c50"
  };
  // 预设一个验证失败回应的结构
  const 失败回应 = {
    into: "#",
    status: "validation_failed",
    message: "有些字段填错了,麻烦检查一下再试试。",
    posted_data_hash: "",
    invalid_fields: []
  };

  // 检查“姓名”字段,为空就塞个错误进去
  if (!表单数据.get("somebodys-name")) {
    失败回应.invalid_fields.push({
      into: "span.wpcf7-form-control-wrap.somebodys-name",
      message: "姓名不能为空",
      idref: null,
      error_id: "-ve-somebodys-name"
    });
  }

  // 用正则检查邮箱格式对不对
  if (!/^[^s@]+@[^s@]+.[^s@]+$/.test(表单数据.get("any-email"))) {
    失败回应.invalid_fields.push({
      into: "span.wpcf7-form-control-wrap.any-email",
      message: "邮箱格式不对,再瞅瞅",
      idref: null,
      error_id: "-ve-any-email"
    });
  }

  // 没有错误就返回成功回应,有错误就返回失败回应
  const 最终回应数据 = !失败回应.invalid_fields.length ? 成功回应 : 失败回应;
  return Response.json(最终回应数据);
};

这么一弄,演示里的表单提交根本不用连外网,所有交互全靠本地伪造的数据撑着。这招好使,就是得注意别把猴子补丁搞得太复杂,不然代码逻辑容易乱,还可能跟别的脚本起冲突。

搭个临时戏台,用模拟服务器

要是觉得改fetch代码太“侵入式”,或者想模拟的场景更复杂,比如要模拟不同的HTTP状态码、复杂的响应头,那就可以考虑弄个假的后台服务器。这年头搞这个不用真租台机器,用云函数(比如DigitalOcean Functions)搭个假的API端点,基本不要钱,还省心。云函数就是一小段代码,跑在云端,给它一个HTTP地址,访问了它就执行。

// 云函数的主函数,名字必须是main
function main(事件对象) {
  // 从事件对象里把提交的数据抠出来,弄成FormData
  const 表单数据 = 从事件中提取FormData(事件对象);

  const 成功回应 = {
    into: "#",
    status: "mail_sent",
    message: "感谢留言,已成功发送!",
    posted_data_hash: "d52f9f9de995287195409fe6dcde0c50"
  };
  const 失败回应 = {
    into: "#",
    status: "validation_failed",
    message: "有些字段填错了,麻烦检查一下再试试。",
    posted_data_hash: "",
    invalid_fields: []
  };

  // 校验逻辑跟前面一样
  if (!表单数据.get("somebodys-name")) {
    失败回应.invalid_fields.push({
      into: "span.wpcf7-form-control-wrap.somebodys-name",
      message: "姓名不能为空",
      idref: null,
      error_id: "-ve-somebodys-name"
    });
  }

  if (!/^[^s@]+@[^s@]+.[^s@]+$/.test(表单数据.get("any-email"))) {
    失败回应.invalid_fields.push({
      into: "span.wpcf7-form-control-wrap.any-email",
      message: "邮箱格式不对,再瞅瞅",
      idref: null,
      error_id: "-ve-any-email"
    });
  }

  const 最终回应数据 = !失败回应.invalid_fields.length ? 成功回应 : 失败回应;
  // 云函数里返回这个对象,就相当于HTTP响应里的JSON
  return { body: 最终回应数据 };
}

云函数收到的请求数据可能不是现成的FormData,特别是表单提交的multipart/form-data格式,传进来可能是经过base64编码的字符串。得先把它解码,然后利用浏览器里头的Response对象转成FormData。

// 辅助函数:把multipart/form-data字符串转成FormData
async function 将字符串转成FormData(数据字符串) {
  // 用正则匹配出boundary分隔符
  const 匹配结果 = 数据字符串.match(/^s*--(S+)/);
  if (!匹配结果) {
    return new FormData();
  }
  const 分隔符 = 匹配结果[1];

  // 利用Response API,把字符串当成multipart/form-data请求体来解析
  return new Response(数据字符串, {
    headers: {
      "Content-Type": `multipart/form-data; boundary=${分隔符}`
    }
  }).formData();
}

async function 从事件中提取FormData(事件对象) {
  // 事件对象里http.body是base64编码的原始数据
  const 原始数据 = 事件对象?.http?.body ?? "";
  const 解码后的文本 = Buffer.from(原始数据, "base64").toString("utf8");
  return 将字符串转成FormData(解码后的文本);
}

把这段代码丢到云函数平台,部署好拿到一个URL,然后把前端演示里请求的地址换成这个URL。这样一来,演示依赖的就是一个完全由自己控制的假服务器,只要不主动删掉这个云函数,它就能一直稳定地返回假数据。

扯两句闲篇

这两种法子,一个是在前端代码里直接拦截,一个是额外起一个假服务。对那种独立的小Demo来说,直接在代码里拦截更省事儿,所有东西都打包在一起,扔到哪儿都能跑。但如果演示逻辑复杂,或者想在不同的地方复用这套假数据逻辑,搞个云函数当假服务器就更灵活。说白了,没有绝对的好坏,就看当时的需求和手头的资源。其实大部分演示也没必要一开始就搞这么复杂,就像之前那些CodePen,愣是稳稳当当跑了四年才出问题。关键是得有个底,万一哪天依赖的服务真挂了,知道手里还有这两把刷子能顶上,心里不慌。