前文提到用jsonp的方式來跨域獲取數據,本文為大家介紹下如何利用window.name+iframe跨域獲取數據。
首先我們要簡單了解下window.name和iframe的相關知識。iframe是html的一個標簽,可以在網頁中創建內聯框架,有個src屬性(指向文件地址,html、php等)可以選擇內聯框架的內容,可以看個例子(猛戳這裡),大概了解下就行了。window.name(一般在js代碼裡出現)的值不是一個普通的全局變量,而是當前窗口的名字,這裡要注意的是每個iframe都有包裹它的window,而這個window是top window的子窗口,而它自然也有window.name的屬性,window.name屬性的神奇之處在於name 值在不同的頁面(甚至不同域名)加載後依舊存在(如果沒修改則值不會變化),並且可以支持非常長的 name 值(2MB)。
跨域解決方案似乎可以呼之欲出了,假設index.html頁面請求遠端服務器的數據,我們在該頁面下新建一個iframe標簽,該iframe的src屬性指向服務器文件地址(利用iframe標簽的跨域能力),服務器文件裡設置好window.name的值(也就是該iframe的contentWindow的name值),然後在index.html裡讀取該iframe的window.name值,一切似乎水到渠成,代碼如下:
<body> <script type="text/javascript"> iframe = document.createElement('iframe'), iframe.src = 'http://localhost:8080/data.php'; document.body.appendChild(iframe); iframe.onload = function() { console.log(iframe.contentWindow.name) }; </script> </body>
<?php echo '<script> window.name = "{\"name\":\"hanzichi\", \"age\":10}"; </script>' ?>
但是不幸的是,報錯了..
提示啥協議、主機、端口三者要一致,這不是赤裸裸地告訴你跨域了麼!為什麼會這樣,因為規定如果index.html頁面和和該頁面裡的iframe框架的src如果不同源,則也無法操作框架裡的任何東西,所以就取不到iframe框架的name值了,告訴你我們不是一家的,你也休想得到我這裡的數據。既然要同源,那就換個src去指,前面說了無論怎樣加載window.name值都不會變化,於是我們在index.html相同目錄下,新建了個proxy.html的空頁面,修改代碼如下:
<body> <script type="text/javascript"> iframe = document.createElement('iframe'), iframe.src = 'http://localhost:8080/data.php'; document.body.appendChild(iframe); iframe.onload = function() { iframe.src = 'http://localhost:81/cross-domain/proxy.html'; console.log(iframe.contentWindow.name) }; </script> </body>
理想似乎很美好,在iframe載入過程中,迅速重置iframe.src的指向,使之與index.html同源,那麼index頁面就能去獲取它的name值了!但是現實是殘酷的,iframe在現實中的表現是一直不停地刷新,也很好理解,每次觸發onload時間後,重置src,相當於重新載入頁面,又觸發onload事件,於是就不停地刷新了(但是需要的數據還是能輸出的)。修改後代碼如下:
<body> <script type="text/javascript"> iframe = document.createElement('iframe'); iframe.style.display = 'none'; var state = 0; iframe.onload = function() { if(state === 1) { var data = JSON.parse(iframe.contentWindow.name); console.log(data); iframe.contentWindow.document.write(''); iframe.contentWindow.close(); document.body.removeChild(iframe); } else if(state === 0) { state = 1; iframe.contentWindow.location = 'http://localhost:81/cross-domain/proxy.html'; } }; iframe.src = 'http://localhost:8080/data.php'; document.body.appendChild(iframe); </script> </body>
能使用這種方式跨域,有幾個條件必不可少。
再簡單了解下window和contentWindow的一些知識。浏覽器就會為原始文檔創建一個 window 對象,再為每個框架(iframe)創建額外的 window 對象。這些額外的對象是原始窗口的子窗口,可能被原始窗口中發生的事件所影響。例如,關閉原始窗口將導致關閉全部子窗口。contentWindow屬性是指指定的frame或者iframe所在的window對象。
很多人或許只關注使用方法,而對原理不怎麼感冒。此法相對於jsonp復雜,使用方法也更復雜些。
服務端一般輸出一段js代碼,例如下面這樣:
<?php echo '<script> window.name = "{\"name\":\"hanzichi\", \"age\":10}"; </script>' ?>
window.name的值是字符串形式,也是需要傳遞給客戶端的數據(當然如果有需要服務端也可以先處理傳過來的數據,這裡僅為只有數據回傳),有需要自己再去解析(如字符串->json數據)。
index.html頁面,我把方法封裝成了函數:
<body> <script type="text/javascript"> function crossDomain(url, fn) { iframe = document.createElement('iframe'); iframe.style.display = 'none'; var state = 0; iframe.onload = function() { if(state === 1) { fn(iframe.contentWindow.name); iframe.contentWindow.document.write(''); iframe.contentWindow.close(); document.body.removeChild(iframe); } else if(state === 0) { state = 1; iframe.contentWindow.location = 'http://localhost:81/cross-domain/proxy.html'; } }; iframe.src = url; document.body.appendChild(iframe); } // 調用 // 服務器地址 var url = 'http://localhost:8080/data.php'; crossDomain(url, function(data) { // 處理數據 data就是window.name的值(string) var data = JSON.parse(iframe.contentWindow.name); console.log(data); }); </script> </body>
這裡還有一點小問題,ie的兼容(iframe的onload ie下似乎要用attachEvent,以及json對於ie的兼容),有興趣的朋友自己去研究了(可以參考下參考文章);另外proxy代理頁面可以沒有這個文件,會報404但是不影響功能(但是路徑一定要和index頁面同源)。
還有一點值得思考的是iframe的src重定向的時候,代碼:
iframe.contentWindow.location = 'http://localhost:81/cross-domain/proxy.html';
這時iframe.src指向的還是服務端頁面,這裡的代碼如果用iframe.src = 'http://localhost:81/cross-domain/proxy.html';代替也是可以輸出結果的,聰明的你知道區別嗎?