寫在前面的話
本文就是對ajax
方面的知識做一個總結,沒有什麼深入的地方。雖然總結的文章有很多,但是看自己寫的和看別人的文章感覺終究還是相去甚遠的。所以如果讀者覺得內容重複請直接右上角。
xhr & fetch
用法:
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(xhr.readyState === 4) {
if(xhr.status >= 200 & xhr.status < 300 || xhr.status === 304) {
}
}
};
xhr.open('GET', 'http://localhost:8080', true);
// 如果是POST請求,則send的引數是具體的資料
xhr.send(null);
複製程式碼
xhr上的事件:(省略字首on)
readystatechange
:每當xhr.readyState
改變時觸發。timeout
:當請求發出超過xhr.timeout
設定的時間後,依然沒有收到響應,則會觸發。如果在超時終止請求之後再呼叫訪問status
等屬性就會導致錯誤,所以最好在onreadystatechange
事件中使用try-catch
。loadstart
:收到響應1byte
後觸發。progress
:其event.tartget === xhr
,event.lengthComputable
表示進度資訊是否可用,event.total:Content-Lengt
的預期位元組數。event.loaded:
已接收的位元組數。(需要伺服器返回Content-Length
頭部,否則lengthComputable
一直為false
)。error
:請求出錯觸發。abort
:xhr.abort();
終止連線時觸發。load
:接受到完整資料時觸發,相當於readyState === 4時loadend
:通訊完成,不管是error、abort
或者load
,都會導致此事件的觸發(沒有瀏覽器實現)。
ps: 為確保相容性、正常執行,
onreadystatechange、progress
最好在open
之前繫結。
xhr上的屬性:
responseText
:作為響應主體被返回的文字responseXML
:如果返回型別是"text/xml" || "application/xml"
則這個屬性儲存這XML DOM
文件。否則為null
。status
:http
狀態碼statusText
:狀態碼的說明readyState
:取值如下:- 0:未初始化,未呼叫
open()
; - 1:已初始化,呼叫了
open()
; - 2:傳送。
send()
; - 3:接收。已接收到部分響應。
- 4:完成,全部over~
需要注意的是,每當
readyState
變化的時候都會觸發onreadystatechange
事件,而且這個事件最好在open
之前就繫結(為了相容性)。
- 0:未初始化,未呼叫
timeout
:超時時間(ms)。
xhr上的方法:
abort
:用於取消非同步請求。setRequestHeader(key, value)
:open()
後send()
前呼叫。getResponseHeader/getAllResponseHeaders
:看名字,不解釋了。overrideMinmeType
:重寫xhr
響應的MIME
(最好在send
之前呼叫,這樣可以確保絕對有效)。
誤區:
並不是所有的事件都是非同步的, xhr.onreadystatechange 和xhr.onloadstart就是同步事件。
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => console.log('ready state change');
xhr.onloadstart = () => console.log('load start');
xhr.open(method, url);
xhr.send();
console.log('sync');
// 所以結果為 ready state change => load start => sync
複製程式碼
fetch
用法:
/**
* 此處的request、response見下文
*/
fetch(request)
.then(response => {
response.json()
.then(data => console.log(data));
})
.catch(err => console.log(err));
複製程式碼
Request
可以通過new Request();
建立request
物件(當然也可以直接寫)
let request = new Request('http://localhost:8080', {
// headers見下文
headers,
method: 'GET',
mode: 'cors'
});
複製程式碼
request
上的方法:
method
: 支援GET
,POST
,PUT
,DELETE
,HEAD
url
:請求的URL
headers
: 對應的Headers
物件referrer
: 請求的referrer
資訊mode
: 可以設定cors
,no-cors
,same-origin
credentials
: 設定cookies
是否隨請求一起傳送。可以設定:omit
,same-origin
redirect
:follow
,error
,manual
integrity
:subresource
完整性值(integrity value
)cache
: 設定cache
模式 (default
,reload
,no-cache
)
headers
可以通過new Header();
來建立請求頭:
let headers = new Headers({'Content-Type': 'text/plain'});
headers.append('accept', 'text/*');
複製程式碼
定義在Headers之上的一些方法如下:
Response
fetch().then(response);
中的response
就是一個Response
物件
可以通過new Request();
建立request
物件
clone()
: 建立一個新的Response
克隆物件.error()
: 返回一個新的,與網路錯誤相關的Response
物件.redirect()
: 重定向,使用新的URL
建立新的response
物件..arrayBuffer()
: Returns a promise that resolves with an ArrayBuffer.blob()
: 返回一個promise
,resolves
是一個Blob
.formData()
: 返回一個promise
,resolves
是一個FormData
物件.json()
: 返回一個promise
,resolves
是一個JSON
物件.text()
: 返回一個promise
,resolves
是一個USVString (text)
.
跨域
同源策略
什麼是同源?
同源就是擁有相同的協議(protocol) && 主機(hostname) && 埠(port)
,那麼這兩個頁面稱為同源。一切非同源的請求均為跨域。並跨域無法隨意請求,只是說為了網站的安全性,瀏覽器才採取同源策略。
如果是協議和埠造成的跨域問題,前端是無能為力的。
跨域問題中的域,瀏覽器只是用url
首部來區分的,並不會對DNS
之後得到的IP
進行判斷。
ps:url首部 = protocol + host;
嚴格的說,瀏覽器並不是拒絕所有的跨域請求,實際上拒絕的是跨域的讀操作。瀏覽器的同源限制策略是這樣執行的:
- 通常瀏覽器允許進行跨域寫操作(Cross-origin writes),如連結,重定向;
- 通常瀏覽器允許跨域資源嵌入(Cross-origin embedding),如 img、script 標籤;
- 通常瀏覽器不允許跨域讀操作(Cross-origin reads)。
同源策略呢,限制了以下行為:
Cookie、LocalStorage、IndexDB
- 瀏覽器中不同域的框架之間是不能進行
js
的互動操作 ajax
請求發不出去(其實可以發出去,只不過瀏覽器將響應給攔截了)
跨域方式:JSONP、CORS、postMessage等
。
一、JSONP
JSONP,JSON with Padding 引數式JSON
。
JSONP
的原理其實就是利用了<script>
標籤的src
引入外部指令碼時不受同源策略的限制,通過手動新增DOM
並賦予src
請求的url
,在請求的url
中填寫接收資料的回撥,再加上伺服器對callback
的支援即可。
二、CORS
Cross-Origin Resource Sharing, CORS 跨域資源共享
。CORS
是一種web
瀏覽器的技術規範,它為web
伺服器定義了一種允許從不同域訪問其資源的方式。而這種跨域訪問是被同源策略所禁止的。CORS
系統定義了一種瀏覽器和伺服器互動的方式來確定是否允許跨域請求, 有更大的靈活性,比起簡單地允許這些操作來說更加安全。
CORS
需要瀏覽器和伺服器共同配合、支援。整個CORS
通訊過程都是由瀏覽器來完成的,除了一些限制以外,程式碼和普通的ajax
沒有什麼不同,實現CORS
的關鍵是伺服器,只要伺服器支援、實現了CORS
介面就能實現CORS
。
簡單請求(simple request)和非簡單請求(not-so-simple request)
滿足以下兩大條件的請求就是simple request
:
-
- Request method是以下三種方法之一的:
- HEAD
- GET
- POST
-
- Http頭部資訊只能(沒有Access-Control-Allow-Origin的前提下)是以下幾種:
- Accept
- Accept-Language
- Content-Language
- Content-Type: oneOf['application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain']
- Last-Event-ID 瀏覽器對簡單請求和非簡單請求的處理是不一樣的。
①簡單請求
對於簡單請求(以下都是跨域情況)瀏覽器直接發出CORS, 增加一個Origin頭部。
這個Origin
欄位作用是告訴伺服器,本次跨域請求是源自哪個主機、埠、協議,伺服器以此來判斷是否允許此次跨域。
如果Origin
不在伺服器的許可範圍內,那伺服器就返回正常的HTTP
響應。瀏覽器發現響應的Access-Control-Allow-Origin
和發起請求的源不相等,或者根本沒有這個欄位,則瀏覽器拒絕此次請求。會被xhr
的onerror
事件捕獲,這種錯誤無法通過狀態碼識別。
否則伺服器返回的響應會多出(所謂多出,其實就是瀏覽器設定了這些頭部)等頭部資訊。
可以看到多出了'Access-Control-Allow-Credentials'、'Access-Control-Allow-Headers'
等頭部,這些頭部具體意義見下文。
Access-Control-Allow-Origin
伺服器必須設定的值,否則不能實現CORS,它的值要麼是精確的請求的Origin,要麼是萬用字元*(在需要Cookie的時候不支援*)。
Access-Control-Allow-Credentials
可選。意為是否允許傳送cookie
,預設為不允許,不過這個欄位只能設定為true
。如果瀏覽器不允許傳送cookie
,刪除該欄位即可。
注意:瀏覽器在請求的時候也必須設定:xhr.withCredentials = true
;不過有的瀏覽器省略還會自動帶上cookie
,可以手動關閉。
Access-Control-Allow-Headers
可選。在CORS
中,用於設定瀏覽器可以傳送的頭部。
res.setHeader('Access-Control-Allow-Headers', 'Your-Fucking-Header');
複製程式碼
Access-Control-Expose-Headers
可選。CORS
返回請求的時候,xhr.getAllResponseHeaders();
只能拿到6個基本頭部欄位:'Cache-Control'、'Content-Language'、'Content-Type'、'Expires'、'Last-Modified'、'Pragma'
。通過res.setHeader('Access-Control-Expose-Headers', 'some headers');
可以獲得允許的header
。
②非簡單請求
非簡單請求指的是那種對伺服器有特殊要求的請求,如:request method
是put、delete
,或者Content-Type
為application/json
等。
非簡單請求的CORS
請求會在正式通訊之前進行一次HTTP查詢請求,稱之為 預檢請求(preflight
)。瀏覽器先詢問伺服器,當傳送方的域名在伺服器允許之列,並且傳送方使用的頭部、請求方法都是伺服器允許的時候才會傳送正式的Ajax
請求,否則報錯。
非簡單請求除了Origin
以外,還會傳送兩個特殊的頭部:'Access-Control-Request-Method','Access-Control-Request-Headers'
。
Access-Control-Request-Method
瀏覽器此次CORS會用到的HTTP方法。
Access-Control-Request-Headers
指出瀏覽器會傳送的額外的頭部
瀏覽器根據伺服器返回的'Access-Control-Allow-Origin'
和'Access-Control-Allow-Headers'
來判斷伺服器是否允許CORS
,除此之外還有以下頭部:
Access-Control-Allow-Methods
必需。值由','分割的String
,意為支援的CORS
請求方法,返回的是所有支援的方法,不是瀏覽器設定的那個方法,避免多次preflight
。
Access-Control-Max-Age
可選。單位: s(秒)。意為本次preflight
的有效時間。在有效時間內不用再次傳送預檢請求,即允許快取該回應。
CORS用到的HTTP頭部
Headers | Server | Browser |
---|---|---|
Access-Control-Allow-Orgin | √ | × |
Access-Control-Allow-Headers | √ | × |
Access-Control-Allow-Methods | √ | × |
Access-Control-Max-Age | √ | × |
Access-Control-Allow-Credentials | √ | √ |
Access-Control-Expose-Headers | √ | × |
Access-Control-Request-Method | × | √ |
Access-Control-Request-Headers | × | √ |
CORS與JSONP的比較
-- | 目的 | 支援方法 | 優勢 | 不足 |
---|---|---|---|---|
CORS | 跨域 | 所有HTTP請求方法 | 請求方法不僅僅侷限於GET,支援所有HTTP請求方法。安全性高。 | 老版本瀏覽器不支援CORS,有一定相容性問題,比如IE10及更早版本、Safari4及更早版本、FireFox3.5及更早版本都不支援。 |
JSONP | 跨域 | GET | 可以向老式、不支援CORS的網站請求資料。而且設定簡單,無需設定過多的響應、請求頭部。 | ①只能支援GET方法 ②對於存在惡意行為的伺服器存在一定的安全隱患。 ③需要一個接收資料的全域性函式,汙染了全域性作用域。 ④判斷請求是否失敗不容易(H5給script新增error事件,但是等瀏覽器實現還需以時日)。 |
其它的一些跨域方法(感覺沒diao用)
①document.domain
如果兩個網頁的主域名相同,這個時候可以令document.domain
都為其主域名(document.domain
只能將其設定為自身和更高一級的父域名)。
由於同源限制的第二條,不同域的iframe
之間不能進行js
互動。所以通過iframe.contentWindow
獲取到的window
物件,它的方法和屬性幾乎都是不可用的,並且不允許獲取此window.document
。
這個時候:
document.domain = /* 兩個頁面共同的父級域名 */;
複製程式碼
然後就可以得到iframe.contentWindow
的屬性了。也可以通過iframe
裡面的方法請求資料,以此也可以達到跨域的目的。
②location.hash
它的原理是父視窗可以對iframe
的URL
進行讀寫,而和祖先視窗(不僅僅是父視窗)同源iframe
也可以讀寫父視窗的URL
,而hash
部分不會傳送到伺服器(不會產生http
請求),所以可以通過修改hash
來實現雙向的通訊。
具體操作是:
super
視窗中有一個跨域的iframe0
,
iframe0
中又有一個和super
同源的iframe1
。
如圖所示,顏色表示是否同源。
-
iframe0
想要傳送資料的時候,可以直接修改iframe1
的hash
(跨域也可以)
-
iframe1
監聽onhashchange
事件,拿到hash
部分後,再修改super
的hash
(因為iframe1
和super
同源,所以可以)
-
super
也監聽onhashchange
事件,就可以拿到資料了。 程式碼如下:
super:
<iframe id = "iframe" src="http://localhost:8080/iframe0.html"></iframe>
<script type="text/javascript">
let counter = 0;
let url = "http://localhost:8080/iframe0.html#";
const iframe = document.getElementById('iframe');
window.onhashchange = function(event) {
console.log('_我得到資料:', event.newURL.split('#')[1]);
}
</script>
iframe0:
<iframe src="http://localhost/iframe1.html" frameborder="0"></iframe>
<script>
let counter = 0;
let url = 'http://localhost/iframe1.html#';
const iframe = document.querySelector('iframe');
setInterval(() => {
console.log('我傳送資料:', + counter);
iframe.src = url + counter ++;
}, 2000);
</script>
iframe1:
<script>
window.onhashchange = function() {
let data = event.newURL.split('#')[1];
// 修改super的hash
window.parent.parent.location.hash = data;
}
</script>
複製程式碼
結果:
④postMessage
要使用postMessage
這個API
必須要有其他視窗的引用otherWindow
傳送方:
otherWindow.postMessage(data, targetOrigin, [transfer]);
複製程式碼
引數說明:
data
:傳送的資料targetOrigin
:指定哪些視窗接收訊息,*表示任何視窗, '/'表示當前域下的視窗。transfer
:可選,和message
同時傳遞的物件,這些物件的所有權被轉移給訊息的接收方,而傳送方不再擁有所有權。
接收方:
window.addEventListener('message', e => {
console.log(e);
}, false);
複製程式碼
在e
中有4個屬性比較重要:
data
:傳送來的訊息物件type
:傳送訊息的型別source
:傳送訊息的window
origin
:傳送訊息的origin
直接通過給e.source
新增引用型別的屬性,可以直接給傳送端的window
新增資料。
總結
其實比較常用的跨域方法就是CORS、JSONP,其他的有個大概瞭解知道就好了。其他的關於XSS、CSRF等內容回頭待續。