1、爲何須要JS跨域
假設咱們構建了一個網上商城www.xxx.com,出於對用戶帳號安全性的考慮,咱們將用戶登陸統一到auth.xxx.com的子域下驗證。當一個未登陸用戶瀏覽商品之後點擊購買,爲了提升網站的用戶體驗,咱們想提供一個無刷新的登陸入口。咱們馬上想到使用AJAX實現無刷新的數據交互,可當咱們實際使用AJAX向auth.xxx.com提交數據的時候,JS卻出現錯誤提示,咱們沒有權限進行此操做,由於XMLHTTPRequest的實現要遵循瀏覽器的安全模型的同源策略規則,JS只能往本身的同源(同協議、同域名、同端口)發送XMLHTTPRequest請求,因此咱們沒法往子域發送AJAX 請求。
解決此問題的一般作法是由auth.xxx.com提供接口,在www.xxx.com的域下作一個服務端代理。但隨着業務的發展,咱們又有 a.xxx.com,b.xxx.com...等等都須要到auth.xxx.com作登陸驗證,那麼咱們須要在每個子域下都有一個服務端的 Proxy,值得慶幸的是這些子域都是咱們本身的,對服務器上的程序有控制權,因此操做起來仍是可行的。然而隨着業務的繼續發展,咱們有愈來愈多的合做夥伴,如今咱們要與他們實現部分數據的跨域互通,那麼咱們就須要和他們去約定一大堆的接口,而後本身逐一去實現Proxy,那麼這種方式會帶來很大的溝通和維護成本。有沒有更好的方式呢?有!那就是客戶端的JS跨域方案。
2、瀏覽器端的JS跨域方案
不管是子域仍是非子域,JAVASCRIPT自己並無官方的跨域實現,下面介紹的跨域方案都是開發者創造出來的巧妙的hack實現:
一、設置document.domain跨子域
二、使用iframe的hash
三、使用script標籤
四、flash轉發
上面各類方案的適用場景和開發成本也是不一樣的,你能夠根據具體的狀況來選擇合適的跨域方案。
一、設置document.domain
若是僅僅是要跨子域,那麼最簡單的方式是設置document.domain,再配合隱藏的iframe,而後經過下面的方式來完成異步請求。
1.1 使用FORM提交
早期瀏覽器並不支持XMLHTTPRequest時,開發者就已經使用隱藏iframe和form的方式來作同源的異步數據提交,原理就是iframe間的相互通訊。雖然咱們如今是在子域間作異步數據提交,但經過設置domain咱們已經獲取了子域iframe之間的控制權(須要注意的是並無直接使用 XMLHTTPRequest往auth子域下的頁面發送異步請求的權限),那麼下面要作的工做和之前沒有區別,這裏就大概描述一下,更具體的能夠搜索下使用iframe作異步提交的資源。
咱們在A頁面(http://www.xxx.com/index.html)中以iframe形式插入一個auth子域下的B頁面(http: //auth.xxx.com/proxy.html),並設置index.html和proxy.html頁面的domain爲xxx.com,這樣 index.html和proxy.html就有了互相訪問的權限。
當用戶在index.html輸入登陸信息提交時,咱們能夠獲取到用戶輸入的用戶名和密碼,而後咱們把此信息寫入到proxy.html頁面中。一般咱們會在proxy頁面中放置一個form,而後把須要提交的數據寫入到此form中再提交此form,這樣就完成了www到auth的數據提交。那咱們怎麼知道用戶是否登陸成功呢,咱們須要一個反饋。proxy中的form對應的action頁面接受到登陸信息之後作驗證,而後將反饋信息以JS腳本的形式輸出,例如document.domain='xxx.com'; top.showLogin({'code':'0','user':'yoyo'});,這樣若是咱們在index頁面中定義了showLogin函數,那麼用戶登陸之後就會自動調用,而且接收都到了auth的登陸反饋。
1.2 使用XMLHTTPRequest提交
剛纔咱們是把數據經過proxy頁面中的FORM提交出去的,實際上如今的主流瀏覽器都已經支持XMLHTTPRequest,咱們徹底能夠控制 proxy頁面實例化一個XMLHTTPRequest對象,那麼這個實例是在proxy的域下的,它是能夠往auth.xxx.com提交AJAX請求的。這和咱們頁面自己發送AJAX請求所不一樣的僅僅在於操做的window對象不一樣,若是咱們把這個差別封裝起來,那使用者也是感覺不到其中的差別的。
二、使用iframe的hash 同剛纔的作法咱們在A頁面(http://www.xxx.com/index.html)中以iframe形式插入一個auth子域下的B頁面 (http://auth.xxx.com/proxy.html),但此次咱們不須要設置document.domain,受安全策略的限制A頁面的 JS沒法獲取到A頁面中iframe的src屬性,更沒有辦法訪問頁面B中的元素,但它能夠設置iframe的src,假如A頁面輸入的用戶名和密碼同時以必定格式加入到了src對應的url中,例如:http://auth.xxx.com/proxy.html?user=yoyo& pwd=123456,B頁面是能夠在GET信息中獲取此登陸信息作驗證的。這就完成了A到B的SET操做。那B登陸驗證之後怎麼給A反饋呢?同理,咱們能夠在B頁面以iframe形式引入一個www下的A_proxy頁面(http://www.xxx.com/proxy.html),那麼它也能夠經過設置iframe的src的方式實現B到A_proxy的SET操做,而A_proxy和A是一個域,他們之間就能夠自由地完成通訊了。
若是咱們把參數以GET的形式設置到iframe的src中,會形成iframe頻繁發送請求。爲了不這點,咱們能夠把參數以錨點的形式傳入進入,同時在iframe裏對錨點的改變作一個監聽便可。那如何監聽錨點的改變呢?
2.1:能夠在iframe中使用setInterval頻繁檢查location.hash是否改變。若是你對你的代碼效率有潔癖,那麼你能夠考慮使用下面的方法。
2.2:在設置iframe的src的同時,改變一下iframe的width屬性,iframe內部使用onresize事件獲知hash的改變。
此方法不侷限於跨子域的需求,在FaceBook的APP平臺中,第三方應用的iframe的高度自適應就是採用此方法實現的(實際上iframe高度自適應沒有A到B的set需求,僅僅是B到A的set需求)。須要注意的是此方法的弊端在於作SET操做的時候受制於各個瀏覽器對url長度的限制,沒法發送過多的數據。
三、使用script標籤
當咱們以script標籤的src屬性引入一個JS文件到頁面中時,咱們須要清楚下面兩條規則:
3.一、文件下載完畢之後,其中的JS會自動運行。
3.二、不管src來源的域是什麼,這段JS執行時信任的域都是此頁面的域。
當用戶登陸的時候,A頁面使用JS動態添加一個script節點到頁面中,而且把用戶名和密碼設置到src屬性中,例如:http://auth.xxx.com/login?user=yoyo&password=123456,那麼完成了A到B的SET操做,當B作完驗證之後,把結果以JS形式返回過來,例如:showLogin({'code':'0','user':'yoyo'})。根據規則一這個 JS函數會自動執行,若是咱們在頁面A中定義了showLogin的函數,這就實現了B到A的回調反饋。
此方法的經典應用就是JSONP,它在此核心功能的基礎上有定義了一些規則:例如返回值都是JSON格式,script標籤的src中引入一個 callback參數指定回調函數名。例如flickr的API,http://api.flickr.com/services/feeds /photos_public.gne?tags=cat&tagmode=any&format=json& jsoncallback=doSomething,返回的結果就是doSomething({JSON格式}),任何網站均可以經過JSONP調用此 API來展現此照片列表。
在Jquery中也有getScript和getJSON的封裝,它隱藏了script標籤和回調函數的動態建立、垃圾回收、非跨域狀況下的瀏覽器阻塞(具體細節的實現也是頗有意思的,有空我會單獨寫一個分析),使用者只須要處理數據和回調函數便可。
此方法不侷限於跨子域的需求,但因爲它也是依賴設置script標籤的src屬性來作的set操做,因此它一樣受制於各個瀏覽器對url長度的限制,沒法發送過多的數據。
四、flash轉發
ACTIONSCRIPT自己也有發送異步請求的實現,雖然FlashPlayer也有安全域限制,但相對於JS而言彷佛更人性化。最初我由AS轉入JS 的時候就很詫異,一樣是基於ECMAScript的實現,爲什麼JS沒有官方的跨域機制呢?首先簡單講一下FlashPlayer沙箱機制中的咱們會用到的三個主要原則:
4.一、FlashPlayer發送請求的時候也須要遵循瀏覽器的同源策略,在早期版本中子域被視爲安全域,但隨着安全級別的提升,目前遵循的同源策略和JS同樣,只能往本身的同源(同協議、同域名、同端口)發送請求。若是須要往第三方的域發送請求,那麼須要第三方在本身的server上放置一個 crossdomain.xml的XML文件。此文件就比如是一個白名單,能夠設置本身信任的第三方域的訪問。
4.二、A域的頁面引入了一個B域的Flash,此Flash是沒法和頁面中的JS通訊的,須要A域頁面在插入Flash的時候設置allowscriptaccess屬性爲B域(flash所在的域),或者是always(開放全部域)。
4.三、A域的頁面中的JS也沒法訪問B域的Flash,須要Flash在腳本中經過allowDomain設置了A域爲本身信任的安全域。
瞭解了上面三個原則之後,咱們就能夠解決A域頁面和B域的flash的互相通訊的問題,B域flash和A域的server通訊的問題。下面介紹下Flash轉發的實現原理:
用戶交互仍是由JS完成,當咱們須要跨域發送請求的時候,咱們可使用JS把數據傳遞給FLASH,而後由FLASH去跨域請求server,而後把 server處理完畢的結果傳遞給js。具體實現主要是網絡通訊和回調函數,須要特別注意的是FLASH和JS通訊的時候,字符串中若是出現"\",那麼須要在AS內部對"\"作一次轉義,不然到了JS端此"\"會被看成轉義符解析掉。
若是你是一個server端的程序員,你可能已經有一個疑惑了:真實的請求是由B域的flash發送的,若是咱們的這個請求須要A域下的cookie信息怎麼辦呢?這裏就須要提到一個FlashPlayer的奇妙"特性",不管flash的域在哪兒,只要A域的server容許此域的flash訪問,它向 A域發送請求的時候,都會把此時瀏覽器中屬於A域的cookie一同發送過去,所以"理論"上你不須要擔憂這個問題,但我須要提醒兩點:
1、這個特性僅限於URLRequest類,AS中的文件上傳類就沒有此特性,相反它的實現讓人感受是殘缺的,見"當心SWFUpload的cookie Bug"。
2、在我項目中我曾經遇到過URLRequest類也丟失cookie的狀況,但後來一直沒有再重現,我也沒法排除當時有其餘因素的影響,因此我剛纔加了"理論"二字。若是你遇到了這個狀況,也能夠給我一個反饋,一塊兒交流一下。如今我爲了保險起見,我仍是主動用JS把cookie獲取,而後加入到 flash的請求中。
在Facebook的APP平臺中有一個Fb:local-proxy的實現,實際上核心就是Flash轉發,它的優點在於跨域沒有侷限,SET和GET操做也沒有數據長度的侷限,劣勢在於客戶端須要依賴FLASH,須要考慮的客戶端複雜性。
測試環境部署麻煩,並且空餘時間也有限,因此沒有去寫具體的示例代碼,上面寫得仍是很粗,見諒。細節問題能夠郵件、IM交流。