AJAX原理與CORS跨域

ajax作為前端開發(fā)必需的基礎(chǔ)能力之一锋勺,你可能會使用它蚀瘸,但并不一定懂得其原理狡蝶,以及更深入的服務(wù)器通信相關(guān)的知識。在最近兩天的整理過程中贮勃,看了大量的文章贪惹,發(fā)現(xiàn)自己的后端能力已經(jīng)限制自己在網(wǎng)絡(luò)通信相關(guān)的知識領(lǐng)域的探索,還是應(yīng)該盡快補齊短板寂嘉。

下面我們來聊一聊ajax相關(guān)的東西奏瞬,包括xhr/xdr/ajax/cors/http的一部分內(nèi)容,其中會拋棄一些被棄用的歷史包袱泉孩,如IE6/7等硼端。

Ajax的出現(xiàn)

2005年,Jesse James Garrett提出了Ajax的技術(shù)寓搬,其全稱為Asynchronous Javascript and XML珍昨,Ajax的核心是XMLHttpRequest對象,簡稱XHR订咸,它用于使瀏覽器向服務(wù)器請求額外的數(shù)據(jù)而不卸載頁面曼尊,極大的提高了用戶體驗。在此之前脏嚷,其實這種技術(shù)已經(jīng)存在并被一些人實現(xiàn)骆撇,但并沒有流行也沒有被瀏覽器支持。不過在此之后父叙,IE5第一次引入XHR對象神郊,并支持ajax技術(shù),后續(xù)被所有瀏覽器支持趾唱。

XMLHttpRequest對象和請求

XHR是一個API涌乳,為客戶端提供服務(wù)端和客戶端之間通信的功能,并且不會刷新頁面甜癞。它并不僅僅能取回XML類型的數(shù)據(jù)夕晓,而能取回所有類型的數(shù)據(jù),除了http協(xié)議悠咱,還支持file和ftp協(xié)議蒸辆。我們可以通過其構(gòu)造函數(shù)來創(chuàng)建一個新的XHR對象,這個操作需要在其它所有操作之前完成:

var xhr = new XMLHttpRequest();

通過控制臺我們可以很方便看到XHR的原型鏈:Object -> EventTarget -> XMLHttpRequestEventTarget -> XMLHttpRequest析既。它擁有原型鏈上和本身的方法和屬性躬贡,現(xiàn)在看下我們常用的方法:

我們解釋下它的幾個主要方法,我們在創(chuàng)建了新的xhr對象之后眼坏,首先要調(diào)用它的open()方法:

// 第一個參數(shù)可以為get/post等拂玻,表示該請求的類型// 第二個參數(shù)是請求的url,可以為相對路徑或絕對路徑// 第三個參數(shù)代表是否異步,為true時異步檐蚜,為false時同步// 第四五個參數(shù)為可選的授權(quán)使用的參數(shù)魄懂,因為安全性不推薦明文使用xhr.open('get', 'example.php', true, username, password);

在這里受同源策略的影響,當(dāng)?shù)诙€參數(shù)url跨域的時候會被瀏覽器報安全錯誤熬甚。同源策略指的是當(dāng)前頁面和目標(biāo)url協(xié)議逢渔、域名和端口均相同。后面也會講到乡括,除IE之外的瀏覽器通過XHR對象實現(xiàn)跨域請求,只需將url設(shè)置為絕對url即可智厌。

當(dāng)初始化請求完成后套鹅,我們調(diào)用send()方法發(fā)送請求:

var data = new FormData();data.append('name', 'Nicholas');// 接受一個請求主體發(fā)送的數(shù)據(jù)枕荞,如果不需要,傳入nullxhr.send(data);

當(dāng)請求的類型為get/head時,send()的參數(shù)會被忽略并置為null厌漂,send()傳遞的參數(shù)會影響到我們請求的頭部content-type的默認(rèn)值,該字段代表返回的資源內(nèi)容的類型峭咒,用于瀏覽器處理圣拄,如果沒有設(shè)置或在一些場景下,瀏覽器會進(jìn)行MIME嗅探來確定怎么處理返回的資源合溺。

在XHR2級中定義了FormData數(shù)據(jù)卒密,用于常見的類表單數(shù)據(jù)序列化:

