web系列之Ajax

_Bruce發表於2018-11-06

寫在前面的話

本文就是對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 === xhrevent.lengthComputable表示進度資訊是否可用,event.total:Content-Lengt的預期位元組數。event.loaded:已接收的位元組數。(需要伺服器返回Content-Length頭部,否則lengthComputable一直為false)。
  • error:請求出錯觸發。
  • abortxhr.abort(); 終止連線時觸發。
  • load:接受到完整資料時觸發,相當於readyState === 4時
  • loadend:通訊完成,不管是error、abort或者load,都會導致此事件的觸發(沒有瀏覽器實現)。

ps: 為確保相容性、正常執行,onreadystatechange、progress最好在open之前繫結。

xhr上的屬性:

  • responseText:作為響應主體被返回的文字
  • responseXML:如果返回型別是"text/xml" || "application/xml" 則這個屬性儲存這XML DOM文件。否則為null
  • statushttp狀態碼
  • statusText:狀態碼的說明
  • readyState:取值如下:
    • 0:未初始化,未呼叫open();
    • 1:已初始化,呼叫了open();
    • 2:傳送。send();
    • 3:接收。已接收到部分響應。
    • 4:完成,全部over~ 需要注意的是,每當readyState變化的時候都會觸發onreadystatechange事件,而且這個事件最好在open之前就繫結(為了相容性)。
  • 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
  • redirectfollow,error,manual
  • integritysubresource 完整性值(integrity value)
  • cache: 設定 cache 模式 (default,reload,no-cache)

headers

可以通過new Header(); 來建立請求頭:

let headers = new Headers({'Content-Type': 'text/plain'});
headers.append('accept', 'text/*');
複製程式碼

定義在Headers之上的一些方法如下:

web系列之Ajax

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 引數式JSONJSONP的原理其實就是利用了<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

    1. Request method是以下三種方法之一的:
    • HEAD
    • GET
    • POST
    1. 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頭部。

web系列之Ajax

這個Origin欄位作用是告訴伺服器,本次跨域請求是源自哪個主機、埠、協議,伺服器以此來判斷是否允許此次跨域。

如果Origin不在伺服器的許可範圍內,那伺服器就返回正常的HTTP響應。瀏覽器發現響應的Access-Control-Allow-Origin和發起請求的源不相等,或者根本沒有這個欄位,則瀏覽器拒絕此次請求。會被xhronerror事件捕獲,這種錯誤無法通過狀態碼識別。

web系列之Ajax

否則伺服器返回的響應會多出(所謂多出,其實就是瀏覽器設定了這些頭部)等頭部資訊。

web系列之Ajax

可以看到多出了'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 methodput、delete,或者Content-Typeapplication/json等。 非簡單請求的CORS請求會在正式通訊之前進行一次HTTP查詢請求,稱之為 預檢請求(preflight)。瀏覽器先詢問伺服器,當傳送方的域名在伺服器允許之列,並且傳送方使用的頭部、請求方法都是伺服器允許的時候才會傳送正式的Ajax請求,否則報錯。

非簡單請求除了Origin以外,還會傳送兩個特殊的頭部:'Access-Control-Request-Method','Access-Control-Request-Headers'

Access-Control-Request-Method

瀏覽器此次CORS會用到的HTTP方法。

Access-Control-Request-Headers

指出瀏覽器會傳送的額外的頭部

web系列之Ajax
瀏覽器根據伺服器返回的'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

它的原理是父視窗可以對iframeURL進行讀寫,而和祖先視窗(不僅僅是父視窗)同源iframe也可以讀寫父視窗的URL,而hash部分不會傳送到伺服器(不會產生http請求),所以可以通過修改hash來實現雙向的通訊。 具體操作是:
super視窗中有一個跨域的iframe0
iframe0中又有一個和super同源的iframe1
如圖所示,顏色表示是否同源。

web系列之Ajax

    1. iframe0想要傳送資料的時候,可以直接修改iframe1hash(跨域也可以)
    1. iframe1監聽onhashchange事件,拿到hash部分後,再修改superhash(因為iframe1super同源,所以可以)
    1. 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>
複製程式碼

結果:

web系列之Ajax

④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等內容回頭待續。

參考

正確面對跨域,別慌
fetch簡介: 新一代Ajax API
ajax跨域,這應該是最全的解決方案了

相關文章