這篇文章我們來(lái)聊聊「跨域」,這個(gè)概念在很多公司的面試中會(huì)被問(wèn)到,網(wǎng)上也有數(shù)不清的關(guān)于「跨域」的文章。
那我為什么還要費(fèi)神去寫(xiě)一篇?
因?yàn)槲野l(fā)現(xiàn)很多文章都是互相抄襲,寫(xiě)了一大堆你也沒(méi)有搞懂他到底在講什么,基本上技術(shù)文章都有這個(gè)通病,不知道是作者覺(jué)很多知識(shí)是不言自明的,所以不屑于講;還是說(shuō)其實(shí)他自己也沒(méi)搞清楚,把別人的文章搬過(guò)來(lái),隨便改改,就成自己的了。
廢話(huà)不多說(shuō)了,下面進(jìn)入正題。
往往我們要解釋清楚一個(gè)概念,必須引入另一個(gè)概念,「跨域」也是如此。
要搞清楚「跨域」,先要搞清楚什么是「同源策略」(Same-origin policy),為了搞清楚「同源策略」,我們先講一些其他的東西。
我們寫(xiě)任何程序,本質(zhì)上只做兩件事件:計(jì)算數(shù)據(jù)和存儲(chǔ)數(shù)據(jù),CPU 負(fù)責(zé)計(jì)算,內(nèi)存負(fù)責(zé)存儲(chǔ)。計(jì)算機(jī)之所以牛逼,不是因?yàn)樗茏龅氖虑槎?,而是因?yàn)樗?jì)算的速度相當(dāng)快,能存儲(chǔ)的數(shù)據(jù)非常多。
既然計(jì)算和存儲(chǔ)都由計(jì)算機(jī)搞定了,那數(shù)據(jù)呢,數(shù)據(jù)從哪來(lái)呢?
數(shù)據(jù)的來(lái)源有很多種,比如你可以在程序里自己造數(shù)據(jù),也稱(chēng)為字面量(literal)數(shù)據(jù);也可以從本機(jī)磁盤(pán)里讀取文件數(shù)據(jù);可以從內(nèi)存中讀取其他程序或資源的數(shù)據(jù);還可以從網(wǎng)絡(luò)服務(wù)器上請(qǐng)求數(shù)據(jù),等等。
但是對(duì)于前端開(kāi)發(fā)來(lái)說(shuō),我們使用 JavaScript 語(yǔ)言來(lái)寫(xiě)程序,一般是操作網(wǎng)頁(yè),獲取數(shù)據(jù)的途徑一般就三種:自己造數(shù)據(jù);讀取當(dāng)前頁(yè)面的數(shù)據(jù);從網(wǎng)絡(luò)服務(wù)器上請(qǐng)求數(shù)據(jù)。
- 第一種沒(méi)什么好說(shuō)的,就是寫(xiě)一堆字面量:
const greeting = 'hello world' - 第二種也簡(jiǎn)單,比如獲取 DOM 元素的數(shù)據(jù):
const lists = document.querySelectorAll('li'),或者獲取各種 Storage(Local Storage, Session Storage, IndexedDB, Cookies 等) 里面存的數(shù)據(jù):localStorage.getItem('person_key')- 這里存在一種情況,就是在當(dāng)前頁(yè)面里面有 iframe 元素時(shí),iframe 里面本質(zhì)上是嵌入了另一個(gè)頁(yè)面
- 這個(gè)頁(yè)面有它自己的一個(gè)全局環(huán)境(window 對(duì)象),所以它也有自己完整的一套 DOM,Storage
- 所以這時(shí)如果要從當(dāng)前頁(yè)面讀取這個(gè)嵌入頁(yè)面的數(shù)據(jù)的話(huà),也會(huì)存在是否同源的問(wèn)題,如果不同源的話(huà),也需要跨域
- 但是因?yàn)?iframe 元素用的不多,所以我們這里先不做考慮
- 第三種就麻煩一點(diǎn)了,我們一般是發(fā)送 Ajax 請(qǐng)求來(lái)獲取服務(wù)器端的數(shù)據(jù)
我們重點(diǎn)討論第三種方式。
要發(fā)送 Ajax 請(qǐng)求,就必然要指定一個(gè) URL,這樣才能告訴瀏覽器的 Ajax 引擎去哪獲取數(shù)據(jù)。
先說(shuō)一個(gè)概念,我們寫(xiě)的一段 JavaScript 程序,不管你是用原生 JavaScript 和最原始的方式寫(xiě)(script 標(biāo)簽),還是用各種庫(kù)、框架、構(gòu)建工具寫(xiě),最終都必然要運(yùn)行在某個(gè)頁(yè)面上,這個(gè)頁(yè)面也必然對(duì)應(yīng)著一個(gè)它自己的 URL。
那么這時(shí)候問(wèn)題就來(lái)了,如果你在 Ajax 請(qǐng)求中指定的這個(gè) URL 跟你當(dāng)前運(yùn)行這段 JavaScript 程序的頁(yè)面的 URL 不同源的話(huà),你的這次請(qǐng)求很可能就會(huì)被瀏覽器拒絕(在沒(méi)有實(shí)現(xiàn)跨域的情況下,一定會(huì)被拒絕),拒絕的表現(xiàn)就是在控制臺(tái)給你報(bào)一個(gè)錯(cuò),當(dāng)然數(shù)據(jù)也拿不到。
問(wèn)題又來(lái)了,什么叫同源,什么叫不同源?為什么不同源瀏覽器會(huì)拒絕呢?這就終于涉及到了我們?cè)谖恼麻_(kāi)頭說(shuō)的「同源策略」。
讓我們先在腦中有一個(gè)基本的概念,雖然我們現(xiàn)在可能還不清楚同源的概念,但是既然涉及到相同和不相同,那一定要掌握兩方面的信息才能得出是否相同的結(jié)論:誰(shuí)跟誰(shuí)做比較?比較的東西是什么?比如「你跟你爸的長(zhǎng)相是否相同」,比較的雙方就是你和你爸,比較的東西就是你們的長(zhǎng)相。
具體到同源這個(gè)概念,用來(lái)做比較的雙方分別是:
- 在當(dāng)前頁(yè)面下運(yùn)行的 JavaScript 程序中,發(fā)送 Ajax 請(qǐng)求的代碼里面指定的 URL
- 當(dāng)前頁(yè)面的 URL
比較什么東西呢?就是同源里的「源」(origin),要知道什么是「源」,我們先看一下 URL 的組成
我們知道,一個(gè)完整的 URL 由以下幾部分組成
scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]
其中協(xié)議(scheme),域名(host),端口號(hào)(port)共同組成了「源」,所以是否同源就是比較它們?nèi)齻€(gè)。
這三者必須完全相同,兩個(gè) URL 才算是同源,只要有一個(gè)不相同,就是不同源。判斷是否完全相同是按照字符逐個(gè)比較,字符沒(méi)有完全匹配即是不相同。
比如域名 127.0.0.1 和 localhost 都表示本機(jī)域名,但是因?yàn)樗鼈兊淖址煌?,所以也算不同源?br>
具體例子有很多,我就不寫(xiě)了,網(wǎng)上一大把,寫(xiě)這個(gè)沒(méi)意思。
知道了什么是同源和不同源,我們?cè)賮?lái)說(shuō)什么是「同源策略」。
首先要知道同源策略是瀏覽器實(shí)現(xiàn)的一種網(wǎng)頁(yè)安全機(jī)制,所謂策略或者說(shuō)機(jī)制,就是瀏覽器根據(jù)某些條件和計(jì)劃來(lái)做出對(duì)應(yīng)的行為,這樣說(shuō)有點(diǎn)虛。舉個(gè)例子:你爸跟你說(shuō),下次考試你要是考到了全班前三名,就獎(jiǎng)勵(lì)你一個(gè)蘋(píng)果手機(jī),你要是不及格,就給你一頓揍。這就是你爸制定的一個(gè)激勵(lì)你學(xué)習(xí)的策略。
具體到同源策略,就是瀏覽器說(shuō),你要是給我的 URL 同源,我就給你數(shù)據(jù);要是不同源,呵呵,我就給你報(bào)錯(cuò)。
雖然它是一種實(shí)現(xiàn)(implementation),但是并不存在一個(gè)與之對(duì)應(yīng)的規(guī)范(specification),這里插一句,說(shuō)一下什么是規(guī)范,什么是實(shí)現(xiàn)。
舉個(gè)例子,如果說(shuō)「規(guī)范」就是一份說(shuō)明怎么制造一個(gè)杯子的文件(用什么材質(zhì),什么結(jié)構(gòu),制作工序等等),那么「實(shí)現(xiàn)」就是根據(jù)這個(gè)規(guī)范做出來(lái)的杯子。
在 Web 里面,制定規(guī)范的都是一些非盈利性的組織和機(jī)構(gòu),比如 W3C,ECMA 等等,實(shí)現(xiàn)規(guī)范的就是各大瀏覽器廠(chǎng)商,比如 Chrome, IE, FireFox 等等,對(duì)某個(gè)規(guī)范的實(shí)現(xiàn)就是某個(gè)具體的功能,比如 HTML 規(guī)范的實(shí)現(xiàn)就是瀏覽器能正確解析 HTML 文件,將它們顯示成頁(yè)面的這個(gè)功能。
需要注意的是,實(shí)現(xiàn)并不一定完全符合規(guī)范,就好比你生產(chǎn)一個(gè)杯子,如果消費(fèi)者喜歡有把手的杯子,但是規(guī)范上又沒(méi)說(shuō)要加把手,這時(shí)候你很可能會(huì)不按照規(guī)范,做一個(gè)有把手的杯子出來(lái)。
Web 里面同樣如此,有些規(guī)范里有的內(nèi)容瀏覽器沒(méi)有實(shí)現(xiàn),有些規(guī)范里沒(méi)有的內(nèi)容,瀏覽器又實(shí)現(xiàn)了。說(shuō)白了規(guī)范對(duì)實(shí)現(xiàn)沒(méi)有約束力,我覺(jué)得你好我就實(shí)現(xiàn),覺(jué)得不好壓根不理你。
扯遠(yuǎn)了,回到同源策略。前面說(shuō)到同源策略沒(méi)有規(guī)范,所以瀏覽器廠(chǎng)商各自的實(shí)現(xiàn)并不完全一樣,但基本目的是一樣的:一個(gè)頁(yè)面中的 JavaScript 程序,無(wú)法訪(fǎng)問(wèn)到跟它不同源的另一個(gè)頁(yè)面(或者URL)里面的數(shù)據(jù)。這里所說(shuō)的數(shù)據(jù),就是我們前面說(shuō)的 DOM 元素?cái)?shù)據(jù),Storage 數(shù)據(jù),還有 Ajax 請(qǐng)求的數(shù)據(jù)。
那么問(wèn)題來(lái)了,瀏覽器為什么要這么做呢?
其實(shí)很好理解,它是出于網(wǎng)絡(luò)安全的考慮。想像一下如果沒(méi)有同源策略,那任何一個(gè)網(wǎng)頁(yè)都可以通過(guò)編寫(xiě) JavaScript 程序來(lái)獲取其他網(wǎng)頁(yè)的敏感數(shù)據(jù),這不是很恐怖嗎?假如你剛剛登陸了網(wǎng)銀,然后又訪(fǎng)問(wèn)了某個(gè)惡意網(wǎng)頁(yè),那這個(gè)惡意網(wǎng)頁(yè)就可以訪(fǎng)問(wèn)甚至修改你的網(wǎng)銀賬戶(hù)數(shù)據(jù)。這樣的話(huà),網(wǎng)絡(luò)就一點(diǎn)都不安全了。
雖然瀏覽器搞的這個(gè)同源策略看起來(lái)很美好,但凡事有利就有弊,有時(shí)候我們也不希望它存在。
比如我要發(fā)送 Ajax 請(qǐng)求訪(fǎng)問(wèn)某個(gè) URL 來(lái)獲取數(shù)據(jù),這些數(shù)據(jù)是別人網(wǎng)站提供的公共數(shù)據(jù),沒(méi)有安全性問(wèn)題,誰(shuí)都能訪(fǎng)問(wèn)(比如一些網(wǎng)部的天氣數(shù)據(jù) URL 接口,新聞數(shù)據(jù) URL 接口等等),但是因?yàn)橥床呗?,不好意思,瀏覽器直接給你拒了。
還比如我們公司是百度,baidu.com 這個(gè)主域名下有很多子域名:news.baidu.com, tieba.baidu.com 等等,那我的主域名頁(yè)面要獲取子域名頁(yè)面的數(shù)據(jù),還有子域名頁(yè)面相互之間通信都會(huì)受到同源策略的限制。
那這要怎么辦呢?這時(shí)候就終于引出了我們要說(shuō)的重點(diǎn):「跨域」
要解決同源策略對(duì)于不同源網(wǎng)頁(yè)之間通信的限制問(wèn)題,就要通過(guò)跨域來(lái)處理。所謂跨域就是下面我們要說(shuō)的解決方案的統(tǒng)稱(chēng),是一套技術(shù)的集合。
通過(guò)前面的學(xué)習(xí),我們發(fā)現(xiàn)跨域這個(gè)詞其實(shí)并不太準(zhǔn)確,應(yīng)該叫「跨源」,因?yàn)橛蛎麅H僅是源的一部分而已(其他兩部分是協(xié)議名和端口號(hào)),但是我們?cè)趯?shí)際開(kāi)發(fā)中遇到最多的還是域名不同導(dǎo)致的不同源,所以才有了跨域這個(gè)說(shuō)法(僅僅是筆者的猜測(cè))。
好了,大家可以發(fā)現(xiàn),我們花了很多時(shí)間來(lái)解釋概念性問(wèn)題,寫(xiě)了這么多還沒(méi)說(shuō)到跨域應(yīng)該怎么跨,別的技術(shù)文章到這估計(jì)都把跨域的幾種方式總結(jié)完了,但是那樣又有什么意義呢?你總結(jié)了十來(lái)種方式,其實(shí)工作中用到的也就那么一兩種,你是人不是機(jī)器,那么多技術(shù)方案你都要背下來(lái)嗎?
很多東西搞清楚 what 和 why 比 how 更重要,因?yàn)?how 是不斷變化的,但是原理性的東西一般不會(huì)輕易改變。掌握了 what 和 why,再來(lái)學(xué) how 就很輕松了。
其實(shí)寫(xiě)到這里,后面跨域的幾種方案,都可以在網(wǎng)上找到詳細(xì)的說(shuō)明。筆者主要想詳細(xì)說(shuō)一下 JSONP 和 CORS 這兩種常用的方法,今天太累了,明天再寫(xiě)。