// 直接傳入表單idvar data = new FormData(document.getElementById('user-form'));// 創(chuàng)建類表單數(shù)據(jù)var data = new FormData();data.append('name', 'Nicholas');// `FormData`可以直接被send()調(diào)用,會自動修改xhr的content-type頭部xhr.send(data);// 請求頭部的content-type: multipart/form-data; boundary=----WebKitFormBoundaryjn3q2KKRYrEH55Vz// 請求的上傳數(shù)據(jù) Request Payload:------WebKitFormBoundaryjn3q2KKRYrEH55VzContent-Disposition: form-data; name="name"Nicholas------WebKitFormBoundaryjn3q2KKRYrEH55Vz--

FormData常用的方法有

append/delete/entries/forEach/get/getAll/has/keys/set/values棠赛,都是常用的跟數(shù)組類似的方法哮奇,不再解釋。

請求方法

GET是最常見的請求類型睛约,可以將查詢字符串參數(shù)添加到URL尾部鼎俘,對XHR而言,該查詢字符串必須經(jīng)過正確編碼辩涝,每個鍵值對必須使用encodeURIComponent()進(jìn)行編碼贸伐,鍵值對之間由&分割:

// 封裝序列化鍵值對function addURLParam(url, name, value) { url += (url.indexOf('?') === -1 ? '?' : '&'; url += encodeURIComponent(name) + '=' + encodeURIComponent(value); return url; }

POST請求使用頻率僅次于GET請求,通常發(fā)送較多數(shù)據(jù)怔揩,且格式不限捉邢,數(shù)據(jù)傳遞給send()作為參數(shù)。

HTTP一共規(guī)定了九種請求方法沧踏,每一個動詞代表不同的語義歌逢,但是常用的只有上面兩種:

- OPTIONS:返回服務(wù)器針對特定資源所支持的HTTP請求方法。也可以利用向Web服務(wù)器發(fā)送'*'的請求來測試服務(wù)器的功能性翘狱。 - HEAD:向服務(wù)器索要與GET請求相一致的響應(yīng)秘案,只不過響應(yīng)體將不會被返回。這一方法可以在不必傳輸整個響應(yīng)內(nèi)容的情況下,就可以獲取包含在響應(yīng)消息頭中的元信息阱高。 - GET:向特定的資源發(fā)出請求赚导。 - POST:向指定資源提交數(shù)據(jù)進(jìn)行處理請求(例如提交表單或者上傳文件)。數(shù)據(jù)被包含在請求體中赤惊。POST請求可能會導(dǎo)致新的資源的創(chuàng)建和/或已有資源的修改吼旧。 - PUT:向指定資源位置上傳其最新內(nèi)容。 - DELETE:請求服務(wù)器刪除Request-URI所標(biāo)識的資源未舟。 - TRACE:回顯服務(wù)器收到的請求圈暗,主要用于測試或診斷。 - CONNECT:HTTP/1.1協(xié)議中預(yù)留給能夠?qū)⑦B接改為管道方式的代理服務(wù)器裕膀。 - PATCH: 用于對資源進(jìn)行部分修改

HTTP頭部信息

每個HTTP請求和響應(yīng)都帶有頭部信息员串,xhr對象允許我們操作部分頭部信息。我們可以通過xhr.setRequestHeader()方法來設(shè)置自定義的頭部信息或者修改瀏覽器默認(rèn)的正常頭部信息昼扛。常用的請求頭部:

// 下面的實例是從我本地的一次請求取出的Accept: 瀏覽器能夠處理的內(nèi)容類型寸齐。// */*Accept-Charset: 瀏覽器能夠顯示的字符集。// 未取到Accept-Encoding: 瀏覽器能夠處理的壓縮編碼抄谐。// gzip,deflateAccept-Language: 瀏覽器當(dāng)前設(shè)置的語言渺鹦。// zh-CN,zh;q=0.8,en;q=0.6Connection: 瀏覽器與服務(wù)器之間連接的類型。// keep-aliveCookie: 當(dāng)前頁面設(shè)置的任意Cookie蛹含。// JlogDataSource=jomodbHost: 發(fā)出請求的頁面所在域毅厚。// gzhxy-cdn-oss-06.gzhxy.baidu.com:8090Referer: 發(fā)出請求的頁面URI。// http://gzhxy-cdn-oss-06.gzhxy.baidu.com:8090/jomocha/index.php?r=tools/offline/indexUser-Agent: 瀏覽器的用戶代理字符串挣惰。// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36

