上一章,我們開發了一些簡單的程式碼,這部分程式碼最最核心的一個方法就是buildURL,應對了把物件處理成query引數的方方面面。雖然我們現在可以發起簡單的請求了,但是第一,我們無法接收到伺服器的響應,哦不對,其實在瀏覽器層面,response已經是接收到了的,只是程式碼裡還拿不到response,因為我們還沒寫。第二,post的請求還沒實現。而處理拿到的response實際上就是處理響應體和響應頭。實現post請求,實際上就是實現請求體和請求頭。今天我們就來實現這四個點的內容。
思考題:get請求可以傳送body麼?大家可以思考下,答案在結尾。不要提前看哦~
一、請求頭和請求體的處理
處理請求的body,實際上就是XMLHttpRequest的send方法,它可以接收一個body作為引數,這個引數可以是Document、XMLHttpRequestBodyInit或者null。而XMLHttpRequestBodyInit可以是 Blob
, BufferSource
(en-US), FormData
, URLSearchParams
, 或者 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請求有啥區別?