HTTP你們都不陌生,可是HTTP的許多細節就並非不少人都知道了,本文將討論一些容易被忽略但又比較重要的點。css
首先,怎麼用原生JS寫一個GET請求呢?以下代碼,只需3行:html
let xhr = new XMLHttpRequest();
xhr.open("GET", "/list");
xhr.send();
複製代碼
xhr.open第一個參數是請求方法,第二個參數是請求url,而後把它send出去就好了。webpack
若是須要加上請求參數,若是用jQuery的ajax,那麼是這麼寫的:nginx
$.ajax({
url: "/list",
data: {
page: 5
}
});複製代碼
若是是用原生的話就得拼在請求url上面,即open的第二個參數:git
而且參數須要轉義,以下代碼所示:github
function ajax (url, data) {
let args = [];
for (let key in data) {
// 參數須要轉義
args.push(`${encodeURIComponent(key)} =
${encodeURIComponent(data[key])}`);
}
let search = args.join("&");
// 判斷當前url是否已有參數
url += ~url.indexOf("?") ? `&${search}` : `?${search}`;
let xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();
}複製代碼
那爲何用jq就不用呢?由於jq幫咱們作了,jq的ajax支持一個叫processData的參數,默認爲true:web
$.ajax({
url: "/list",
data: {
page: 5
},
processData: true
});
複製代碼
這個參數的做用是用來處理傳進來的data的,以下jq的源碼:ajax
若是傳了data,而且processData爲true,而且data不是一個string了,就會調param處理data。而後咱們來看下這個param函數是怎麼實現的:數據庫
能夠看到,它也是跟我本身實現的ajax相似,把key和value轉義用"="拼接,而後push到一個數組,最後再join地一下。不同的地方是它的判斷邏輯比個人複雜,它會再調一個buildParams的函數去處理key/value,由於value多是一個數組,也多是一個Object。若是value是一個Object,那麼直接encode一下就會變成"[object Object]"的轉義了:json
因此buildParams在處理每一個key/value時,會先判斷當前value是不是一個數組或者是Object,以下圖所示:
若是它是一個數組的話,這個數組的每個元素都會變成單獨的一個請求字段,它的key是父級的key拼上數組的索引獲得,如{ids: [1, 2, 3]}就會被拼成:ids[0]=一、ids[1] = 二、ids[2] = 3,若是是一個Object的話key的後綴就是子Object的key,如{user: {id: 1333, name: "yin"}}會被拼成:user[id]=133三、user[name]=yin,不然就認爲它是一個簡單的類型,就直接調一下param函數定義的add,把它push到s那個數組。這裏用到了遞歸調用,會不斷地拼key值,直到value是一個普通變量了,就到了最後面的else邏輯。
也就是說,如下代碼:
$.ajax({
url: "/list",
data: {
user: {
name: "yin",
age: 18
}
},
});複製代碼
將會被拼成的url爲:
/list?user[name]=yin&user[age]=18
注意上面的中括號尚未轉義。而若是是一個數組的話:
$.ajax({
url: "/list",
data: {
ids: [1, 2, 3]
},
});複製代碼
拼成的請求url爲:
/list?ids[0]=1&ids[1]=2&ids[2]=3
若是後端用的Java的Spring MVC框架的話,是理解這種格式的,框架收到這樣的參數後會生成一個Array,傳遞給業務代碼,業務代碼是不用關心怎麼處理這種參數的。其它的框架應該也相似。
怎麼用原生JS寫一個POST請求呢?以下圖所示:
POST請求的參數就不是放在url了,而是放在send裏面,即請求體。你可能會問:難道就不能放url麼?我就要放url。若是你夠任性,那麼能夠,前提是後端所使用的http框架可以在url裏面取數據,由於它必定會收到url,也必定會收到請求體,因此取決於它要怎麼處理,按照http標準,若是請求方法是POST,那麼應該是得去請求體拿的,就不會在url的search上取了,固然它能夠改一下,改爲兩個均可以拿。
而後咱們會發現請求的mime類型是text/plain:
而且查看請求參數的時候,並非平時所看到可以按照字段一行行地展現:
這是爲何呢?這是由於咱們沒有設置它的Content-Type,以下代碼:
let xhr = new XMLHttpRequest();
xhr.open("POST", "/add");
xhr.setRequestHeader("Content-type",
"application/x-www-form-urlencoded");
xhr.send("id=5&name=yin");複製代碼
若是設置Content-Type爲x-www-form-urlencoded的話,那麼在檢查的話,Chrome也會按字段分行展現了:
這個也是jq默認的Content-Type:
它是一種最經常使用的一種請求編碼方式,支持GET/POST等方法,特色是:全部的數據變成鍵值對的形式key1=value1&key2=value2的形式,而且特殊字符須要轉義成utf-8編號,如空格會變成%20:
因爲中文在utf-8須要佔用3個字節,因此它有3個%符號。
咱們剛剛是xhr.send一個字符串,若是send一個Object會怎麼樣呢?以下代碼所示:
let xhr = new XMLHttpRequest();
xhr.open("POST", "/add");
xhr.send({id:5, name: "yin"});複製代碼
檢查控制檯的時候是這樣的:
也就是說,其實是調了Object的toString方法。因此能夠看到,在send的數據須要轉成字符串。
除了字符串以外,send還支持FormData/Blob等格式,如:
let form = $("form")[0];
xhr.send(new FormData(form));
複製代碼
但最後都是被轉成字符串發送。
咱們再看下其它的請求格式,如Github的REST API是使用json的格式發請求:
這個時候要求格式要變成json,就須要指定Content-Type爲application/json,而後send的數據要stringify一下:
let xhr = new XMLHttpRequest();
xhr.open("POST", "/add");
xhr.setRequestHeader("Content-type", "application/json");
let data = {id:5, name: "yin"};
xhr.send(JSON.stringify(data));複製代碼
若是是用jq的話,那麼能夠這樣:
$.ajax({
processData: false,
data: JSON.stringify(data),
contentType: "application/json"
});
複製代碼
這個時候processData爲false,告訴jq不要處理數據了——即拼成key1=value1&key2=value2的形式,直接把傳給它的數據send就行了。
咱們能夠比較json和urlencoded這兩種形式的優缺點,json的缺點是parse解析的工做量要明顯高於split("&")的工做量,可是json的優勢又在於表達複雜結構的時候比較簡潔,如二維數組(m * n)在urlencoded須要拆成m * n個字段,而json就不用了。因此相對來講,若是請求數據結構比較簡單應該是使用經常使用的urlencoded比較有利,而比較複雜時使用json比較有利。一般來講比較經常使用的仍是urlencoded.
還有第3種常見的編碼是multipart/form-data,這種也能夠用來發請求,以下代碼所示:
let formData = new FormData();
formData.append("id", 5); // 數字5會被當即轉換成字符串 "5"
formData.append("name", "#yin");
// formData.append("file", input.files[0]);
let xhr = new XMLHttpRequest();
xhr.open("POST", "/add");
xhr.send(formData);複製代碼
它一般用於上傳文件,上傳文件必需要使用這種格式。上面代碼發送的內容以下圖所示:
每一個字段之間用一個隨機字符串隔開,保證發送的內容不會出現這個字符串,這樣發送的內容就不須要進行轉義了,由於若是文件很大的話,轉義須要花費至關的時間,體積也可能會成倍地增加。
而後再討論一個問題,咱們知道在瀏覽器地址欄輸入一個網址請求數據,這個時候是用的GET請求,而咱們在代碼裏面用ajax發的GET請求也是GET,瀏覽器訪問網址的GET和ajax的GET有什麼區別嗎?
爲了可以觀察到瀏覽器自已發出去的GET,須要用一個抓包工具看一下這個GET是怎麼樣的,以下圖所示:
瀏覽器本身發的GET有一個明顯的特色,它會設置http請求頭的Accept字段,而且把text/html排在第一位,即它最但願收到的是html格式。而動態的ajax抓包顯示是這樣的:
能夠看到,使用地址欄訪問的和使用ajax的get請求本質上都是同樣的,只是使用ajax咱們能夠設置http請求頭,而使用地址欄訪問的是由瀏覽器添加默認的請求頭。
上面是使用http抓的包,咱們能夠看到請求的完整url,包括請求的參數,而若是是抓的https的包的話,GET放在url上的參數就看不到了:
也就是說https的請求報文是加密的,包括請求的uri等,須要解密後才能看到。那是否是說使用https的GET也是安全的,而不是https的POST不會比GET安全?
咱們先來看一下http的請求報文,以下圖所示:
若是使用抓包工具的話,能夠看到請求報文確實是按照上圖排列的,如圖所示的GET:
而POST是這樣的:
因此本質上GET/POST是同樣的,只是GET把數據拼到url,而POST是放到請求體。另一點,url是有長度限制的,包括瀏覽器和接收的服務如nginx,而請求體是沒有限制的(瀏覽器沒有限制,可是通常nginx接收會有限制),還有POST的數據支持多種編碼格式。
雖然如此,POST仍是比GET安全的,體如今如下幾點:
接着討論請求響應狀態碼。不少人都知道200、40四、500這幾個,對於其它的可能就不甚瞭解了。這裏我把一些經常使用的狀態碼列一下。
1. 301 永久轉移
當你想換域名的時候,就可使用301,如以前的域名叫www.renfed.com,後來換了一個新域名www.rrfed.com,但願用戶訪問老域名的時候可以自動跳轉到新的域名,那麼就可使用nginx返回301:
server {
listen 80;
server_name www.rrfed.com;
root /home/fed/wordpress;
return 301 https://www.rrfed.com$request_uri;
}複製代碼
瀏覽器收到301以後,就會自動跳轉了。搜索引擎在爬的時候若是發現是301,在若干天以後它會把以前收錄的網頁的域名給換了。
還有一個場景,若是但願訪問http的時候自動跳轉到https也是能夠用301,由於若是直接在瀏覽器地址欄輸入域名而後按回車,前面沒有帶https,那麼是默認的http協議,這個時候咱們但願用戶可以訪問安全的https的,不要訪問http的,因此要作一個重定向,也可使用301,如:
server {
listen 80;
server_name www.rrfed.com;
if ($scheme != "https") {
return 301 https://$host$request_uri;
}
}
複製代碼
2. 302 Found 資源暫時轉移
不少短連接跳轉長連接就是使用的302,以下圖所示:
3. 304 Not Modified 沒有修改
在本地使用webpack-dev-server開發的時候,若是沒有改js/css,那麼刷新頁面的時候請求本地的js/css文件將返回304,以下圖所示:
webpack-dev-server的服務怎麼知道沒有修改呢,由於瀏覽器在請求的時候帶上了etag,如:
W/"10e632-Oz38I6asQyS459XpsaJYkjMUoZI"
服務會計算當前文件的etag,它是一個文件的哈希值,而後比較一下傳過來的etag,若是相等,則認爲沒有修改返回304。若是有修改的話就會返回200和文件的內容,並從新給瀏覽器一個新的etag。下次請求的時候瀏覽器就會帶上這個新的etag。若是把控制檯的disable cached打開的話,那麼瀏覽器即便有etag也不會帶上。另一個判斷有沒有修改的字段是Last Modified Time,這是根據文件的修改時間。
4. 400 Bad Request 請求無效
當必要參數缺失、參數格式不對時,後端一般會返回400,以下圖所示:
並帶上了提示信息:
{"message":"opportunityId type mismatch required type 'long' "}
經過400能夠知道請求參數有誤,結合提示信息,說明須要傳一個數字,而不是字符串。
5. 403 Forbidden 拒絕服務
服務可以理解你的請求,包括傳參正確,可是拒絕提供服務。例如,服務容許直接訪問靜態文件:
可是不容許訪問某個目錄:
不然,別人對你服務器上的文件就一覽無遺了。
403和401的區別在於,401是沒有認證,沒有登錄驗證之類的錯誤。
6. 500 內部服務器錯誤
如業務代碼出現了異常沒有捕獲,被tomcat捕獲了,就會返回500錯誤:
如:數據庫字段長度限制爲30個字符,若是沒有判斷直接插入一條31個字符的記錄,就會致使數據庫拋異常,若是異常沒有捕獲處理,就直接返回500。
當服務完全掛了,連返回都沒有的時候,那麼就是502了。
7. 502 Bad Gateway 網關錯誤
以下圖所示:
這種狀況是由於nginx收到請求,可是請求沒有打過去,多是由於業務服務掛了,或者是打過去的端口號寫錯了:
server {
location / {
# webpack的服務
proxy_pass https://127.0.0.1:7071;
}
}
複製代碼
nginx返回了502.
8. 504 Gateway Timeout 網關超時
一般是由於服務處理請求過久,致使超時,如PHP服務默認的請求響應最長處理時間爲30s,若是超過30s,將會掛掉,返回504,以下圖所示:
這種狀況多是由於服務還要請求第三方的服務,第三方服務處理時間較久沒有返回,如在向FCM發送Push的時候,若是一個請求裏面須要發送的訂閱的瀏覽器(subscriptions)太多了,就常常會處理好久,致使504.
9. 101 協議轉換
websocket是從http升級而來,在創建鏈接前須要先經過http進行協議升級:
還有一個600,600是一種不太經常使用的狀態碼,表示服務器沒有返回響應頭部,只返回實體內容。
這些狀態碼實際上就是一個數字,能夠任意返回,可是最好是按照http的規定返回合適的狀態碼。若是返回四、五、6開頭的http狀態碼,瀏覽器將會打印錯誤,認爲當前請求失敗。
咱們尚未說怎麼判斷請求成功了,以下代碼所示:
xhr.open("POST", UPLOAD_URL);
xhr.onreadystatechange = function() {
// readyState爲4表示請求完成
if (this.readyState === 4){
if (this.status === 200) {
let response = JSON.parse(this.responseText);
if (!response.status || response.status.code !== 0) {
// 失敗
callback.failed && callback.failed();
} else {
// 成功
callback.success(response.data.url);
}
} else if (this.status >= 400 || this.status === 0) {
// 失敗
callback.failed && callback.failed();
// 正常不該該返回20幾的狀態碼,這種狀況也認爲是失敗
} else {
callback.failed && callback.failed();
}
}
};
xhr.send(formData);複製代碼
這裏有個問題,若是返回的狀態碼是3開頭的重定向,須要本身再去發一個請求嗎?
實踐證實,不須要,瀏覽器會自動重定向,以下圖所示:
最後,本文提到了3種經常使用的請求編碼,分別是application/www-x-form-urlencoded、application/json、multipart/form-data,第一種是最經常使用的一種,適用於GET/POST等,第二種常見於請求響應的數據格式,第三種一般用於上傳文件。而後還比較了POST和GET,雖然二者的請求數據都在http報文裏面,只是位置不同,可是考慮到用戶、搜索引擎等使用場景,POST仍是會比GET更安全。最後說了幾個經常使用的http狀態碼,並用一些實際的例子加深印象和理解。