我們一般不修改瀏覽器正常的頭部信息卧斟,可能會影響到服務(wù)器響應(yīng)。如果需要可以通過xhr.setRequestHeader()進(jìn)行修改:

// 傳入頭部鍵值對憎茂,鍵值不區(qū)分大小寫珍语,如果多次設(shè)置,則追加// 此時請求頭部的content-type: application/json, text/htmlxhr.setRequestHeader('content-type', 'application/json');xhr.setRequestHeader('content-type', 'application/json');

設(shè)置頭部信息需要在open()之后竖幔,send()之前進(jìn)行調(diào)用板乙。響應(yīng)的頭部信息在后端處理,不在此處講解拳氢。有一部分請求頭部信息不允許設(shè)置募逞,如Accept-Encoding, Cookie等。

在請求返回后馋评,我們可以獲取到響應(yīng)頭部:

// 獲取指定項的響應(yīng)頭xhr.getResponseHeader('content-type'); // application/json;charset=utf-8// 獲取所有的響應(yīng)頭部信息xhr.getAllResponseHeaders();

這里簡單說下content-type值放接,指的是請求和響應(yīng)的HTTP內(nèi)容類型,影響到服務(wù)器和瀏覽器對數(shù)據(jù)的處理方式留特,默認(rèn)為text/html纠脾,常用的如:

// 包含資源類型玛瘸,字符編碼, 邊界字符串三個參數(shù)苟蹈,可選填text/html;charset=utf-8 // html標(biāo)簽文本text/plain // 純文本text/css // css文件text/javascript // js文件// 普通的表單數(shù)據(jù)糊渊,可以通過表單標(biāo)簽的enctype屬性指定application/x-www-form-urlencode// 發(fā)送文件的POST包,包過大需要分片時使用`boundary`屬性分割數(shù)據(jù)作邊界multipart/form-data; boundary=something// json數(shù)據(jù)格式application/json// xml類型的標(biāo)記語言application/xml

XHR對象的響應(yīng)

我們現(xiàn)在對請求的發(fā)起很了解了慧脱,接著看下如何拿到響應(yīng)數(shù)據(jù)渺绒。如果我們給open()傳遞的第三個參數(shù)是true,則代表為同步請求菱鸥,那么js會被阻塞直到拿到響應(yīng)宗兼,而如果為false則是異步請求,我們只需要綁定xhr.onreadystatechange()事件監(jiān)聽響應(yīng)即可采缚。最上面的圖已經(jīng)說明了readystate的值含義针炉,所以我們可以:

// xhr v1 的寫法,檢測readystate的值扳抽,為4則說明數(shù)據(jù)準(zhǔn)備完畢,需要在open()前定義 xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status === 200 || xhr.status === 304) { console.log(xhr.responseText); } else { console.log(xhr.statusText); } }? } // xhr v2 的寫法殖侵,onload()事件說明數(shù)據(jù)準(zhǔn)備完畢 xhr.onload = function () { if (xhr.status === 200 || xhr.status === 304) { console.log(xhr.responseText); } else { console.log(xhr.statusText); } }

xhr對象的響應(yīng)數(shù)據(jù)中包含幾個屬性:

response // 響應(yīng)的數(shù)據(jù)responseURL // 發(fā)起響應(yīng)的URLresponseType // 響應(yīng)的類型贸呢,用于瀏覽器強行重置響應(yīng)數(shù)據(jù)的類型responseText // 如果為普通文本,則在這顯示responseXML // 如果為xml類型文本拢军,在這里顯示

數(shù)據(jù)會出現(xiàn)在responseText/responseXML中的哪一個楞陷,取決于服務(wù)器返回的MIME類型,當(dāng)然我們也有一些方式在瀏覽器端設(shè)置如何處理這些數(shù)據(jù):

// xhr v1 的寫法茉唉,設(shè)置響應(yīng)資源的處理類型 xhr.overrideMimeType('text/xml');// xhr v2 的寫法固蛾, 可用值為 arraybuffer/blob/document/json/text xhr.responseType = 'document';

響應(yīng)數(shù)據(jù)相關(guān)的屬性默認(rèn)為null / '',只有當(dāng)請求完成并被正確解析的時候才會有值度陆,取決于responseType的值艾凯,來確定

response/responseText/responseXML誰最終具有值。

XHR的高級功能

