2017年第一篇博客,從去年11月份開始到現(xiàn)在已經(jīng)幾個(gè)月沒有動(dòng)筆了威根。寫這篇博客的契機(jī)是瓶竭,有個(gè)哥們?cè)谧约旱牟┛拖到y(tǒng)上想加載個(gè)人在豆瓣上的讀書信息。而且
1. 不想使用ruby來做這個(gè)事情(后端請(qǐng)求)挠他,因?yàn)樗X得拿到信息后放到后臺(tái)處理,最后再丟到前端去渲染的方式很挫扳抽。
2. 如果在瀏覽器端使用js來fetch對(duì)應(yīng)數(shù)據(jù)的話會(huì)產(chǎn)生跨域問題。
OK殖侵,容我從下面幾個(gè)方面來寫寫近期對(duì)跨域問題的理解
1. 怎樣才算跨域?
2. 我們要如何發(fā)送跨域請(qǐng)求?
3. 如何跨域調(diào)用豆瓣的api來獲取對(duì)應(yīng)的圖書數(shù)據(jù)?
一. 怎樣才算跨域
跨域贸呢,其實(shí)簡(jiǎn)單地去理解就是不同域名之間的http請(qǐng)求。比如我朋友的豆瓣api是 https://api.douban.com/v2/book/user/119280372/collections
拢军,我需要從瀏覽器通過js來獲取這個(gè)url返回的數(shù)據(jù)楞陷,我使用比較傳統(tǒng)的做法是用XMLHttpRequest
創(chuàng)建一個(gè)對(duì)象來做這個(gè)事情。
var req = new XMLHttpRequest()
req.open('GET', 'https://api.douban.com/v2/book/user/119280372/collections')
req.send(null)
好像很合理茉唉,但是如果我是在百度的首頁做這個(gè)事情固蛾,瀏覽器會(huì)給我這個(gè)反饋
XMLHttpRequest cannot load https://api.douban.com/v2/book/user/119280372/collections. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://www.baidu.com' is therefore not allowed access.
表明這是一個(gè)跨域請(qǐng)求结执。犀牛書里面有個(gè)比較簡(jiǎn)單的判斷請(qǐng)求是否跨域的方法就是同時(shí)判斷需要訪問的url的域名
以及端口號(hào)
,如果 (域名不同
|| 端口號(hào)不同
) 為邏輯true
, 則表示對(duì)這個(gè)url的請(qǐng)求是一個(gè)跨域請(qǐng)求艾凯∠揍#看上面的例子,首先域名就不一樣了趾诗,馬上就能夠判斷這是一個(gè)跨域請(qǐng)求了蜡感。
二. 如何發(fā)送跨域請(qǐng)求
這個(gè)時(shí)候我們會(huì)有疑問,我們<img>
這個(gè)標(biāo)簽不是可以獲取到其他域名下的圖片數(shù)據(jù)嗎恃泪?非常正確
郑兴。我們可以給<img>
標(biāo)簽設(shè)置src
屬性為https://unsplash.it/1000/150
來獲取這個(gè)圖片服務(wù)網(wǎng)站上面的圖片數(shù)據(jù),從程序員的角度去理解的話贝乎,它發(fā)送了一個(gè)GET
請(qǐng)求杈笔,但是它的局限性就在于,它就只能發(fā)送這個(gè)GET請(qǐng)求了糕非,而且我們似乎不能做更多的事情了。
下面介紹兩種方式
1. XMLHttpRequest
有些瀏覽器本身就支持XMLHttpRequest
所產(chǎn)生對(duì)象的跨域請(qǐng)求球榆,我們還可以自定義需要的請(qǐng)求方法(GET, POST)朽肥, 一般情況下是通過判斷該對(duì)象是否具有withCredentials
這個(gè)屬性來判斷的。
var supprotCORS = (new XMLHttpRequest()).withCredentials !== undefined
>> undefined
supprotCORS
>> true
這樣就表示我這個(gè)瀏覽器的XMLHttpRequest
所產(chǎn)生的對(duì)象是支持跨域請(qǐng)求的持钉。但是問題來了衡招,為什么剛才我用它來請(qǐng)求豆瓣的api的時(shí)候還會(huì)有跨域的錯(cuò)?
因?yàn)榭缬蛘?qǐng)求還需要服務(wù)端支持,產(chǎn)生跨域請(qǐng)求的時(shí)候還是要選擇值得信賴的
合作伙伴
每强。
舉個(gè)離我們最近的例子始腾。有些CDN網(wǎng)絡(luò)它本身服務(wù)就是支持跨域請(qǐng)求的。我們可以做個(gè)實(shí)驗(yàn)空执。我們使用https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css
這個(gè)url來請(qǐng)求css資源浪箭。當(dāng)然我們要發(fā)送的是跨域請(qǐng)求:
這里表示這個(gè)請(qǐng)求已經(jīng)成功了,并且沒有產(chǎn)生跨域問題辨绊,因?yàn)檫@個(gè)服務(wù)本身支持跨域請(qǐng)求的奶栖,具體怎么支持,這個(gè)要服務(wù)端的小伙伴自己摸索了门坷。
2. JSONP
我們還是沒有解決豆瓣上的跨域問題宣鄙。之前同學(xué)解決的方式是使用下面代碼
$.ajax({
type : "get", //jquey是不支持post方式跨域的
async: false,
url : "https://api.douban.com/v2/book/user/119280372/collections", //跨域請(qǐng)求的URL
dataType : "jsonp",
//傳遞給請(qǐng)求處理程序,用以獲得jsonp回調(diào)函數(shù)名的參數(shù)名(默認(rèn)為:callback)
jsonp: "callback",
//自定義的jsonp回調(diào)函數(shù)名稱默蚌,默認(rèn)為jQuery自動(dòng)生成的隨機(jī)函數(shù)名
jsonpCallback:"success_jsonpCallback",
//成功獲取跨域服務(wù)器上的json數(shù)據(jù)后,會(huì)動(dòng)態(tài)執(zhí)行這個(gè)callback函數(shù)
success : function(json){
}
});
但是我表示我完全不知道它在說什么冻晤,jsonp是什么?直到最近看犀牛書剛好碰到這個(gè)跨域相關(guān)的內(nèi)容绸吸。才稍微有點(diǎn)理解鼻弧。
JSONP是用<script>
元素作為Ajax傳輸?shù)募夹g(shù)设江。
咋一看,跟<img>
標(biāo)簽是不是有點(diǎn)兒像温数,他們都是可以通過設(shè)置src
屬性對(duì)應(yīng)的url來從服務(wù)端獲取數(shù)據(jù)绣硝,而且他們本身是允許跨域的。但是他們卻只能單向地從服務(wù)端獲取數(shù)據(jù)撑刺。這就可以理解了為什么上述的jquery代碼里面寫著jsonp只支持跨域GET
請(qǐng)求了吧(<script>
標(biāo)簽其實(shí)是無法發(fā)送POST這類修改服務(wù)端數(shù)據(jù)的請(qǐng)求的)鹉胖。然后,我們?cè)倩仡櫼幌?code><script>標(biāo)簽是怎么工作的:
1. 從URL獲取對(duì)應(yīng)的js腳本。
2. 瀏覽器運(yùn)行對(duì)應(yīng)腳本够傍。
這樣看來似乎我們就可以通過甫菠,把對(duì)應(yīng)的豆瓣url放入到script標(biāo)簽的src屬性里面。然后冕屯,添加這個(gè)標(biāo)簽到文檔里面寂诱,它就會(huì)自動(dòng)請(qǐng)求url并且獲取對(duì)應(yīng)的數(shù)據(jù)。
不過這里也是有問題的:
我們獲取服務(wù)端返回的數(shù)據(jù)之后安聘,瀏覽器會(huì)把對(duì)應(yīng)的數(shù)據(jù)當(dāng)作js執(zhí)行痰洒,萬一我們獲取的數(shù)據(jù)不是js腳本,怎么辦? 我們看下面的例子:
var script = document.createElement('script'); // 創(chuàng)建一個(gè)script標(biāo)簽
script.setAttribute('src': 'https://api.douban.com/v2/book/user/119280372/collections'); // 設(shè)置script標(biāo)簽的src屬性為對(duì)應(yīng)的url
document.body.appendChild(script); // 把script標(biāo)簽插入到body元素的最后浴韭,插入之后就會(huì)直接發(fā)起跨域請(qǐng)求丘喻。
我在瀏覽器端運(yùn)行上述腳本,但是很不幸:
原因上面也說了念颈,其實(shí)豆瓣返回的是json數(shù)據(jù)泉粉,我們script標(biāo)簽加載之后把它當(dāng)作js來運(yùn)行了,所以報(bào)錯(cuò)是可以理解的榴芳。
接下來咋辦, 當(dāng)服務(wù)端返回的是json
數(shù)據(jù)的時(shí)候嗡靡,如果我們能夠把json數(shù)據(jù)放進(jìn)函數(shù)里面進(jìn)行處理就好了。
當(dāng)我們獲得的數(shù)據(jù)是 {name: "lanzhiheng"}
的時(shí)候窟感,我多么希望我能夠通過:
callback({name: "lanzhiheng"})
來對(duì)返回的數(shù)據(jù)進(jìn)行處理啊!!!
我們其實(shí)可以告訴服務(wù)器讓它給我們返回一個(gè)JSONP響應(yīng)讨彼,而不單單是JSON數(shù)據(jù),常用的方式是在url后面添加一個(gè)?jsonp=callback
這樣的查詢字符串,形如
https://api.douban.com/v2/book/user/119280372/collections?jsonp=callback
然后我們返回的數(shù)據(jù)就會(huì)有個(gè)名為callback
的函數(shù)包裹著柿祈。當(dāng)然想得美
! 這個(gè)東西既然是服務(wù)端支持点骑,當(dāng)然不會(huì)寫死。除了jsonp還可能會(huì)有其他的參數(shù)名字谍夭,對(duì)于豆瓣而言黑滴,它是用callback來作為參數(shù)名的,為了避免混淆我把url改成下面這樣
https://api.douban.com/v2/book/user/119280372/collections?callback=handleResponse
嘗試在瀏覽器調(diào)用這個(gè)方法紧索。
Awesome袁辈,我們返回的json數(shù)據(jù)已經(jīng)被一個(gè)handleResponse
函數(shù)包裹著,我們只需要提前對(duì)handleResponse進(jìn)行定義珠漂,就可以對(duì)這堆json數(shù)據(jù)為所欲為了晚缩∥膊玻回顧上面的jquery代碼,也就不難理解jsonp
參數(shù)名對(duì)應(yīng)的值為何是callback
了吧?
三. 如何跨域調(diào)用豆瓣的api來獲取對(duì)應(yīng)的圖書數(shù)據(jù)
其實(shí)在介紹jsonp的時(shí)候已經(jīng)基本上把核心代碼都寫出來了荞彼,最為核心的代碼其實(shí)就是
var script = document.createElement('script'); // 創(chuàng)建一個(gè)script標(biāo)簽
script.setAttribute('src': 'https://api.douban.com/v2/book/user/119280372/collections'); // 設(shè)置script標(biāo)簽的src屬性為對(duì)應(yīng)的url
document.body.appendChild(script); // 把script標(biāo)簽插入到body元素的最后冈敛,插入之后就會(huì)直接發(fā)起跨域請(qǐng)求。
我們可以稍微把代碼寫得好一點(diǎn)鸣皂,定義一個(gè)函數(shù)給他自動(dòng)追加查詢字符串抓谴。(犀牛書里面的對(duì)應(yīng)例子精簡(jiǎn)版本)
var responseHandler; // 定義一個(gè)全局作用域的函數(shù)
function getJSONP(url, cb) {
if (url.indexOf('?') === -1) {
url += '?callback=responseHandler';
} else {
url += '&callback=responseHandler';
}
// 創(chuàng)建script 標(biāo)簽
var script = document.createElement('script');
// 在函數(shù)內(nèi)部實(shí)現(xiàn)包裹函數(shù),因?yàn)橐玫絚b
responseHandler = function(json) {
try {
cb(json)
} finally {
// 函數(shù)調(diào)用之后不管發(fā)生什么都要移除對(duì)應(yīng)的標(biāo)簽寞缝,留著也沒用
script.parentNode.removeChild(script);
}
}
script.setAttribute('src', url)
document.body.appendChild(script);
}
OK癌压,現(xiàn)在測(cè)試一下這個(gè)函數(shù),我們需要傳入一個(gè)對(duì)返回?cái)?shù)據(jù)進(jìn)行處理的函數(shù)荆陆。我這里簡(jiǎn)單地把數(shù)據(jù)打印出來
很好, 結(jié)果就是我們需要的滩届,我們已經(jīng)可以把跨域請(qǐng)求所返回的JSON數(shù)據(jù)打印出來了,當(dāng)然后續(xù)是否需要進(jìn)行更多的操作這就取決于你了!