1. 跨域和同源
首先來(lái)看摘自MDN上對(duì)于跨域缸榛,較為標(biāo)準(zhǔn)的解釋:
當(dāng)一個(gè)資源從與該資源本身所在的服務(wù)器不同的域或端口請(qǐng)求一個(gè)資源時(shí)俐银,資源會(huì)發(fā)起一個(gè)跨域 HTTP 請(qǐng)求懈涛。
比如蔽莱,站點(diǎn) http://domain-a.com 的某 HTML 頁(yè)面通過(guò) <img> 的 src 請(qǐng)求 http://domain-b.com/image.jpg弟疆。網(wǎng)絡(luò)上的許多頁(yè)面都會(huì)加載來(lái)自不同域的CSS樣式表,圖像和腳本等資源碾褂。
出于安全考慮兽间,瀏覽器會(huì)限制從腳本內(nèi)發(fā)起的跨域HTTP請(qǐng)求。例如正塌,XMLHttpRequest 和 Fetch 遵循同源策略。因此恤溶,使用 XMLHttpRequest或 Fetch 的Web應(yīng)用程序如果不使用跨域技術(shù)乓诽,只能將HTTP請(qǐng)求發(fā)送到其自己的域。
同源策略(same-origin policy):
瀏覽器出于安全方面的考慮咒程,只允許與本域下的接口交互鸠天。不同源的客戶端腳本在沒有明確授權(quán)的情況下,不能讀寫對(duì)方的資源帐姻,防止惡意的網(wǎng)站竊取數(shù)據(jù)稠集、cookie等。
但不一定是瀏覽器限制了發(fā)起跨站請(qǐng)求饥瓷,也可能是跨站請(qǐng)求可以正常發(fā)起剥纷,但是返回結(jié)果被瀏覽器攔截了。
比如我現(xiàn)在用的Chrome/61.0.3163.91呢铆,雖然有同源策略的存在晦鞋,但是在調(diào)試工具的Network下,Status Code 200 OK, 說(shuō)明數(shù)據(jù)是返回回來(lái)了, 并且可以在Preview 或者 Response里看到數(shù)據(jù)悠垛。
什么才是同源:
字符串完全匹配才是同源线定,協(xié)議不同 域名不同(子域名和主域名并不是同源)端口不同,都不算是同源确买。
本地調(diào)試時(shí):
一個(gè)http-server服務(wù)器只能監(jiān)聽一個(gè)端口斤讥,監(jiān)聽多個(gè)可以設(shè)置不同端口,比如:
http-server -c-1 -p 80
http-server -c-1 -p 81
是跨域還是本域同源湾趾,看2個(gè)點(diǎn):
- 發(fā)送AJAX 請(qǐng)求的當(dāng)前頁(yè)面 URL 是什么
- AJAX 請(qǐng)求的 URL 是什么
這兩個(gè) URL 同源周偎,則不是跨域。
此外撑帖, <iframe> 標(biāo)簽蓉坎,也是受同源策略限制的。
2. 跨域的幾種方式
- JSONP
JSONP是服務(wù)器與客戶端跨源通信的常用方法胡嘿。最大特點(diǎn)就是簡(jiǎn)單適用蛉艾,老式瀏覽器全部支持,服務(wù)器改造非常小衷敌。
html中 <script> 標(biāo)簽可以引入其他域下的js勿侯,比如引入線上的jquery庫(kù)。利用這個(gè)特性缴罗,可實(shí)現(xiàn)跨域訪問(wèn)接口, 但是需要后端支持助琐。
首先引入標(biāo)簽,參數(shù)中指定回調(diào)函數(shù)名:
<script src="http://weather.com.cn?city=hefei&callback=showWeather">
請(qǐng)求到的數(shù)據(jù)面氓, 由于受到后端支持兵钮,所以類似如下結(jié)構(gòu),后端把原始數(shù)據(jù)放在要執(zhí)行的函數(shù)的參數(shù)里面返回給前端:
showWeather({
"city": "hefei",
weatheer: {xxx}
})
當(dāng)數(shù)據(jù)返回到了客戶端舌界,由于是<script>標(biāo)簽掘譬,所以會(huì)自動(dòng)當(dāng)作js代碼執(zhí)行。
但是呻拌,我們雖然引入script標(biāo)簽時(shí)葱轩,在URL的中指定了callback的函數(shù)名,以便后端能用正確的函數(shù)名去‘包裝’藐握,可是這個(gè)傳入了原始數(shù)據(jù)的函數(shù)在我們的瀏覽器中運(yùn)行起來(lái)靴拱,到底要得到什么結(jié)果呢?
答案幾乎是顯而易見的猾普,就像非跨域請(qǐng)求一樣袜炕,我們需要對(duì)數(shù)據(jù)進(jìn)行處理,比如解析數(shù)據(jù)抬闷,進(jìn)行html的拼接妇蛀,并最終展示到頁(yè)面上耕突。
所以我們要聲明并完善這個(gè)callback函數(shù):
function showWeather(json){
// do something
}
以上有個(gè)問(wèn)題,就是在引入script標(biāo)簽的時(shí)候评架,是直接提前寫在HTML文檔里的眷茁,而我們拿到數(shù)據(jù)處理完后(js執(zhí)行完畢后),這個(gè)標(biāo)簽就沒有用處了纵诞,而且實(shí)際場(chǎng)景中有多個(gè)類似請(qǐng)求的情況時(shí)上祈,如何才能保持HTML的干凈利索呢?
可以考慮下面的方式浙芙,使用js創(chuàng)建script標(biāo)簽登刺,引用到資源后,這里由于是立即并且逐條執(zhí)行的嗡呼,所以看似下一句立刻刪除了標(biāo)簽纸俭,但實(shí)際上中間已經(jīng)執(zhí)行過(guò)了我們起初定義的函數(shù)。
document.querySelector('.change').addEventListener('click', function(){
var script = document.createElement('script')
script.src = 'http://127.0.0.1:8080/getNews?callback=appendHtml';
document.head.appendChild(script)
document.head.removeChild(script) //請(qǐng)求數(shù)據(jù)南窗,執(zhí)行完畢后揍很,就立即刪除script的引入標(biāo)簽
})
而后端之前也提到了,只需要在同源請(qǐng)求中万伤,增加判斷語(yǔ)句窒悔,如果有callback參數(shù),則返回使用函數(shù)‘包裝’后的數(shù)據(jù):
var cb = req.query.callback
if(cb){
res.send(cb + '(' + JSON.stringify(data) + ')')
}else{
res.send(data)
}
敌买!注意演示中简珠,使用了 server-mock 工具,使用隨同NodeJS一起安裝的包管理工具NPM進(jìn)行server-mock的安裝虹钮,然后把index.html 和router.js 放在一個(gè)文件夾聋庵,接著終端里進(jìn)入當(dāng)前文件夾, 使用 mock start芜抒,開啟本地服務(wù)器即可珍策。
- CORS(Cross-origin resource sharing) 跨域
CORS 也叫跨域資源共享,它是W3C標(biāo)準(zhǔn)宅倒,是跨源AJAX請(qǐng)求的根本解決方法,克服了AJAX只能同源使用的限制屯耸。相比JSONP只能發(fā)GET請(qǐng)求拐迁,CORS允許任何類型的請(qǐng)求,可以說(shuō)是老式JSONP的現(xiàn)代升級(jí)版疗绣。
目前线召,除了 IE瀏覽器IE10以下外,所有瀏覽器都支持該功能多矮。
對(duì)于開發(fā)者來(lái)說(shuō)缓淹,CORS通信與同源的AJAX通信沒有差別哈打,實(shí)現(xiàn)CORS通信的關(guān)鍵在與服務(wù)器支持與否,只要服務(wù)器實(shí)現(xiàn)了CORS接口讯壶,就可以跨域通信料仗。
還是上一個(gè)實(shí)例,服務(wù)器端開啟CORS的方法是伏蚊,在響應(yīng)頭信息中添加 Access-Control-Allow-Origin:
res.header('Access-Control-Allow-Origin', 'http://wangpeng.com:8080')
//res.header('Access-Control-Allow-Origin', '*')
第二個(gè)參數(shù)用來(lái)聲明哪些源站有權(quán)限訪問(wèn)哪些資源立轧。
星號(hào) *代表來(lái)自任意域名的請(qǐng)求,都不會(huì)受到瀏覽器同源策略限制躏吊。
- 降域?qū)崿F(xiàn)跨域
和上面都是請(qǐng)求資源的場(chǎng)景不同氛改。對(duì)于兩個(gè)不同頁(yè)面的腳本,只有當(dāng)執(zhí)行它們的頁(yè)面位于具有相同的協(xié)議比伏,端口號(hào)胜卤,以及主機(jī)(document.domain 也就是原始域名----origin domain 設(shè)置為相同的值,也就是同時(shí)降域成一致)時(shí)赁项,這兩個(gè)腳本才能相互通信葛躏。
降域主要場(chǎng)景是, a.xxx.com 和 b.xxx.com 之間的訪問(wèn)肤舞,雖然都是xxx.com 子域名紫新,但是也存在瀏覽器同源策略的限制, 也無(wú)法直接獲取對(duì)方信息李剖。此時(shí)芒率,使用降域即可解決。
方法是: a.xxx.com 和 b.xxx.com 的頁(yè)面JS中篙顺, 同時(shí)加入 document.domain = "xxx.com" 彼此都降域到主域名偶芍。
于是二者可以在各自的頁(yè)面中,使用<iframe>
引入另一個(gè)域名下同時(shí)做了降域的頁(yè)面德玫,并可以進(jìn)行相互操作匪蟀。
但是,如果不是一個(gè)主域名下的兩個(gè)二級(jí)域名宰僧,那么是不可能降域到一樣的材彪, 比如 a.baidu.com 和 b.taobao.com, 降域后分別是 baidu.com 和 taobao.com ,這顯然不是同源。
使用server-mock工具 或者h(yuǎn)ttp-server 搭建本地服務(wù)琴儿,任意打開a頁(yè)面段化,兩個(gè)input中任意一個(gè)輸入value值,另一個(gè)input會(huì)隨之改變造成,說(shuō)明實(shí)現(xiàn)了跨域操作显熏。
- postMessage
上例中通過(guò)降域,向其他窗口比如 iframe 晒屎、執(zhí)行window.open返回的窗口對(duì)象等發(fā)送數(shù)據(jù)喘蟆,實(shí)現(xiàn)兩個(gè)不同頁(yè)面的腳本跨域通信缓升,是存在局限性的,因?yàn)榻涤蛞惨从蛎麠l件蕴轨。
HTML5為了解決這個(gè)問(wèn)題港谊,引入了一個(gè)全新的API:跨文檔通信 API(Cross-document messaging)。
這個(gè)API為window對(duì)象新增了一個(gè)window.postMessage方法尺棋,允許跨窗口通信封锉,不論這兩個(gè)窗口是否同源。
舉例來(lái)說(shuō)膘螟,父窗口http://aaa.com向子窗口http://bbb.com發(fā)消息成福,調(diào)用postMessage方法就可以了。
對(duì)于不同的域下荆残,可以向其發(fā)送數(shù)據(jù)奴艾,如果對(duì)方認(rèn)可接受這個(gè)數(shù)據(jù),那么就可以使用内斯,如果對(duì)方?jīng)]有監(jiān)聽接受這個(gè)數(shù)據(jù)蕴潦,那么就沒有任何效果。
postMessage方法的第一個(gè)參數(shù)是具體的信息內(nèi)容俘闯,第二個(gè)參數(shù)是接收消息的窗口的源(origin)潭苞,即"協(xié)議 + 域名 + 端口"。也可以設(shè)為*真朗,表示不限制域名此疹,向所有窗口發(fā)送。
還是上一個(gè)應(yīng)用了降域的例子遮婶,這一次我們換用postMessage方法蝗碎。
a頁(yè)面:
//發(fā)送
document.querySelector('.main input').addEventListener('input', function(){
console.log(this.value)
//把輸入框的值發(fā)給兒子iframe,第二個(gè)參數(shù)指定哪些窗口能接收到消息事件旗扑,其值可以是字符串"*"(表示無(wú)限制)或者一個(gè)URI
window.frames[0].postMessage(this.value, '*')
})
//監(jiān)聽iframe的消息
window.addEventListener('message', function(e){
document.querySelector('.main input').value = e.data
console.log(e.data)
})
//關(guān)于postMessage 的使用蹦骑,MDN文檔有詳細(xì)描述,已經(jīng)更規(guī)范更安全的建議臀防,本文只是做跨域的簡(jiǎn)單探討眠菇,簡(jiǎn)化了很多細(xì)節(jié)。
b頁(yè)面:
//發(fā)送
document.querySelector('#input').addEventListener('input', function(){
window.parent.postMessage(this.value, '*') //把輸入框的值發(fā)給parent
})
//監(jiān)聽parent的消息
window.addEventListener('message', function(e){
document.querySelector('#input').value = e.data
console.log(e.data)
})
關(guān)于跨域問(wèn)題袱衷,MDN文檔有詳細(xì)使用描述琼锋,以及更規(guī)范更安全的建議,
另外祟昭,阮老師的這兩篇博文也做了詳盡的闡述。
阮一峰-瀏覽器同源政策及其規(guī)避方法
阮一峰-跨域資源共享 CORS 詳解
P.S. 最近學(xué)習(xí)了AJAX和跨域怖侦,剛好試著調(diào)用下API篡悟,發(fā)現(xiàn)很好玩! 可以做很多有趣的事情谜叹,唯一的局限是自身水平和想象力的局限。就在剛剛寫這篇拙文的時(shí)候搬葬,就發(fā)現(xiàn)放在github上的音樂(lè)頁(yè)面荷腊,不能請(qǐng)求到資源,說(shuō)我請(qǐng)求的mixed content(混合內(nèi)容急凰,確切說(shuō)是音頻資源) 被block掉了女仰。google一下,才恍然想起來(lái)抡锈,github pages 是 https協(xié)議的疾忍。趕緊跑去換了個(gè)https協(xié)議的API,搞定床三。
如有任何想法或疑惑一罩,歡迎評(píng)論區(qū)提出,我們一起探討:D