在xhr v2里提供了超時和進(jìn)度事件懂傀。

超時

xhr.timeout = 1000; // 1分鐘趾诗,單位為msxhr.ontimeout = function () {};

在請求send()之后開始計時,等待timeout時長后蹬蚁,如果沒有收到響應(yīng)恃泪,則觸發(fā)ontimeout()事件,超時會將readystate=4犀斋,直接觸發(fā)onreadystatechange()事件贝乎。

請求進(jìn)度

像上圖所示,xhr v2定義了不同的進(jìn)度事件:

loadstart/progress/error/abort/load/loadend叽粹,這其中我們已經(jīng)說過了onload()事件為內(nèi)容加載完成可用±佬В現(xiàn)在說一下onprogress()進(jìn)度事件:

xhr.onprogress = function (event) { if (event.lengthComputable) { console.log(event.loaded / event.total); } }

該事件會接收一個event對象却舀,其target屬性為該xhr對象,lengthComputable屬性為total size是否已知朽肥,即是否可用進(jìn)度信息禁筏,loaded屬性為已經(jīng)接收的字節(jié)數(shù),total為總字節(jié)數(shù)衡招。該事件會在數(shù)據(jù)接收期間不斷觸發(fā)篱昔,但間隔不確定。

跨域CORS

提到XHR對象始腾,我們就會講到跨域問題州刽,它是為了預(yù)防某些惡意行為的安全策略,但有時候我們需要跨域來實現(xiàn)某些功能浪箭。需要注意的是跨域并不僅僅是前端單方面的事情穗椅,它需要后端代碼進(jìn)行配合,我們只是通過一些方式跳過了瀏覽器的阻攔奶栖。

對那些可能對服務(wù)器數(shù)據(jù)產(chǎn)生副作用的 HTTP 請求方法(特別是 GET 以外的 HTTP 請求匹表,或者搭配某些 MIME 類型的 POST 請求),瀏覽器必須首先使用 OPTIONS 方法發(fā)起一個預(yù)檢請求(preflight request)宣鄙,從而獲知服務(wù)端是否允許該跨域請求袍镀。服務(wù)器確認(rèn)允許之后,才發(fā)起實際的 HTTP 請求冻晤。

CORS(Cross-Origin Resource Sharing, 跨域資源共享)的思想是瀏覽器和服務(wù)端通過頭部信息來進(jìn)行溝通確認(rèn)是否給予響應(yīng)苇羡。如:

Origin: http://www.baidu.com // 瀏覽器的頭部信息// 如果服務(wù)端認(rèn)可這個域名的跨域請求,如下設(shè)置就可跨域訪問資源Access-Control-Allow-Origin: http://www.baidu.com

如上就可以實現(xiàn)最簡單的跨域訪問鼻弧,但是此時不能攜帶任何的cookie设江,如果我們需要傳遞cookie進(jìn)行身份認(rèn)證,需要設(shè)置:

xhr.withCredentials = true; // 瀏覽器端Access-Control-Allow-Credentials: true; // 服務(wù)端

這樣我們就可以傳遞認(rèn)證信息了攘轩,但如果允許認(rèn)證叉存,

Access-Control-Allow-Origin不能設(shè)置為*,而一定是具體的域名信息撑刺。

現(xiàn)在的瀏覽器都對CORS有了實現(xiàn)鹉胖,如IE使用XDomainRequest對象,其它瀏覽器使用XMLHttpRequest對象够傍。所以在此之前有很多奇技淫巧甫菠,如通過jsonp/圖像 Ping方法都不再詳述,而且其都需要服務(wù)端配合并且有很多局限性冕屯。

IE實現(xiàn): XDomainRequest

var xdr = new XDomainRequest(); xdr.open('get', 'http://www.site.com/page'); xdr.send(null);

XDR區(qū)別于XHR:

不能傳輸cookie

只能設(shè)置請求頭部的content-type

不能訪問響應(yīng)頭部信息

只支持get/post方法

通過這些區(qū)別可以阻止一部分的CSRF(Cross-Site Request Forgery寂诱,跨站點請求偽造)和XSS(Cross-Site Scripting,跨站點腳本)安聘。

XDR與XHR非常相似痰洒,區(qū)別有幾點:

open()方法只接受兩個參數(shù)瓢棒,請求類型和URL

只允許異步請求

