AJAX的出現與跨域處理
本文原載於簡書,作者_少木木(飢人谷學員),轉載已獲作者授權。
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
正文如下。
XMLHttpRequest JSON AJAX CORS 四個名詞來開會
如何發請求
在前端的世界裡也逛盪了不少日子了,目前已經get到大約5種發起請求的方式,主流的、非主流的。
可是
- 我們可能想用
GET
POST
PUT
DELETE
方法 - 不想刷新整個頁面,想用一種更易於理解的方式來響應
AJAX出現
瀏覽器和伺服器交互模式 V1.0
在AJAX
未出現之前,瀏覽器想從伺服器獲得資源,注意是獲取資源,會經過如下一個過程
- 瀏覽器發起請求->伺服器接到請求響應給你HTML文檔->瀏覽器收到資源,刷新頁面,載入獲得的的HTML。簡略的過程
我稱這種交互方式是 V1.0,此時還是以獲取資源為導向。後來隨著時代的發展,人們日益增長的文化需求成為了社會的主要矛盾……有一天,小明看了一篇報道,他只是想在下面評論一下,發表對實事的親切問候,問候完了,唉,你給我刷新頁面幹啥,我只是想評論一下啊。
大概那是網民們第一次對 良好的用戶體驗 提出了要求。後來的蘋果爸爸,把大家慣壞了,天天嚷著 "你這產品用戶體驗太差了"……
彼時,微軟還是對web做出了很大的貢獻的。
交互模式2.0
大約1999年,微軟發布IE 5.0
版本,它允許JavaScript腳本向伺服器發起HTTP請求。不過很遺憾,當時也沒有火起來,直到2004年Gmail發布和2005年Google Map發布,才引起廣泛重視。2005年,一個叫Jesse James Garrett的人提出了一個新術語----AJAX
,它是一系列技術的組合體,全稱是 Asynchronous JavaScript + XML
(非同步的JS和XML)可以阻止頁面整體刷新,只是動態響應用戶的操作,快速顯示到局部,用戶就可以很愉快的繼續上網了。
AJAX
可以看出IE當時還是很猛的,隨著IE 6.0 市場份額進一步擴大,IE已經把火狐整的半死不活,放眼整個瀏覽器市場,微軟是當之無愧的王者,後來微軟就把瀏覽器團隊解散了……不得不說這是一波神操作,能與之媲美的操作大概只有殘血我能反殺
塔下我能秀他
了。微軟強行為後續各家瀏覽器的發展提供了優秀的工程師,尤其是08、09年出生的谷歌瀏覽器,再看如今的IE……
既然AJAX
是一系列的技術的組合體,接下來認識一下其中的幾位主角
XMLHttpRequest
XMLHttpRequest
對象是用來在瀏覽器和伺服器之間傳輸數據的。
古代的操作的是:
- 瀏覽器構造
XMLHttpRequest
實例化對象 - 用這個對象發起請求
- 伺服器響應一個
XML
格式的字元串,是字元串,是字元串,是字元串,也就是說響應的第四部分是字元串。 - JS解析符合XML格式的字元串,更新局部頁面。
什麼是XML,可擴展標記語言。
以上是最初的用法,用的是XML
,前端代碼片段如下
let request = new XMLHttpRequest() //實例化XMLHttpRequest對象 request.onreadystatechange = () => { if (request.readyState === 4) { console.log(請求和響應都完畢了) if (request.status >= 200 && request.status <= 300) { console.log(說明請求成功了) console.log(request.responseText) let parser = new DOMParser() let xmlDoc = parser.parseFromString(request.responseText, "text/xml") //用parser解析request.responseText let c = xmlDoc.getElementsByTagName(body)[0].textContent console.log(c) } else if (request.status >= 400) { console.log(說明請求失敗了) } } } request.open(GET, /xxx) //配置request request.send()
伺服器端的對應代碼片段如下
... response.statusCode = 200 response.setHeader(Content-Type, text/xml;charset=utf-8) response.write(` <note> <to>木木</to> <from>少少</from> <heading>你好哇</heading> <body>好久不見啊</body> </note> `) response.end() ...
本地模擬的話,一定要記得開倆不同的埠
例如:
node server.js 8001node server.js 8002
XMLHttpRequest實例的詳解
正如上面的前端代碼片段寫的一樣,主要用到了open()
send()
方法, onreadystatechange
readyState
屬性。
1.request.open(method, URL, async)方法。
- 一般用三個參數,第一個參數是請求的方法,可以用
GET POST DELETE PUT
等等,URL是用訪問的路徑,async是是否使用同步,默認true,開啟非同步,不需要做修改即可,所以實際中只寫前兩個參數 - 如果非要寫false,開啟同步,會對瀏覽器有阻塞效應,而且如果值為false,則send()方法不會返回任何東西,直到接受到了伺服器的返回數據
2.request.send()方法。
- 發送請求. 如果該請求是非同步模式(默認),該方法會立刻返回. 相反,如果請求是同步模式,則直到請求的響應完全接受以後,該方法才會返回
3.readyState
屬性。
- 描述請求的五個狀態。
- 0 === 常量
UNSENT
(未打開) open()方法未調用 - 1 ===
OPENED
(未發送) 只是open()方法調用了 - 2 ===
HEADERS_RECEIVED (已獲取響應頭)
send()方法調用了,響應頭和響應狀態已經返回了 - 3 ===
LOADING (正在下載響應體)
響應體下載中,responseText
已經獲取了部分數據 - 4 ===
DONE (請求完成)
整個響應過程完畢了。 這個值是實際中用到的。 - 只要不等於4,就表示請求還在進行中。
4.responseText屬性是此次響應的文本內容。
5.onreadystatechange
屬性。
readyState
屬性的值發生改變,就會觸發readyStateChange
事件。- 我們可以通過
onReadyStateChange
屬性,指定這個事件的回調函數,對不同狀態進行不同處理。尤其是當狀態變為4的時候,表示通信成功,這時回調函數就可以處理伺服器傳送回來的數據。即前面的代碼片段的處理方式。
6.其他的方法、屬性、事件詳見阮一峰博客、MDN文檔
習慣用javaScript
的前端是不想和XML
打交道的,應該用一種符合js
風格的數據格式語言。
JSON
後來一個美國程序員道格拉斯·克羅克福特發明了JSON
,解決了上面的問題,這貨還寫了一本蝴蝶書JavaScript語言精粹,還發明了一個JS校驗器 ----JSLint。
JSON(JavaScript Object Notation) 是一種輕量級的數據交換格式。 易於人閱讀和編寫。同時也易於機器解析和生成。 它基於JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999的一個子集。 JSON採用完全獨立於語言的文本格式,但是也使用了類似於C語言家族的習慣(包括C, C++, C#, Java, JavaScript, Perl, Python等)。 這些特性使JSON成為理想的數據交換語言。
以上是JSON官網的簡介,可以看出它是一門全新的語言,不是JavaScript的子集。
JSON
很簡單,數據類型和JS有點不同的地方。
2. 瀏覽器的全局對象window
上有JSON
對象,直接使用window.JSON.parse(string)
let string = request.responseTextlet json = window.JSON.parse(string) //string 要符合JSON的格式
以上是JSON解析部分的代碼。
此時伺服器端代碼是
response.statusCode = 200response.setHeader(Content-Type, text/json;charset=utf-8)response.write(` { "note" : { "to" : "木木", "from" : "少少", "heading" : "你好哇", "content" : "好久不見啊" } }`)
3.我們瀏覽器有同源政策,不是同協議 同域名 同埠 的網頁無法相互訪問。
4.AJAX
恰好是同源政策的擁躉
CORS
- 如果
AJAX
向非同源的地址發起請求,會報錯。
- 這種錯誤無法通過狀態碼識別,因為HTTP回應的狀態碼有可能是200,也就是說即使你看到了200的正確碼,也沒有用
2. 但是form表單無視同源政策,可以發起跨域請求。
<button id="myButton">點我</button><form action="https://www.baidu.com" method="get"> <input type="password" name="password"> <input type="submit" value="提交"> </form>
上述請求響應都沒有問題
然而對於AJAX
就不行...request.open(GET, http://www.baidu.com)...
- 這是為什麼呢,因為
原頁面用 form 提交到另一個域名之後,原頁面的腳本無法獲取新頁面中的內容,所以瀏覽器認為這是安全的。
而 AJAX 是可以讀取響應內容的,因此瀏覽器不能允許你這樣做。如果你細心的話你會發現,其實請求已經發送出去了,你只是拿不到響應而已。所以瀏覽器這個策略的本質是,一個域名的 JS ,在未經允許的情況下,不得讀取另一個域名的內容。但瀏覽器並不阻止你向另一個域名發送請求。作者:方應杭鏈接:https://www.zhihu.com/question/31592553/answer/190789780來源:知乎著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
那麼如何讓AJAX
跨域發起請求呢。
CORS
3.CORS
目前是W3C的標準,它允許瀏覽器跨域發起XMLHttpRequest
請求,而且可以發起多種請求,不像JSONP
只能發起GET
請求,全稱是"跨域/源資源共享"(Cross-origin resource sharing)。
- 如果想要發起跨域請求 例如: http://wushao.com:8001 要想訪問 http://shaolin.com:8002,可以做如下處理
request.open(GET, http://wushao.com:8001/xxx) //配置request
- 伺服器端的代碼需要做如下處理
response.setHeader(Access-Control-Allow-Origin, http://shaolin.com:8002)
一定要注意是誰去訪問誰,8001去訪問8002,那麼8001的前端代碼要告訴8002的後端代碼,咱們是一家人,你和瀏覽器說說別讓它禁我了。
AJAX一些其他知識
既然可以發請求,那麼請求頭的四部分如何獲得的,響應的四部分又是如何獲得呢
獲得請求和響應頭
- 獲得請求頭的方法
request.open(GET, http://shaolin.com:8002/xxx)// 請求的第一部分request.setRequestHeader(Content-Type, x-www-form-urlencoded)//請求的第二部分request.setRequestHeader(wushao, 18) //請求的第二部分request.send(我要設置請求的第四部分) //請求的第四部分request.send(name=wushao&password=wushao) //請求的第四部分
對應的典型的http請求四部分
GET /xxx HTTP/1.1HOST: http://shaolin.com:8002Content-Type: x-www-form-urlencodedwushao: 18name=wushao&password=wushao
2.獲得響應的方法
request.status //響應的第一部分 200request.statusText //響應的第一部分 OKrequest.getAllResponseHeaders //響應的第二部分,這個方法好啊,全部的響應頭request.getResponseHeader(Content-Type) //響應的第二部分具體的request.responseText //響應的第四部分
對應的典型的http響應的四部分
HTTP/1.1 200 OKContent-Type: text/json;charset=utf-8{ "note" : { "to" : "木木", "from" : "少少", "heading" : "你好哇", "content" : "好久不見啊" } }
回顧一下各個status對應的意思
100200 === OK,請求成功301 === 被請求的資源已永久移動到新位置302 === 請求臨時重定向,要求客戶端執行臨時重定向304 === 和上次請求一樣,未改變403 === 伺服器已經理解請求,但是拒絕訪問404 === 請求失敗,伺服器上沒有這個資源502 === 作為網關或者代理工作的伺服器嘗試執行請求時,從上游伺服器接收到無效的響應。503 === Service Unavailable 由於臨時的伺服器維護或者過載,伺服器當前無法處理請求。
練習一下JQuery封裝AJAX
- 初級的jq封裝這是一個很簡陋的效果,首先我還是把jq假設的很簡單,就是一個window的屬性,請輕噴……
window.jQuery = function (nodeOrSelector) { let nodes = {} nodes.addClass = function () {} nodes.html = function () {} return nodes}window.jQuery.ajax = function (options) { let url = options.url let method = options.method let headers = options.headers let body = options.body let successFn = options.successFn let failFn = options.failFn let request = new XMLHttpRequest() //實例化XMLHttpRequest對象 request.open(method, url) for (let key in headers) { let value = headers[key] request.setRequestHeader(key, value) } request.onreadystatechange = () => { if (request.readyState === 4) { if (request.status >= 200 && request.status <= 300) { successFn.call(undefined, request.responseText) } else if (request.status >= 400) { failFn.call(undefined, request) } } } request.send(body)}
以上就是jq對ajax的簡陋的封裝,ajax()方法接受一個對象作為參數,這個對象有很多鍵。這些鍵就是http請求的頭的各個部分,以及一個成功函數和一個失敗函數。
myButton.addEventListener(click, (e) => { window.jQuery.ajax ({ url: /xxx, method: POST, headers: { content-type: application/x-www-form-urlencoded, wushao: 18 }, body: a=1&b=6, successFn: (x) => { ... }, failFn: (x) => { ... } }) })
以上就是簡化後的使用方法,給button綁定事件的時候,函數體直接就是ajax()
2.目前你會發現options這個對象傻傻的,因為總有一些用戶不希望只傳一個參數。所以我們稍微改造一下。
let url if (arguments.length === 1) { url = options.url } else if (arguments.length === 2) { url = arguments[0] options = arguments[1] } let method = options.method let headers = options.headers let body = options.body let successFn = options.successFn let failFn = options.failFn
加了一點,判斷ajax()的參數個數。
3.一千個人有一千零一個成功或失敗函數的寫法,所以為了維護世界和平,大家約定俗成了一套理論 Promise then( )
//Promise這個對象呢,大概長這個樣子,真實面目我是沒見過 //簡單的寫一下promise window.Promise = function (fn) {//...一些其他代碼 return { then: function () {} }}
Promise這個構造函數呢,又會返回一個函數,這個返回的函數一個then屬性,value又是一個函數。處處都體現著函數是第一公民的地位!!!
那我們可以利用這個強大的Promise對象搞一些事情了。
//第一步的代碼改造成這樣,第一步用到了ES6的解構賦值法 window.jQuery.ajax = function ({url, method, body, headers}) { return new Promise(function (resolve, reject) { let request = new XMLHttpRequest() request.open(method, url) for(let key in headers) { let value = headers[key] request.setRequestHeader(key, value) } request.onreadystatechange = () => { if (request.readyState === 4) { if (request.status >= 200 && request.status <= 300) { resolve.call(undefined, request.responseText) } else if (request.status >= 400) { reject.call(undefined, request) } } } request.send(body) })}
關於解構賦值:ES6 允許按照一定模式,從數組和對象中提取值,對變數進行賦值,這被稱為解構(Destructuring)
詳見ES6解構賦值//經過上面這麼一折騰,可以很簡單的使用了myButton.addEventListener(click, (e) => { let promise = window.jQuery.ajax({ url: /xxx, method: get, headers: { content-type: application/x-www-form-urlencoded, wushao: 18 } }) promise.then( (responseText) => { console.log(responseText) }, (request) => { console.log(request) } ) })
注意then可以傳入兩個函數,第一個函數表示成功了執行這個,第二個函數表示失敗了執行這個,而且可以進行鏈式調用,一直點下去。
4.所以實際上jq的寫法大多是這麼寫的
myButton.addEventListener(click, (e) => { $.ajax({ url: /xxx, type: GET, }).then( (responseText) => { console.log(responseText) return responseText }, (request) => { console.log(error) return 已經處理 } ).then( (responseText) => { console.log(responseText) }, (request) => { console.log(error2) } )})
鏈式調用的意思就是:成功函數成功了,就執行第二個then的第一個函數;成功函數失敗了,就執行第二個then的第二個函數。
完整代碼詳見我的gitHub
推薦閱讀:
※從process.versions了解Node.js的構成
※2017新出爐的前端資源匯總
※前端頁面熱更新實現方案
※手把手教你用 SVG 符號和 CSS 變數做出彩色圖標
※序章 曦粥簡言