在软件开发过程中,对于各类 API 的调试工作至关重要。API调试是验证和测试利用程序接口的有效性和正确性的关键步骤。传统的API调试办法通常依赖于独立的工具或桌面应用程序,限度了调试过程的灵活性和效率。

为推动API调试向更便捷、高效的方向倒退,越来越多的开发人员开始寻求在纯Web端实现各类API调试的解决方案。纯Web端的API调试具备许多劣势,包含无需装置额定软件、跨平台反对、便于团队合作等。本文将以开源我的项目 AREX 为例为大家介绍如何在 Web 端实现对各类 API 的调试性能。

对于 AREX

AREX (http://arextest.com/)是一款开源的基于实在申请与数据的自动化回归测试平台,利用 Java Agent 技术与比对技术,通过流量录制回放能力实现疾速无效的回归测试。同时提供了接口测试、接口比对测试等丰盛的自动化测试性能。

难点一:跨域限度

要想在纯 Web 端实现各类 API 的调试工作,首先要解决的难题是解决浏览器的跨域限度。

什么是跨域

浏览器跨域问题是指在 Web 开发中,当应用 JavaScript 代码从一个域名的网页拜访另一个域名的资源时会遇到的限度。浏览器施行了一种安全策略,称为同源策略(Same-Origin Policy), 用于爱护用户信息的平安。同源策略要求网页中的 JavaScript 只能拜访与其起源(协定、域名和端口号)雷同的资源,而对于不同域名的资源拜访会受到限制。

因为浏览器存在跨域限度,咱们不能在浏览器端得心应手地发送 HTTP 申请,这是浏览器的安全策略决定的。

解决方案

经调研,冲破此限度的办法有两种:别离是 **Chrome** 插件代理服务端代理,以下是两种办法的比拟。

         Chrome 插件代理                  服务端代理            
拜访本地能够                                    不能够                    
速度    无申请工夫损耗                          整个流程速度受代理接口影响
理论申请已知 Origin 源会被批改为 Chrome 插件的源齐全一样                  

衡量下来 AREX 抉择了 Chrome 插件代理的办法,其原理是利用了 Chrome 插件中 background 能够发送跨域申请的能力,咱们将浏览器端拦挡到的申请通过 window.postmassage 与 Chrome 插件的 background 进行通信(其中通信还须要 Chrome 插件的 content-script作为数据桥梁)。

具体实现如下:

  • 在页面脚本中
  1. 生成一个随机的字符串,并将其转换为字符串模式,存储在 \`tid\` 变量中。
  2. 应用 window.postMessage() 办法发送一条音讯到其余扩大程序,音讯包含一个类型为 AREX_EXTENSION_REQUEST 的标识、tid、以及 params 参数。
  3. 增加一个 message 事件监听器 receiveMessage,用于接管其余扩大程序发送的音讯。

4. 在 receiveMessage 函数中,查看接管到的音讯是否为类型为 AREX_EXTENSION_RES,并且 tid 与之前发送的音讯的 tid 相匹配。如果匹配胜利,则移除事件监听器。

  • 在内容脚本中
  1. 增加一个 message 事件监听器,用于接管来自页面脚本或其余扩大程序发送的音讯。
  2. 在事件监听器中,查看接管到的音讯是否为类型为 AREX_EXTENSION_REQUEST,如果是,则应用 chrome.runtime.sendMessage() 办法将音讯发送给后盾脚本。
  3. 在接管到来自后盾脚本的响应后,应用 window.postMessage() 办法将响应音讯发送回页面脚本或其余扩大程序。
  • 在后盾脚本中
  1. 应用 chrome.runtime.onMessage.addListener() 办法增加一个监听器,用于接管来自内容脚本或其余扩大程序发送的音讯。
  2. 在监听器中能够解决接管到的音讯,并依据须要作出响应。
// arexconst tid = String(Math.random());window.postMessage(  {    type: '__AREX_EXTENSION_REQUEST__',    tid: tid,    payload: params,  },  '*',);window.addEventListener('message', receiveMessage);function receiveMessage(ev: any) {  if (ev.data.type === '__AREX_EXTENSION_RES__' && ev.data.tid == tid) {    window.removeEventListener('message', receiveMessage, false);  }}// content-script.jswindow.addEventListener("message", (ev) => {  if (ev.data.type === "__AREX_EXTENSION_REQUEST__"){    chrome.runtime.sendMessage(ev.data, res => {      //   与background通信      window.postMessage(        {          type: "__AREX_EXTENSION_RES__",          res,          tid:ev.data.tid        },        "*"      )    })  }})// background.jschrome.runtime.onMessage.addListener((req, sender, sendResponse) => {})

难点二:API 调试

上述曾经解决了跨域问题,接下来就是如何实现 API 调试的性能。

解决方案

Postman 是业内成熟的 API 调试工具,咱们站在了 Postman 这位伟人的肩膀上,在 AREX 中引入了 Postman 的 JavaScript 沙盒,应用它的沙盒运行前置脚本、后置脚本以及断言来调试 API。

以下是 AREX 申请的流程图:

当点击发送申请的时候,会将表单中的数据汇聚到一起,数据结构为:

export interface Request {  id: string;  name: string;  method: string;  endpoint: string;  params: {key:string,value:string}[];  headers: {key:string,value:string}[];  preRequestScript: string;  testScript: string;  body: {contentType:string,body:string};}

这是 AREX 的数据结构,咱们会将其转换成 Postman 的数据结构。之后调用 PostmanRuntime.Runner() 办法,将转换好了的 Postman 数据结构和以后所选的环境变量传入,Runner 会执行 preRequestScripttestScript 脚本。preRequestScript 产生在申请之前,能够在其中交叉申请以及对申请参数、环境变量进行操作,testScript 产生在申请之后,能够对 response 返回数据进行断言操作,并且脚本中也能够通过 console.log 输入数据,在控制台进行调试。

var runner = new runtime.Runner(); // runtime = require('postman-runtime');// 一个规范的postman汇合对象var collection = new sdk.Collection();runner.run(collection, {}, function (err, run) {    run.start({      assertion:function (){}, //断言      prerequest:function (){}, // 预申请勾子      test:function (){}, //测试勾子      response:function (){} //返回勾子    });});

在 Postman 沙盒中也存在跨域问题,因为 Postman 沙盒的集成度十分高,为了确保与 PostmanRuntime 的同步以及方便性,咱们采纳了 Ajax 拦挡技术。通过在浏览器端拦挡 Ajax 申请,咱们能够对申请进行批改、增加自定义逻辑或者进行其余解决操作。这样能够实现对申请和响应的全局管制和定制化。

当 Postman 沙盒发送申请时,会携带一个名为 "postman-token" 的申请头。咱们拦挡到这个 Ajax 申请后,会将申请参数进行拼装,并通过 window.postMessage 发送给浏览器插件。浏览器插件再次构建 fetch 申请,将数据返回给 Postman 沙盒,使其输入最终后果,包含响应(response)、测试后果(testResult)和控制台日志(console.log)。须要留神的是,responseType 必须指定为 arraybuffer。

具体流程如下:

  1. 应用 xspy.onRequest() 办法注册一个申请处理程序。这个处理程序承受两个参数:request 和 sendResponse。request 参数蕴含申请的相干信息,例如办法、URL、头部、申请体等。sendResponse 是一个回调函数,用于发送响应给申请方。

2. 在处理程序中,通过查看申请的头部中是否存在 postman-token 来判断申请是否来自 Postman。

  • 如果存在该头部,示意申请是通过 Postman 发送的。则应用 AgentAxios 发动一个新的申请,应用原始申请的办法、URL、头部和申请体。AgentAxios 返回一个 agentData 对象,其中蕴含了响应的状态码、头部和数据等信息。创立一个名为 dummyResponse 的响应对象,蕴含了与原始申请相干的信息。dummyResponse 的 status 字段为 agentData 的状态码,headers 字段为将 agentData 的头部数组转换为对象格局的后果,ajaxType 字段为字符串 xhr,responseType 字段为字符串 arraybuffer,response 字段为将 agentData 的数据转换为 JSON 字符串并用 Buffer 包装的后果。最初,应用 sendResponse(dummyResponse) 将响应发送给申请方。
  • 如果申请不是来自 Postman,则间接调用 sendResponse(),示意不返回任何响应。
xspy.onRequest(async (request: any, sendResponse: any) => {  // 判断是否是pm发的  if (request.headers['postman-token']) {    const agentData: any = await AgentAxios({      method: request.method,      url: request.url,      headers: request.headers,      data: request.body,    });    const dummyResponse = {      status: agentData.status,      headers: agentData.headers.reduce((p: any, c: { key: any; value: any }) => {        return {          ...p,          [c.key]: c.value,        };      }, {}),      ajaxType: 'xhr',      responseType: 'arraybuffer',      response: new Buffer(JSON.stringify(agentData.data)),    };    sendResponse(dummyResponse);  } else {    sendResponse();  }});

难点三:二进制对象序列化传递

还有一点值得一提,对于 x-www-form-urlencodedRaw 类型的申请,因为它们都是一般的 JSON 对象,解决起来比拟容易。然而对于 form-databinary 类型的申请,须要反对传输二进制文件负载。然而,Chrome 插件的 postMessage 通信形式不反对间接传递二进制对象,导致无奈间接解决这两种类型的申请。

解决方案

为了解决这个问题,AREX 采纳了 base64 编码技术。在用户抉择文件时,AREX 会将二进制文件转换为 base64 字符串,而后进行传输。在 Chrome 插件端,AREX 会将 base64 数据进行解码,并用于构建理论的 fetch 申请。这样能够绕过间接传递二进制对象的限度。

这个流程图形容了将 FormData 中的二进制文件转换为 Base64 字符串,并通过 Chrome 插件代理将其转换回文件并进行进一步解决的过程。

  1. form-data binary(A):示意一个蕴含二进制文件的 FormData 表单数据。
  2. FileReader(B):应用 FileReader 对象来读取二进制文件。
  3. readAsDataURL base64 string:FileReader 应用 readAsDataURL 办法将二进制文件读取为 Base64 字符串。
  4. Chrome 插件代理(C):Base64 字符串通过读取操作后,传递给 Chrome 插件代理进行进一步解决。
  5. base64 string:示意通过 FileReader 读取二进制文件后失去的 Base64 字符串。
  6. Uint8Array(D):在 Chrome 插件代理中,将 Base64 字符串转换为 Uint8Array。
  7. File(E):应用 Uint8Array 的数据创立一个新的 File 对象。
  8. fetch(F):将新创建的 File 对象通过 fetch 办法或其余形式进行进一步解决,例如上传到服务器或进行其余操作。

代码剖析

以下是代码层面的剖析:

toBase64 函数承受一个 File 对象作为参数,并返回一个 Promise 对象,该 Promise 对象将解析为示意文件的 Base64 字符串。

在函数外部,创立了一个 FileReader 对象。 通过调用 reader.readAsDataURL(file) 将文件读取为 Data URL。 当读取操作实现时,通过 reader.onload 事件处理程序将读取后果解析为字符串,并应用 resolve 将其传递给 Promise。 如果产生谬误,将应用 reject 将谬误传递给 Promise。 base64ToFile 函数承受两个参数:dataurl(Base64 字符串)和 filename(文件名),并返回一个 File 对象。

首先,将 dataurl 应用逗号宰割成数组 arr,如果宰割后果为空,则将其设为蕴含一个空字符串的数组。 通过正则表达式匹配 arr[0] 中的内容,提取出 MIME 类型,即数据的类型。 应用 atob 将 Base64 字符串解码为二进制字符串 bstr。 创立一个长度为 n 的 Uint8Array 数组 u8arr。 应用循环遍历 bstr,将每个字符的 Unicode 编码放入 u8arr 中。 最初,应用 File 构造函数创立并返回一个新的 File 对象,其中蕴含了从 u8arr 中读取的文件数据、文件名和 MIME 类型。 导出 base64ToFile 函数,以便在其余中央应用。

// 文件转Base64const toBase64 = (file: File): Promise<string> =>  new Promise((resolve, reject) => {    const reader = new FileReader();    reader.readAsDataURL(file);    reader.onload = () => resolve(reader.result as string);    reader.onerror = reject;  });// base64转文件function base64ToFile(dataurl: string, filename: string) {  const arr = dataurl.split(',') || [''],    mime = arr[0].match(/:(.*?);/)?.[1],    bstr = atob(arr[1]);  let n = bstr.length;  const u8arr = new Uint8Array(n);  while (n--) {    u8arr[n] = bstr.charCodeAt(n);  }  return new File([u8arr], filename, { type: mime });}export default base64ToFile;
  • AREX 文档:arextest.com/zh-Hans/doc…
  • AREX 官网:arextest.com/
  • AREX GitHub:github.com/arextest
  • AREX 官网 QQ 交换群:656108079