动态

详情 返回 返回

如何實現在純 Web 端完成各類 API 調試? - 动态 详情

在軟件開發過程中,對於各類 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. 在監聽器中可以處理接收到的消息,並根據需要作出響應。
// arex
const 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.js
window.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.js
chrome.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 函數,以便在其他地方使用。

// 文件轉Base64
const 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

Add a new 评论

Some HTML is okay.