大多數情況下,在前端發起一個網路請求我們只需關注下面幾點:
url
cookie
form
ifream
Ajax
jQuery
fetch
axios、request
帶著以上這些問題、關注點我們對幾種網路請求進行一次全面的分析。
在Ajax出現之前,web程序是這樣工作的:
web
這種交互的的缺陷是顯而易見的,任何和伺服器的交互都需要刷新頁面,用戶體驗非常差,Ajax的出現解決了這個問題。Ajax全稱Asynchronous JavaScript + XML(非同步JavaScript和XML)
Asynchronous JavaScript + XML
JavaScript
XML
使用Ajax,網頁應用能夠快速地將增量更新呈現在用戶界面上,而不需要重載(刷新)整個頁面。
Ajax本身不是一種新技術,而是用來描述一種使用現有技術集合實現的一個技術方案,瀏覽器的XMLHttpRequest是實現Ajax最重要的對象(IE6以下使用ActiveXObject)。
XMLHttpRequest
IE6
ActiveXObject
儘管X在Ajax中代表XML, 但由於JSON的許多優勢,比如更加輕量以及作為Javascript的一部分,目前JSON的使用比XML更加普遍。
X
JSON
Javascript
這裡主要分析XMLHttpRequest對象,下面是它的一段基礎使用:
var xhr = new XMLHttpRequest(); xhr.open(post,www.xxx.com,true) // 接收返回值 xhr.onreadystatechange = function(){ if(xhr.readyState === 4 ){ if(xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){ console.log(xhr.responseText); } } } // 處理請求參數 postData = {"name1":"value1","name2":"value2"}; postData = (function(value){ var dataString = ""; for(var key in value){ dataString += key+"="+value[key]+"&"; }; return dataString; }(postData)); // 設置請求頭 xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded"); // 異常處理 xhr.onerror = function() { console.log(Network request failed) } // 跨域攜帶cookie xhr.withCredentials = true; // 發出請求 xhr.send(postData);
下面分別對XMLHttpRequest對象常用的的函數、屬性、事件進行分析。
open
用於初始化一個請求,用法:
xhr.open(method, url, async);
method
get、post
async
send
用於發送HTTP請求,即調用該方法後HTTP請求才會被真正發出,用法:
HTTP
xhr.send(param)
param
string、Blob
abort
用於終止一個ajax請求,調用此方法後readyState將被設置為0,用法:
ajax
readyState
0
xhr.abort()
setRequestHeader
用於設置HTTP請求頭,此方法必須在open()方法和send()之間調用,用法:
open()
send()
xhr.setRequestHeader(header, value);
getResponseHeader
用於獲取http返回頭,如果在返回頭中有多個一樣的名稱,那麼返回的值就會是用逗號和空格將值分隔的字元串,用法:
http
var header = xhr.getResponseHeader(name);
用來標識當前XMLHttpRequest對象所處的狀態,XMLHttpRequest對象總是位於下列狀態中的一個:
|值|狀態|描述 |-|-|-| |0 | UNSENT |代理被創建,但尚未調用 open() 方法。 |1 | OPENED |open() 方法已經被調用。 |2 | HEADERS_RECEIVED | send()方法已經被調用,並且頭部和狀態已經可獲得。 |3 | LOADING | 下載中; responseText 屬性已經包含部分數據。 |4 | DONE | 下載操作已完成。
UNSENT
OPENED
HEADERS_RECEIVED
LOADING
responseText
DONE
status
表示http請求的狀態, 初始值為0。如果伺服器沒有顯式地指定狀態碼, 那麼status將被設置為默認值, 即200。
200
responseType
表示響應的數據類型,並允許我們手動設置,如果為空,默認為text類型,可以有下面的取值:
text
|值 |描述| |-|-|-| |""| 將 responseType設為空字元串與設置為"text"相同, 是默認類型 (實際上是 DOMString)。 |"arraybuffer"| response 是一個包含二進位數據的JavaScript ArrayBuffer 。 |"blob"|response是一個包含二進位數據的 Blob 對象 。 |"document"| response 是一個HTML Document或XML XMLDocument,這取決於接收到的數據的 MIME 類型。 |"json"|response 是一個 JavaScript 對象。這個對象是通過將接收到的數據類型視為JSON解析得到的。 |"text"|response是包含在DOMString對象中的文本。
""
"text"
DOMString
"arraybuffer"
response
JavaScript ArrayBuffer
"blob"
Blob
"document"
HTML Document
XML XMLDocument
"json"
返迴響應的正文,返回的類型由上面的responseType決定。
withCredentials
ajax請求默認會攜帶同源請求的cookie,而跨域請求則不會攜帶cookie,設置xhr的withCredentials的屬性為true將允許攜帶跨域cookie。
xhr
true
onreadystatechange
xhr.onreadystatechange = callback;
當readyState屬性發生變化時,callback會被觸發。
onloadstart
xhr.onloadstart = callback;
在ajax請求發送之前(readyState==1後, readyState==2前),callback會被觸發。
readyState==1
readyState==2
callback
onprogress
xhr.onprogress = function(event){ console.log(event.loaded / event.total); }
回調函數可以獲取資源總大小total,已經載入的資源大小loaded,用這兩個值可以計算載入進度。
total
loaded
onload
xhr.onload = callback;
當一個資源及其依賴資源已完成載入時,將觸發callback,通常我們會在onload事件中處理返回值。
onerror
xhr.onerror = callback;
當ajax資源載入失敗時會觸發callback。
ontimeout
xhr.ontimeout = callback;
當進度由於預定時間到期而終止時,會觸發callback,超時時間可使用timeout屬性進行設置。
timeout
在很長一段時間裡,人們使用jQuery提供的ajax封裝進行網路請求,包括$.ajax、$.get、$.post等,這幾個方法放到現在,我依然覺得很實用。
$.ajax、$.get、$.post
$.ajax({ dataType: json, // 設置返回值類型 contentType: application/json, // 設置參數類型 headers: {Content-Type,application/json},// 設置請求頭 xhrFields: { withCredentials: true }, // 跨域攜帶cookie data: JSON.stringify({a: [{b:1, a:1}]}), // 傳遞參數 error:function(xhr,status){ // 錯誤處理 console.log(xhr,status); }, success: function (data,status) { // 獲取結果 console.log(data,status); } })
$.ajax只接收一個參數,這個參數接收一系列配置,其自己封裝了一個jqXHR對象,有興趣可以閱讀一下jQuary-ajax 源碼
$.ajax
jqXHR
常用配置:
當前頁地址。發送請求的地址。
type
類型:String 請求方式 ("POST" 或"GET"), 默認為 "GET"。注意:其它HTTP請求方法,如PUT和 DELETE也可以使用,但僅部分瀏覽器支持。
String
"POST"
"GET"
PUT
DELETE
類型:Number設置請求超時時間(毫秒)。此設置將覆蓋全局設置。
Number
success
類型:Function 請求成功後的回調函數。
Function
jsonp
在一個jsonp請求中重寫回調函數的名字。這個值用來替代在"callback=?"這種GET或POST請求中URL參數里的"callback"部分。
"callback=?"
GET
POST
URL
"callback"
error 類型:Function 。請求失敗時調用此函數。
注意:源碼里對錯誤的判定:
isSuccess = status >= 200 && status < 300 || status === 304;
返回值除了這幾個狀態碼都會進error回調。
error
dataType
"xml": 返回 XML 文檔,可用 jQuery 處理。 "html": 返回純文本 HTML 信息;包含的 script 標籤會在插入 dom 時執行。 "script": 返回純文本 JavaScript 代碼。不會自動緩存結果。除非設置了 "cache" 參數。注意:在遠程請求時(不在同一個域下),所有 POST 請求都將轉為 GET 請求。(因為將使用 DOM 的 script標籤來載入) "json": 返回 JSON 數據 。 "jsonp": JSONP 格式。使用 JSONP 形式調用函數時,如 "myurl?callback=?" jQuery 將自動替換 ? 為正確的函數名,以執行回調函數。 "text": 返回純文本字元串
data
類型:String 使用JSON.stringify轉碼
JSON.stringify
complete
類型:Function請求完成後回調函數 (請求成功或失敗之後均調用)。
類型:Boolean 默認值:true。默認設置下,所有請求均為非同步請求。如果需要發送同步請求,請將此選項設置為 false。
Boolean
false
contentType
類型:String默認值: "application/x-www-form-urlencoded"。發送信息至伺服器時內容編碼類型。
"application/x-www-form-urlencoded"
鍵值對這樣組織在一般的情況下是沒有什麼問題的,這裡說的一般是,不帶嵌套類型JSON,也就是 簡單的JSON,形如這樣:
{ a: 1, b: 2, c: 3 }
但是在一些複雜的情況下就有問題了。 例如在 Ajax中你要傳一個複雜的 json 對像,也就說是對象嵌數組,數組中包括對象,你這樣傳:application/x-www-form-urlencoded 這種形式是沒有辦法將複雜的JSON組織成鍵值對形式。
json
application/x-www-form-urlencoded
{ data: { a: [{ x: 2 }] } }
可以用如下方式傳遞複雜的json對象
$.ajax({ dataType: json, contentType: application/json, data: JSON.stringify({a: [{b:1, a:1}]}) })
近年來前端MV*的發展壯大,人們越來越少的使用jQuery,我們不可能單獨為了使用jQuery的Ajax api來單獨引入他,無可避免的,我們需要尋找新的技術方案。
MV*
Ajax api
尤雨溪在他的文檔中推薦大家用axios進行網路請求。axios基於Promise對原生的XHR進行了非常全面的封裝,使用方式也非常的優雅。另外,axios同樣提供了在node環境下的支持,可謂是網路請求的首選方案。
axios
Promise
XHR
node
未來必定還會出現更優秀的封裝,他們有非常周全的考慮以及詳細的文檔,這裡我們不多做考究,我們把關注的重點放在更底層的APIfetch。
Fetch API是一個用用於訪問和操縱HTTP管道的強大的原生 API。
Fetch API
這種功能以前是使用 XMLHttpRequest實現的。Fetch提供了一個更好的替代方法,可以很容易地被其他技術使用,例如 Service Workers。Fetch還提供了單個邏輯位置來定義其他HTTP相關概念,例如CORS和HTTP的擴展。
可見fetch是作為XMLHttpRequest的替代品出現的。
使用fetch,你不需要再額外載入一個外部資源。但它還沒有被瀏覽器完全支持,所以你仍然需要一個polyfill。
polyfill
一個基本的 fetch請求:
const options = { method: "POST", // 請求參數 headers: { "Content-Type": "application/json"}, // 設置請求頭 body: JSON.stringify({name:123}), // 請求參數 credentials: "same-origin", // cookie設置 mode: "cors", // 跨域 } fetch(http://www.xxx.com,options) .then(function(response) { return response.json(); }) .then(function(myJson) { console.log(myJson); // 響應數據 }) .catch(function(err){ console.log(err); // 異常處理 })
Fetch API提供了一個全局的fetch()方法,以及幾個輔助對象來發起一個網路請求。
fetch()
fetch()方法用於發起獲取資源的請求。它返回一個promise,這個 promise 會在請求響應後被 resolve,並傳回 Response 對象。
promise
resolve
Response
Headers
可以通過Headers()構造函數來創建一個你自己的headers對象,相當於 response/request 的頭信息,可以使你查詢到這些頭信息,或者針對不同的結果做不同的操作。
Headers()
headers
response/request
var myHeaders = new Headers(); myHeaders.append("Content-Type", "text/plain");
Request
通過Request()構造函數可以創建一個Request對象,這個對象可以作為fetch函數的第二個參數。
Request()
在fetch()處理完promises之後返回一個Response實例,也可以手動創建一個Response實例。
promises
由於fetch是一個非常底層的API,所以我們無法進一步的探究它的底層,但是我們可以藉助它的polyfill探究它的基本原理,並找出其中的坑點。
API
由代碼可見,polyfill主要對Fetch API提供的四大對象進行了封裝:
Fetch
代碼非常清晰:
xhr onload
body
可以發現,調用reject有三種可能:
reject
注意:當和伺服器建立簡介,並收到伺服器的異常狀態碼如404、500等並不能觸發onerror。當網路故障時或請求被阻止時,才會標記為 reject,如跨域、url不存在,網路異常等會觸發onerror。
404、500
所以使用fetch當接收到異常狀態碼都是會進入then而不是catch。這些錯誤請求往往要手動處理。
可以在request參數中傳入signal對象,並對signal對象添加abort事件監聽,當xhr.readyState變為4(響應內容解析完成)後將signal對象的abort事件監聽移除掉。
request
signal
xhr.readyState
4
這表示,在一個fetch請求結束之前可以調用signal.abort將其終止。在瀏覽器中可以使用AbortController()構造函數創建一個控制器,然後使用AbortController.signal屬性
signal.abort
AbortController()
AbortController.signal
這是一個實驗中的功能,此功能某些瀏覽器尚在開發中
在header對象中維護了一個map對象,構造函數中可以傳入Header對象、數組、普通對象類型的header,並將所有的值維護到map中。
map
Header
header
之前在fetch函數中看到調用了header的forEach方法,下面是它的實現:
forEach
可見header的遍歷即其內部map的遍歷。
另外Header還提供了append、delete、get、set等方法,都是對其內部的map對象進行操作。
append、delete、get、set
Request對象接收的兩個參數即fetch函數接收的兩個參數,第一個參數可以直接傳遞url,也可以傳遞一個構造好的request對象。第二個參數即控制不同配置的option對象。
option
可以傳入credentials、headers、method、mode、signal、referrer等屬性。
credentials、headers、method、mode、signal、referrer
這裡注意:
fetch函數中還有如下的代碼:
if (request.credentials === include) { xhr.withCredentials = true } else if (request.credentials === omit) { xhr.withCredentials = false }
默認的credentials類型為same-origin,即可攜帶同源請求的coodkie。
credentials
same-origin
然後我發現這裡polyfill的實現和MDN-使用Fetch以及很多資料是不一致的:
mdn: 默認情況下,fetch 不會從服務端發送或接收任何 cookies
於是我分別實驗了下使用polyfill和使用原生fetch攜帶cookie的情況,發現在不設置credentials的情況下居然都是默認攜帶同源cookie的,這和文檔的說明說不一致的,查閱了許多資料後都是說fetch默認不會攜帶cookie,下面是使用原生fetch在瀏覽器進行請求的情況:
然後我發現在MDN-Fetch-Request已經指出新版瀏覽器credentials默認值已更改為same-origin,舊版依然是omit。
omit
確實MDN-使用Fetch這裡的文檔更新的有些不及時,誤人子弟了...
Response對象是fetch調用成功後的返回值:
回顧下fetch中對Response`的操作:
f
中對
xhr.onload = function () { var options = { status: xhr.status, statusText: xhr.statusText, headers: parseHeaders(xhr.getAllResponseHeaders() || ) } options.url = responseURL in xhr ? xhr.responseURL : options.headers.get(X-Request-URL) var body = response in xhr ? xhr.response : xhr.responseText resolve(new Response(body, options)) }
Response構造函數:
可見在構造函數中主要對options中的status、statusText、headers、url等分別做了處理並掛載到Response對象上。
options
status、statusText、headers、url
構造函數裡面並沒有對responseText的明確處理,最後交給了_initBody函數處理,而Response並沒有主動聲明_initBody屬性,代碼最後使用Response調用了Body函數,實際上_initBody函數是通過Body函數掛載到Response身上的,先來看看_initBody函數:
_initBody
Body
可見,_initBody函數根據xhr.response的類型(Blob、FormData、String...),為不同的參數進行賦值,這些參數在Body方法中得到不同的應用,下面具體看看Body函數還做了哪些其他的操作:
xhr.response
Blob、FormData、String...
Body函數中還為Response對象掛載了四個函數,text、json、blob、formData,這些函數中的操作就是將_initBody中得到的不同類型的返回值返回。
text、json、blob、formData
這也說明了,在fetch執行完畢後,不能直接在response中獲取到返回值而必須調用text()、json()等函數才能獲取到返回值。
text()、json()
這裡還有一點需要說明:幾個函數中都有類似下面的邏輯:
var rejected = consumed(this) if (rejected) { return rejected }
consumed函數:
function consumed(body) { if (body.bodyUsed) { return Promise.reject(new TypeError(Already read)) } body.bodyUsed = true }
每次調用text()、json()等函數後會將bodyUsed變數變為true,用來標識返回值已經讀取過了,下一次再讀取直接拋出TypeError(Already read)。這也遵循了原生fetch的原則:
bodyUsed
TypeError(Already read)
因為Responses對象被設置為了 stream 的方式,所以它們只能被讀取一次
VUE的文檔中對fetch有下面的描述:
VUE
使用fetch還有很多別的注意事項,這也是為什麼大家現階段還是更喜歡axios 多一些。當然這個事情在未來可能會發生改變。
由於fetch是一個非常底層的API,它並沒有被進行很多封裝,還有許多問題需要處理:
支持傳入不同的參數類型:
function stringify(url, data) { var dataString = url.indexOf(?) == -1 ? ? : &; for (var key in data) { dataString += key + = + data[key] + &; }; return dataString; }
if (request.formData) { request.body = request.data; } else if (/^get$/i.test(request.method)) { request.url = `${request.url}${stringify(request.url, request.data)}`; } else if (request.form) { request.headers.set(Content-Type, application/x-www-form-urlencoded;charset=UTF-8); request.body = stringify(request.data); } else { request.headers.set(Content-Type, application/json;charset=UTF-8); request.body = JSON.stringify(request.data); }
fetch在新版瀏覽器已經開始默認攜帶同源cookie,但在老版瀏覽器中不會默認攜帶,我們需要對他進行統一設置:
request.credentials = same-origin; // 同源攜帶 request.credentials = include; // 可跨域攜帶
當接收到一個代表錯誤的 HTTP 狀態碼時,從 fetch()返回的 Promise 不會被標記為 reject, 即使該 HTTP 響應的狀態碼是 404 或 500。相反,它會將 Promise 狀態標記為 resolve (但是會將 resolve 的返回值的 ok 屬性設置為 false ),僅當網路故障時或請求被阻止時,才會標記為 reject。
因此我們要對fetch的異常進行統一處理
.then(response => { if (response.ok) { return Promise.resolve(response); }else{ const error = new Error(`請求失敗! 狀態碼: ${response.status}, 失敗信息: ${response.statusText}`); error.response = response; return Promise.reject(error); } });
對不同的返回值類型調用不同的函數接收,這裡必須提前判斷好類型,不能多次調用獲取返回值的方法:
.then(response => { let contentType = response.headers.get(content-type); if (contentType.includes(application/json)) { return response.json(); } else { return response.text(); } });
fetch本身沒有提供對jsonp的支持,jsonp本身也不屬於一種非常好的解決跨域的方式,推薦使用cors或者nginx解決跨域,具體請看下面的章節。
cors
nginx
fetch封裝好了,可以愉快的使用了。
嗯,axios真好用...
談到網路請求,就不得不提跨域。
瀏覽器的同源策略限制了從同一個源載入的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要安全機制。通常不允許不同源間的讀操作。
跨域條件:協議,域名,埠,有一個不同就算跨域。
下面是解決跨域的幾種方式:
使用nginx反向代理實現跨域,參考我這篇文章:前端開發者必備的nginx知識
CORS是一個W3C標準,全稱是"跨域資源共享"(Cross-origin resource sharing)。它允許瀏覽器向跨源伺服器,發出XMLHttpRequest請求。
CORS
W3C
(Cross-origin resource sharing)
服務端設置Access-Control-Allow-Origin就可以開啟CORS。 該屬性表示哪些域名可以訪問資源,如果設置通配符則表示所有網站都可以訪問資源。
Access-Control-Allow-Origin
app.all(*, function (req, res, next) { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "X-Requested-With"); res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS"); next(); });
script標籤的src屬性中的鏈接可以訪問跨域的js腳本,利用這個特性,服務端不再返回JSON格式的數據,而是返回一段調用某個函數的js代碼,在src中進行了調用,這樣實現了跨域。
script
src
js
jquery對jsonp的支持:
jquery
$.ajax({ type : "get", url : "http://xxxx" dataType: "jsonp", jsonp:"callback", jsonpCallback: "doo", success : function(data) { console.log(data); } });
fetch、axios等並沒有直接提供對jsonp的支持,如果需要使用這種方式,我們可以嘗試進行手動封裝:
fetch、axios
(function (window,document) { "use strict"; var jsonp = function (url,data,callback) {
// 1.將傳入的data數據轉化為url字元串形式 // {id:1,name:jack} => id=1&name=jack var dataString = url.indexof(?) == -1? ?: &; for(var key in data){ dataString += key + = + data[key] + &; };
// 2 處理url中的回調函數 // cbFuncName回調函數的名字 :my_json_cb_名字的前綴 + 隨機數(把小數點去掉) var cbFuncName = my_json_cb_ + Math.random().toString().replace(.,); dataString += callback= + cbFuncName;
// 3.創建一個script標籤並插入到頁面中 var scriptEle = document.createElement(script); scriptEle.src = url + dataString;
// 4.掛載回調函數 window[cbFuncName] = function (data) { callback(data); // 處理完回調函數的數據之後,刪除jsonp的script標籤 document.body.removeChild(scriptEle); }
document.body.appendChild(scriptEle); }
window.$jsonp = jsonp;
})(window,document)
postMessage()方法允許來自不同源的腳本採用非同步方式進行有限的通信,可以實現跨文本檔、多窗口、跨域消息傳遞。
postMessage()
//捕獲iframe var domain = http://scriptandstyle.com; var iframe = document.getElementById(myIFrame).contentWindow;
//發送消息 setInterval(function(){ var message = Hello! The time is: + (new Date().getTime()); console.log(blog.local: sending message: + message); //send the message and target URI iframe.postMessage(message,domain); },6000); //響應事件 window.addEventListener(message,function(event) { if(event.origin !== http://davidwalsh.name) return; console.log(message received: + event.data,event); event.source.postMessage(holla back youngin!,event.origin); },false);
postMessage跨域適用於以下場景:同瀏覽器多窗口間跨域通信、iframe間跨域通信。
postMessage
iframe
WebSocket 是一種雙向通信協議,在建立連接之後,WebSocket的 server與 client都能主動向對方發送或接收數據而不受同源策略的限制。
WebSocket
server
client
function WebSocketTest(){ if ("WebSocket" in window){ alert("您的瀏覽器支持 WebSocket!"); // 打開一個 web socket var ws = new WebSocket("ws://localhost:3000/abcd"); ws.onopen = function(){ // Web Socket 已連接上,使用 send() 方法發送數據 ws.send("發送數據"); alert("數據發送中..."); }; ws.onmessage = function (evt) { var received_msg = evt.data; alert("數據已接收..."); }; ws.onclose = function(){ // 關閉 websocket alert("連接已關閉..."); }; } else{ // 瀏覽器不支持 WebSocket alert("您的瀏覽器不支持 WebSocket!"); } }
想閱讀更多優質文章,或者需要文章中思維導圖源文件可關注我的github博客,歡迎star?。
文中如有錯誤,歡迎在評論區指正,謝謝閱讀。
TAG:前端工程師 | JavaScript | 前端框架 |