林東洲html
1 天前前端
想學習更多前端或編程知識,歡迎關注專欄: 敲代碼,學編程 - 知乎專欄
閱讀本文前,但願你有必定的 JS/Node 基礎,這裏不另外介紹如何使用 ajax 作異步請求,若是不瞭解,能夠先看:node
Ajax 知識體系大梳理 - 掘金nginx
最近在面試的時候常被問到如何解決跨域的問題,看了網上的一些文章後,發現許多文章都沒有寫清楚明白,使讀者(我)感到困惑,因此今天我整理了一下經常使用跨域的技巧,寫這篇關於跨域文章的目的在於:git
這篇文章的全部代碼我放在了 happylindz/blog Github 上,建議你 clone 下來,方便你閱讀代碼,跟我一塊兒測試。github
後面代碼的測試環境:不考慮跨域的兼容性問題,旨在理解其思想面試
使用過 Ajax 的同窗都知道其便利性,能夠在不向服務器提交完整的頁面的狀況下,實現局部更新頁面。可是瀏覽器處於對安全方面的考慮,不容許跨域調用其餘頁面的對象,這對於咱們在注入 iframe 或是 ajax 應用上帶來很多麻煩。ajax
簡單說來,只有當協議,域名,端口相同的時候纔算是同一個域名,不然均認爲須要作跨域的處理。chrome
今天一共介紹七種經常使用跨域的方式,關於跨域大概能夠分爲 iframe 的跨域和純粹的跨全域請求。npm
下面就先介紹三種跨全域的方法:
1. JSONP:
只要說到跨域,就必須聊到 JSONP,JSONP全稱爲:JSON with Padding,可用於解決主流瀏覽器的跨域數據訪問的問題。
Web 頁面上調用 js 文件不受瀏覽器同源策略的影響,因此經過 Script 便籤能夠進行跨域的請求:
實例:
後端邏輯:
//server.js const url = require('url'); require('http').createServer((req, res) => { const data = { x: 10 }; const callback = url.parse(req.url, true).query.callback; res.writeHead(200); res.end(`${callback}(${JSON.stringify(data)})`); }).listen(3000, '127.0.0.1'); console.log('啓動服務,監聽 127.0.0.1:3000');
經過 node server.js 啓動服務,監聽端口 3000,這樣服務端就創建起來了
前端頁面:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>index.html</title> </head> <body> <script> function jsonpCallback(data) { alert('得到 X 數據:' + data.x); } </script> <script src="http://127.0.0.1:3000?callback=jsonpCallback"></script> </body> </html>
邏輯已經寫好了,那如何來模擬一個跨域的場景呢?
這裏咱們經過端口號的不一樣來模擬跨域的場景,經過 http://127.0.0.1:8080 端口來訪問頁面。先經過 npm 下載 http-server 模塊:
nom install -g http-server
而且在頁面同目錄下輸入:
http-server
這樣就能夠經過端口 8080 訪問 index.html 剛纔那個頁面了,至關因而開啓兩個監聽不一樣端口的 http 服務器,經過頁面中的請求來模擬跨域的場景。打開瀏覽器,訪問 http://127.0.0.1:8080 就能夠看到從 http://127.0.0.1:3000 獲取到的數據了。
至此,經過 JSONP 跨域獲取數據已經成功了,可是經過這種事方式也存在着必定的優缺點:
優勢:
缺點:
2. CORS:
CORS 是一個 W3C 標準,全稱是"跨域資源共享"(Cross-origin resource sharing)它容許瀏覽器向跨源服務器,發出 XMLHttpRequest 請求,從而克服了 ajax 只能同源使用的限制。
CORS 須要瀏覽器和服務器同時支持才能夠生效,對於開發者來講,CORS 通訊與同源的 ajax 通訊沒有差異,代碼徹底同樣。瀏覽器一旦發現 ajax 請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感受。
所以,實現 CORS 通訊的關鍵是服務器。只要服務器實現了 CORS 接口,就能夠跨源通訊。
首先前端先建立一個 index.html 頁面:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CORS</title> </head> <body> <script> const xhr = new XMLHttpRequest(); xhr.open('GET', 'http://127.0.0.1:3000', true); xhr.onreadystatechange = function() { if(xhr.readyState === 4 && xhr.status === 200) { alert(xhr.responseText); } } xhr.send(null); </script> </body> </html>
這彷佛跟一次正常的異步 ajax 請求沒有什麼區別,關鍵是在服務端收到請求後的處理:
require('http').createServer((req, res) => { res.writeHead(200, { 'Access-Control-Allow-Origin': 'http://localhost:8080' }); res.end('這是你要的數據:1111'); }).listen(3000, '127.0.0.1'); console.log('啓動服務,監聽 127.0.0.1:3000');
關鍵是在於設置相應頭中的 Access-Control-Allow-Origin,該值要與請求頭中 Origin 一致才能生效,不然將跨域失敗。
接下來再次開啓兩個 http 服務器進程:
打開瀏覽器訪問 http://localhost:8080 就能夠看到:
成功的關鍵在於 Access-Control-Allow-Origin 是否包含請求頁面的域名,若是不包含的話,瀏覽器將認爲這是一次失敗的異步請求,將會調用 xhr.onerror 中的函數。
CORS 的優缺點:
這裏只是對 CORS 作一個簡單的介紹,若是想更詳細地瞭解其原理的話,能夠看看下面這篇文章:
3. Server Proxy:
服務器代理,顧名思義,當你須要有跨域的請求操做時發送請求給後端,讓後端幫你代爲請求,而後最後將獲取的結果發送給你。
假設有這樣的一個場景,你的頁面須要獲取 CNode:Node.js專業中文社區 論壇上一些數據,如經過 https://cnodejs.org/api/v1/topics,當時由於不一樣域,因此你能夠將請求後端,讓其對該請求代爲轉發。
代碼以下:
const url = require('url'); const http = require('http'); const https = require('https'); const server = http.createServer((req, res) => { const path = url.parse(req.url).path.slice(1); if(path === 'topics') { https.get('https://cnodejs.org/api/v1/topics', (resp) => { let data = ""; resp.on('data', chunk => { data += chunk; }); resp.on('end', () => { res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(data); }); }) } }).listen(3000, '127.0.0.1'); console.log('啓動服務,監聽 127.0.0.1:3000');
經過代碼你能夠看出,當你訪問 http://127.0.0.1:3000 的時候,服務器收到請求,會代你發送請求https://cnodejs.org/api/v1/topics 最後將獲取到的數據發送給瀏覽器。
一樣地開啓服務:
打開瀏覽器訪問 http://localhost:3000/topics,就能夠看到
跨域請求成功。
純粹的跨全域請求的方式已經介紹完了,另外介紹四種經過 iframe 跨域與其它頁面通訊的方式。
4. location.hash:
在 url 中,http://www.baidu.com#helloworld 的 "#helloworld" 就是 location.hash,改變 hash 值不會致使頁面刷新,因此能夠利用 hash 值來進行數據的傳遞,固然數據量是有限的。
假設 localhost:8080 下有文件 cs1.html 要和 localhost:8081 下的 cs2.html 傳遞消息,cs1.html 首先建立一個隱藏的 iframe,iframe 的 src 指向 localhost:8081/cs2.html,這時的 hash 值就能夠作參數傳遞。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CS1</title> </head> <body> <script> // http://localhost:8080/cs1.html let ifr = document.createElement('iframe'); ifr.style.display = 'none'; ifr.src = "http://localhost:8081/cs2.html#data"; document.body.appendChild(ifr); function checkHash() { try { let data = location.hash ? location.hash.substring(1) : ''; console.log('得到到的數據是:', data); }catch(e) { } } window.addEventListener('hashchange', function(e) { console.log('得到的數據是:', location.hash.substring(1)); }); </script> </body> </html>
cs2.html 收到消息後經過 parent.location.hash 值來修改 cs1.html 的 hash 值,從而達到數據傳遞。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CS2</title> </head> <body> <script> // http://locahost:8081/cs2.html switch(location.hash) { case "#data": callback(); break; } function callback() { const data = "some number: 1111" try { parent.location.hash = data; }catch(e) { // ie, chrome 下的安全機制沒法修改 parent.location.hash // 因此要利用一箇中間的代理 iframe var ifrproxy = document.createElement('iframe'); ifrproxy.style.display = 'none'; ifrproxy.src = 'http://localhost:8080/cs3.html#' + data; // 該文件在請求域名的域下 document.body.appendChild(ifrproxy); } } </script> </body> </html>
因爲兩個頁面不在同一個域下IE、Chrome不容許修改parent.location.hash的值,因此要藉助於 localhost:8080 域名下的一個代理 iframe 的 cs3.html 頁面
<script> parent.parent.location.hash = self.location.hash.substring(1); </script>
以後老規矩,開啓兩個 http 服務器:
這裏爲了圖方便,將 cs1,2,3 都放在同個文件夾下,實際狀況的話 cs1.html 和 cs3.html 要與 cs2.html 分別放在不一樣的服務器纔對。
以後打開瀏覽器訪問 localhost:8080/cs1.html,注意不是 8081,就能夠看到獲取到的數據了,此時頁面的 hash 值也已經改變。
固然這種方法存在着諸多的缺點:
5. window.name:
window.name(通常在 js 代碼裏出現)的值不是一個普通的全局變量,而是當前窗口的名字,這裏要注意的是每一個 iframe 都有包裹它的 window,而這個 window 是top window 的子窗口,而它天然也有 window.name 的屬性,window.name 屬性的神奇之處在於 name 值在不一樣的頁面(甚至不一樣域名)加載後依舊存在(若是沒修改則值不會變化),而且能夠支持很是長的 name 值(2MB)。
舉個簡單的例子:
你在某個頁面的控制檯輸入:
window.name = "Hello World"; window.location = "http://www.baidu.com";
頁面跳轉到了百度首頁,可是 window.name 卻被保存了下來,仍是 Hello World,跨域解決方案彷佛能夠呼之欲出了:
首先建立 a.html 文件:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>a.html</title> </head> <body> <script> let data = ''; const ifr = document.createElement('iframe'); ifr.src = "http://localhost:8081/b.html"; ifr.style.display = 'none'; document.body.appendChild(ifr); ifr.onload = function() { ifr.onload = function() { data = ifr.contentWindow.name; console.log('收到數據:', data); } ifr.src = "http://localhost:8080/c.html"; } </script> </body> </html>
以後在建立 b.html 文件:
<script> window.name = "你想要的數據!"; </script>
http://localhost:8080/a.html 在請求遠端服務器 http://localhost:8081/b.html 的數據,咱們能夠在該頁面下新建一個 iframe,該 iframe 的 src 屬性指向服務器地址,(利用 iframe 標籤的跨域能力),服務器文件 b.html 設置好 window.name 的值。
可是因爲 a.html 頁面和該頁面 iframe 的 src 若是不一樣源的話,則沒法操做 iframe 裏的任何東西,因此就取不到 iframe 的 name 值,因此咱們須要在 b.html 加載完後從新換個 src 去指向一個同源的 html 文件,或者設置成 'about:blank;' 都行,這時候我只要在 a.html 相同目錄下新建一個 c.html 的空頁面便可。若是不從新指向 src 的話直接獲取的 window.name 的話會報錯:
老規矩,打開兩個 http 服務器:
打開瀏覽器就能夠看到結果:
6. postMessage:
postMessage 是 HTML5 新增長的一項功能,跨文檔消息傳輸(Cross Document Messaging),目前:Chrome 2.0+、Internet Explorer 8.0+, Firefox 3.0+, Opera 9.6+, 和 Safari 4.0+ 都支持這項功能,使用起來也特別簡單。
首先建立 a.html 文件:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>a.html</title> </head> <body> <iframe src="http://localhost:8081/b.html" style='display: none;'></iframe> <script> window.onload = function() { let targetOrigin = 'http://localhost:8081'; window.frames[0].postMessage('我要給你發消息了!', targetOrigin); } window.addEventListener('message', function(e) { console.log('a.html 接收到的消息:', e.data); }); </script> </body> </html>
建立一個 iframe,使用 iframe 的一個方法 postMessage 能夠想 http://localhost:8081/b.html 發送消息,而後監聽 message,能夠得到其餘文檔發來的消息。
一樣的 b.html 文件:
<script> window.addEventListener('message', function(e) { if(e.source != window.parent) { return; } let data = e.data; console.log('b.html 接收到的消息:', data); parent.postMessage('我已經接收到消息了!', e.origin); }); </script>
一樣的開啓 http 服務器:
打開瀏覽器一樣能夠看到:
對 postMessage 感興趣的詳細內容能夠看看教程:
7. document.domain:
對於主域相同而子域不一樣的狀況下,能夠經過設置 document.domain 的辦法來解決,具體作法是能夠在 http://www.example.com/a.html 和 http://sub.example.com/b.html 兩個文件分別加上 document.domain = "a.com";而後經過 a.html 文件建立一個 iframe,去控制 iframe 的 window,從而進行交互,固然這種方法只能解決主域相同而二級域名不一樣的狀況,若是你異想天開的把 script.example.com 的 domain 設爲 qq.com 顯然是沒用的,那麼如何測試呢?
測試的方式稍微複雜點,須要安裝 nginx 作域名映射,若是你電腦沒有安裝 nginx,請先去安裝一下: nginx news
先建立一個 a.html 文件:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>a.html</title> </head> <body> <script> document.domain = 'example.com'; let ifr = document.createElement('iframe'); ifr.src = 'http://sub.example.com/b.html'; ifr.style.display = 'none'; document.body.append(ifr); ifr.onload = function() { let win = ifr.contentWindow; alert(win.data); } </script> </body> </html>
在建立一個 b.html 文件:
<script> document.domain = 'example.com'; window.data = '傳送的數據:1111'; </script>
以後打開 http 服務器:
這時候只是開啓了兩個 http 服務器,還須要經過 nginx 作域名映射,將 Example Domain 映射到 localhost:8080,sub.example.com 映射到 localhost:8081 上
打開操做系統下的 hosts 文件:mac 是位於 /etc/hosts 文件,並添加:
127.0.0.1 www.example.com 127.0.0.1 sub.example.com
這樣在瀏覽器打開這兩個網址後就會訪問本地的服務器。
以後打開 nginx 的配置文件:/usr/local/etc/nginx/nginx.conf,並在 http 模塊裏添加:
上面代碼的意思是:若是訪問本地的域名是 Example Domain,就由 localhost:8080 代理該請求。
因此咱們這時候在打開瀏覽器訪問 Example Domain 的時候其實訪問的就是本地服務器 localhost:8080。
最後打開瀏覽器訪問 http://www.example.com/a.html 就能夠看到結果:
8. flash:
這種方式我沒有嘗試過,很差往下定論,感興趣的話能夠上網搜看看教程。
前面八種跨域方式我已經所有講完,其實講道理,經常使用的也就是前三種方式,後面四種更多時候是一些小技巧,雖然在工做中不必定會用到,可是若是你在面試過程當中可以提到這些跨域的技巧,無疑在面試官的心中是一個加分項。
上面闡述方法的時候可能有些講的不明白,但願在閱讀的過程當中建議你跟着我敲代碼,當你打開瀏覽器看到結果的時候,你也就能掌握到這種方法。