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):
瀏覽器出于安全方面的考慮,只允許與本域下的接口交互。不同源的客戶端腳本在沒(méi)有明確授權(quán)的情況下,不能讀寫(xiě)對(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)聽(tīng)一個(gè)端口,監(jiān)聽(tīng)多個(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é)果呢?
答案幾乎是顯而易見(jiàn)的,就像非跨域請(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í)候,是直接提前寫(xiě)在HTML文檔里的,而我們拿到數(shù)據(jù)處理完后(js執(zhí)行完畢后),這個(gè)標(biāo)簽就沒(méi)有用處了,而且實(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,開(kāi)啟本地服務(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ì)于開(kāi)發(fā)者來(lái)說(shuō),CORS通信與同源的AJAX通信沒(méi)有差別,實(shí)現(xiàn)CORS通信的關(guān)鍵在與服務(wù)器支持與否,只要服務(wù)器實(shí)現(xiàn)了CORS接口,就可以跨域通信。
還是上一個(gè)實(shí)例,服務(wù)器端開(kāi)啟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ù),任意打開(kāi)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)聽(tīng)接受這個(gè)數(shù)據(jù),那么就沒(méi)有任何效果。
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)聽(tīng)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)聽(tīng)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)很好玩! 可以做很多有趣的事情,唯一的局限是自身水平和想象力的局限。就在剛剛寫(xiě)這篇拙文的時(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