響應(yīng)完成觸發(fā)onload()事件,但我們只能訪問responseText原始文本丘喻,并且無法獲取響應(yīng)的status.

異常事件都會觸發(fā)error事件脯宿,并且無錯誤信息可用。

其余瀏覽器實現(xiàn): XMLHttpRequest

其余瀏覽器通過XHR對象直接實現(xiàn)了CORS泉粉,你只需要做的就是open()方法中傳入一個絕對URL连霉。

xhr.open('get', 'http://www.site.com/page', true);

相對于普通的XHR對象,CORS-XHR依然有部分限制:

不能使用setRequestHeader()定義頭部

不能傳遞cookie

調(diào)用getAllResponseHeaders()嗡靡,結(jié)果為空

其余跨域方法

上面的兩種方法已經(jīng)很成熟了跺撼,但是仍然有一部分方法可以跨域,比如圖像Ping:

var img = new Image(); img.onload = img.onerror = function () { console.log('done'); } img.src = 'http://www.site.com/test?name=Nicholas';

這種方式常用于服務(wù)端統(tǒng)計廣告的點擊次數(shù)讨彼,其缺陷為:

只能是GET請求

單向通信歉井,無法獲取響應(yīng)文本

另外還有JSONP:

function handleResponse(response) { console.log(response.ip, response.city); }var script = document.createElement('script'); script.src = 'http://freegeoip.net/json?callback=handleResponse';document.body.insertBefore(script, document.body.firstChild);

這種方式通過和服務(wù)器配合,跨域請求一個js文件并被服務(wù)器處理后傳回:

handleResponse({'name': 'Nicholas'});

然后直接在瀏覽器調(diào)用了該函數(shù)哈误,傳回的數(shù)據(jù)被當(dāng)做response形參進(jìn)行處理哩至。但它也有一些缺陷:

訪問的方式是請求js,所以如果域名不安全蜜自,則很容易被惡意代碼直接執(zhí)行并攻擊

無法檢測是否錯誤憨募,因為js不支持這樣的接口事件,只能超時判斷

上面兩種方式很容易看出袁辈,我們在支持CORS之前,使用的方法只不過是采用img/css/js等不受跨域訪問限制的對象珠漂,變相拿到了響應(yīng)數(shù)據(jù)晚缩,但都有缺陷,所以如果沒有歷史包袱媳危,建議采用XDR或XHR對象來實現(xiàn)跨域訪問荞彼。

XHR的兼容性

我們可以直接到Can I use宇晨PHP培訓(xùn):這個網(wǎng)站上查詢兼容性問題:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市待笑,隨后出現(xiàn)的幾起案子鸣皂,更是在濱河造成了極大的恐慌,老刑警劉巖暮蹂,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寞缝,死亡現(xiàn)場離奇詭異,居然都是意外死亡仰泻,警方通過查閱死者的電腦和手機荆陆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來集侯,“玉大人被啼,你說我怎么就攤上這事帜消。” “怎么了浓体?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵泡挺,是天一觀的道長。 經(jīng)常有香客問我命浴,道長娄猫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任咳促,我火速辦了婚禮稚新,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘跪腹。我一直安慰自己褂删,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布冲茸。 她就那樣靜靜地躺著屯阀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪轴术。 梳的紋絲不亂的頭發(fā)上难衰,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音逗栽,去河邊找鬼盖袭。 笑死,一個胖子當(dāng)著我的面吹牛彼宠,可吹牛的內(nèi)容都是我干的鳄虱。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼凭峡,長吁一口氣:“原來是場噩夢啊……” “哼拙已!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起摧冀,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤倍踪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后索昂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體建车,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年楼镐,在試婚紗的時候發(fā)現(xiàn)自己被綠了癞志。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡框产,死狀恐怖凄杯,靈堂內(nèi)的尸體忽然破棺而出错洁,到底是詐尸還是另有隱情,我是刑警寧澤戒突,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布屯碴,位于F島的核電站,受9級特大地震影響膊存,放射性物質(zhì)發(fā)生泄漏导而。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一隔崎、第九天 我趴在偏房一處隱蔽的房頂上張望今艺。 院中可真熱鬧,春花似錦爵卒、人聲如沸虚缎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽实牡。三九已至,卻和暖如春轴合,著一層夾襖步出監(jiān)牢的瞬間创坞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工受葛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留题涨,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓总滩,卻偏偏與公主長得像携栋,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子咳秉,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容