写代码演示最怕啥?怕它过段时间自己就挂了,尤其是那种依赖外部接口的玩意儿。之前搞了一堆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,愣是稳稳当当跑了四年才出问题。关键是得有个底,万一哪天依赖的服务真挂了,知道手里还有这两把刷子能顶上,心里不慌。
