一比一還原axios原始碼(二)—— 請求響應處理

Zaking發表於2022-03-16

  上一章,我們開發了一些簡單的程式碼,這部分程式碼最最核心的一個方法就是buildURL,應對了把物件處理成query引數的方方面面。雖然我們現在可以發起簡單的請求了,但是第一,我們無法接收到伺服器的響應,哦不對,其實在瀏覽器層面,response已經是接收到了的,只是程式碼裡還拿不到response,因為我們還沒寫。第二,post的請求還沒實現。而處理拿到的response實際上就是處理響應體和響應頭。實現post請求,實際上就是實現請求體和請求頭。今天我們就來實現這四個點的內容。

  思考題:get請求可以傳送body麼?大家可以思考下,答案在結尾。不要提前看哦~

一、請求頭和請求體的處理

  處理請求的body,實際上就是XMLHttpRequest的send方法,它可以接收一個body作為引數,這個引數可以是Document、XMLHttpRequestBodyInit或者null。而XMLHttpRequestBodyInit可以是 BlobBufferSource (en-US)FormDataURLSearchParams, 或者 USVString 物件。當然,我們最常用的就是傳一個物件的場景,所以我們需要額外的處理一下,給傳遞的body的物件資料轉換成JSON字串。

  

  上圖,是MDN中send方法引數的詳細描述。

   那麼在axios中的使用方法是這樣的:

// Send a POST request
axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
});

  上面的程式碼就是axios官方文件中的一部分,下面我們來實現它,首先,我們需要一個transformRequest方法,來把post請求中的data引數的物件,轉換成JSON字串,因為這裡的data是一個物件,send方法是不接受物件的,所以我們要轉換成字串,也就是文件中的USVString 型別。

function transformRequest(data) {
  if (utils.isPlainObject(data)) {
    return JSON.stringify(data);
  }
  return data;
}

  我們直接在xhr.js中實現上面的程式碼即可,其中isPlainObject是工具類中的一個方法,用來判斷是否是一個純物件,你也知道在js中,typeof 的結果是“object”並不代表是純物件,也可能是表單物件,日期物件等等,這裡我們需要通過isPlainObjecr判斷出它是一個純粹的如我們所知“物件”。

function isPlainObject(val) {
  if (toString.call(val) !== "[object Object]") {
    return false;
  }

  var prototype = Object.getPrototypeOf(val);
  return prototype === null || prototype === Object.prototype;
}

  isPlainObject的實現如上,個人理解,這裡判斷條件是“可以拿到原型鏈上的"[object Object]",又通過原型鏈與Object的原型同源”才會被命中條件。而isObject方法,就是簡單的判斷typeof,這些工具方法大家都可以去gitHub上對應的章節分支及其下的檔案中找到。以後這些就不再強調和重複了。

  按照上述步驟完成後,我們發現還是傳過去的並不是我們想像的那樣,這是因為我們還沒處理header,預設的request header是text/plain,所以服務端無法處理我們傳過去的資料,這時候我們就需要來處理下header。  

function processHeaders(headers, data) {
  normalizeHeaderName(headers, "Content-Type");
  if (utils.isPlainObject(data)) {
    if (headers && !headers["Content-Type"]) {
      headers["Content-Type"] = "application/json;charset=utf-8";
    }
  }
  return headers;
}

  上面就是處理header的部分,其實也很簡單,normalizeHeaderName方法需要我們在helpers資料夾下在建立一個normalizeHeaderName檔案,它的作用就是統一header的名稱,你傳入小寫的,也會轉換一下。具體的註釋大家可以去自己看這裡不多說。上面的程式碼其實就是如果是一個物件,那麼就加上application的型別,很簡單。我們直接寫在xhr對xhrAdapter中傳入的config.header做一下處理即可。

    config.headers = processHeaders(config.headers || {}, config.data);

  最後,如果我們沒傳data的話,但是又設定了content-type請求頭,那麼手動去除一下:

Object.keys(config.headers).forEach((name) => {
      if (config.data === null && name.toLowerCase() === "content-type") {
        delete config.headers[name];
      } else {
        request.setRequestHeader(name, config.headers[name]);
      }
    });

  至此,請求體和請求頭就處理完了,我們可以正確的發起攜帶body的請求了。那麼到此我們來簡單回顧一下,其實總結起來就一句話:針對普通物件的body傳遞,轉換成json並手動設定正確的請求頭

   總結一下,預設的request header 的content-type型別是text/plain,所以,雖然我們轉換了body的物件為JSON字串,但是伺服器端是不知道的,所以需要設定request header的content-type為application/json即可讓伺服器識別。那麼我們就可以正常的拿到響應體的內容了。

  那你可能會問了,開頭的時候不是說了還有其他型別麼?什麼表單、arrayBuffer啥的?不用設定頭欄位麼?額。。稍安勿躁,後面見分曉。

二、響應頭和響應體的處理

  上面第一小節,我們已經可以發起帶body的請求,並且伺服器也能根據request header正確的解析了,下面我們要做的就是來處理返回的資料。我們還是來看最開始的axios官網的例子:

 

  我們看到,結果是返回了一個promise。ok,這就是我們想要的東西~很簡單,在xhrAdapter中包一層promise即可:

 

   很簡單,對吧~中間的紅框request.onreadystatechange繫結的方法,就是我們要核心實現的處理響應體的方法:

request.onreadystatechange = function handleLoad() {
      if (request.readyState !== 4) {
        return;
      }

      const responseHeaders = parseHeaders(request.getAllResponseHeaders());
      const responseData =
        config.responseType && config.responseType !== "text"
          ? request.response
          : request.responseText;
      const response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config,
        request,
      };
      resolve(response);
    };

  首先onreadystatechange是XMLHttpRequest例項的一個方法,可以監聽響應事件,readyState也是XMLHttpRequest例項上的一個屬性,它會告訴你響應的狀態,這些大家可以去MDN檢視,首先我們面臨了第一個問題,就是我們通過XMLHttpRequest例項上的getAllResponseHeaders方法獲取到的響應頭其實是一個以\r\n(回車符和換行符)結尾拼接的字串,我們需要把它們轉換成物件,轉換成物件的方法就需要parseHeaders輔助函式來處理了,下面我們在helpers資料夾中建立一個parseHeaders檔案:

"use strict";

import utils from "../utils";

// Headers whose duplicates are ignored by node
// c.f. https://nodejs.org/api/http.html#http_message_headers
// 這是我們需要忽略的header
var ignoreDuplicateOf = [
  "age",
  "authorization",
  "content-length",
  "content-type",
  "etag",
  "expires",
  "from",
  "host",
  "if-modified-since",
  "if-unmodified-since",
  "last-modified",
  "location",
  "max-forwards",
  "proxy-authorization",
  "referer",
  "retry-after",
  "user-agent",
];

/**
 * Parse headers into an object
 *
 * ```
 * Date: Wed, 27 Aug 2014 08:58:49 GMT
 * Content-Type: application/json
 * Connection: keep-alive
 * Transfer-Encoding: chunked
 * ```
 *
 * @param {String} headers Headers needing to be parsed
 * @returns {Object} Headers parsed into an object
 */
export default function parseHeaders(headers) {
  var parsed = {};
  var key;
  var val;
  var i;
  // 沒有headers就返回個空物件
  if (!headers) {
    return parsed;
  }
  // 用自定義的forEach方法來遍歷分割後的headers
  utils.forEach(headers.split("\n"), function parser(line) {
    // 這時候,line還是個字串,i就是每一個key、value中間的位置
    i = line.indexOf(":");
    // 分割key,去空格
    key = utils.trim(line.substr(0, i)).toLowerCase();
    // 分割value,去空格
    val = utils.trim(line.substr(i + 1));

    if (key) {
      // 如果存在key並且是重複項,則略過
      if (parsed[key] && ignoreDuplicateOf.indexOf(key) >= 0) {
        return;
      }
      // 如果key是set-cookie,那麼用陣列合並作為值
      if (key === "set-cookie") {
        parsed[key] = (parsed[key] ? parsed[key] : []).concat([val]);
      } else {
        // 否則用逗號分隔已有的值或者直接設定值
        parsed[key] = parsed[key] ? parsed[key] + ", " + val : val;
      }
    }
  });

  return parsed;
}

  其實這個程式碼也並不複雜,大家看註釋說明就好。寫好了轉換程式碼,直接在onreadystatechange中轉換一下即可。然後,還有一個需要額外處理的東西,就是responseType引數:

      const responseData =
        config.responseType && config.responseType !== "text"
          ? request.response
          : request.responseText;

  responseType的不同,會影響到返回的響應體的型別。最後,我們把這些轉換後的資料,resolve出去即可:

const response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config,
        request,
      };
      resolve(response);

  這部分resolve的內容,實際上就是axios定義的需要返回的內容:

   一模一樣,對嘛~

  OK,到此為止我們完成了完整的請求響應過程。並且處理其中的部分引數以及加入了promise。目前,我們所做的事情,完成了整個axios請求最核心的主線,那麼我們來總結下到現在為止,我們都做了axios中的哪些事情:

  實現的axios API如下:

axios({
   method:"post",
   url:"xxx",
   params:{},
   data:{},
   responseType:"",
   headers:{},     
}).then((res) => {
   console.log(res) 
})

  還有res中的response的資料。前面說過了,再有就是promise。那麼在實際的程式碼中呢,我們實現了發起ajax請求的一條主線,也就是從請求發起,到響應返回的過程,並且在過程中,由於json的特殊性,對此還進行了相應頭欄位和body的轉換,再有一個實用的buildURL方法,來處理物件到query字串的轉換。哦對,再有就是我們獲取到的response header是字串,我們還要分割下,把字串轉換成一個物件的parseHeader方法。

  以上,buildURL都是可以拿到實際的專案中去使用的,我就複製到了我們專案裡,爽得一批(好吧,原諒我頭髮不長,見識也不長)。

  好了。。。。大家注意沒,上面的程式碼沒有.catch,是的,錯誤處理還沒寫,下一章我們就來寫錯誤處理的相關程式碼。

 

答案:

  從技術層面上講,get是可以傳body的,但是在客戶端,瀏覽器的層面,不允許get傳body,所有的get中的body都視為null。但是在伺服器端的http請求中,get是可以傳遞body的。

  另外一個思考題:get和post請求有啥區別?   

相關文章