跨域是開發中經常會遇到的一個場景,也是面試中經常會討論的一個問題。掌握常見的跨域解決方案及其背后的原理,不僅可以提高我們的開發效率,還能在面試中表現的更加游刃有余。
因此今天就來和大家從前端的角度來聊聊解決跨域常見的幾種方式。
什么是跨域
在講跨域之前,我們先來看看URL的組成內容:
一個URL的組成,通常包含協議、主機名、端口號、路徑、查詢參數和錨點幾個部分。
這里展示了一個URL的示例:
https://www.example.com:8080/path/resource.html?page=1&sort=desc#header
在上述示例中:
● 協議為HTTPS
● 主機名為www.example.com
● 端口號為8080
● 路徑為/path/resource.html
● 查詢參數為page=1&sort=desc
● 錨點為header
所謂跨域,指的是請求URL中協議、主機名、端口號中任意一個部分不相同。
以上述URL為例,下面幾種寫法都算是和它跨域:
http://www.example.com:8080/????//?協議不同 https://www.example.a.com:8080/?//?主機名不同 https://www.example.com:8081/???//?端口號不同
為什么會跨域
其實跨域問題的出現是受限于瀏覽器的同源策略。
所謂同源策略,其實是瀏覽器的一種安全機制,用于限制一個網頁中的網絡請求僅能夠訪問來自同一源(域名、協議和端口號均相同)的資源,主要目的是防止惡意網站通過腳本竊取其他網站的敏感數據,保障用戶的隱私和安全。
當瀏覽器端的腳本(js文件)訪問了其他域的網絡資源時,就會出現跨域問題。
如何解決跨域
前文說到,跨域問題的出現是受限于瀏覽器的同源策略,那么常見的解決跨域問題的方案,其實也是圍繞著瀏覽器展開的:
1.代理服務器
在我們平常的開發中,解決跨域問題最常使用的方案是使用代理服務器。
代理服務器解決跨域問題其實是抓住了同源策略只受限于瀏覽器訪問服務器,對于服務器訪問服務器并沒有限制的特點,作為中間服務器做了一個請求轉發的功能。
具體來說,就是前端工程師編寫的網頁運行在由webpack等腳手架搭建的代理服務器上,當前端網頁在瀏覽器中發起網絡請求時,其實這個請求是發送到代理服務器上的,然后代理服務器會將請求轉發給目標服務器,再將目標服務器返回的響應轉發給客戶端。
代理服務器在此過程中扮演了一個中轉的角色,可以對請求和響應進行一些修改、過濾和攔截,以實現一些特定的功能。因為前端網頁運行在代理服務器上,所以不存在跨域問題。
那么在線上環境和開發環境下,代理服務器是如何做請求轉發的呢?
1.線上環境
在線上環境下,我們一般會采用nginx來做反向代理,從而把前端的請求轉發到目標接口上。
nginx是一個輕量級高并發的web服務器,基于事件驅動,而且跨平臺,window和Linux都可以進行配置。
它作為代理服務器來解決開發中的跨域問題的主要方法就是監聽線上前端網址的運行端口,然后碰到包含特殊標記的請求后就進行請求轉發。
2.開發環境
在開發環境下,無論是借助于webpack還是使用vite或其他腳手架搭建的前端項目,解決跨域問題的核心是借助http-proxy-middleware中間件實現的。而http-proxy-middleware中間件的核心又是對http-proxy的進一步封裝。
這里先展示一下在項目中使用http-proxy-middleware來實現請求轉發功能的示例代碼:
const?{?createProxyMiddleware?}?=?require('http-proxy-middleware'); module.exports?=?{ ??server:?{ ????proxy:?{ ??????//?將?/api/*?的請求代理到?http://localhost:3000/* ??????'/api':?{ ????????target:?'http://localhost:3000', ????????changeOrigin:?true, ????????pathRewrite:?{?'^/api':?'/'?} ??????} ????} ??} };
接著我們可以自己使用原生node,借助http-proxy庫來搭建一個具有請求轉發功能的代理服務器Demo,感興趣的朋友可以自己測試玩玩:
1. 首先需要創建一個空文件夾(全英命名)作為項目文件夾,然后使用npm init -y命令將項目升級為node的項目:
npm?init?-y
2. 接著在項目根目錄下創建一個index.html文件用于發起跨域請求:
nbsp;html> ????<meta> ????<title>請求轉發測試</title> ????<h1>請求轉發測試</h1> ????<p></p> ????<script> fetch('/api/login') .then(response => response.text()) .then(data => { document.getElementById('message').textContent = data; }); </script>
3. 接著在項目根目錄下新建index.js文件來編寫服務端的代碼。
index.js文件是實現具有請求轉發功能的代理服務器的核心文件。
const?http?=?require('http'); const?httpProxy?=?require('http-proxy'); const?fs?=?require('fs'); const?path?=?require('path'); //?創建代理服務器實例 const?proxy?=?httpProxy.createProxyServer({}); //?創建HTTP服務器 const?server?=?http.createServer((req,?res)?=>?{ ????if?(req.url?===?'/'?||?req.url.endsWith('.html'))?{ ????????//?讀取HTML文件 ????????const?filename?=?path.join(__dirname,?'index.html'); ????????fs.readFile(filename,?'utf8',?(err,?data)?=>?{ ????????????if?(err)?{ ????????????????res.writeHead(500); ????????????????res.end('Error?reading?HTML?file'); ????????????}?else?{ ????????????????res.writeHead(200,?{?'Content-Type':?'text/html'?}); ????????????????res.end(data); ????????????} ????????}); ????}?else?if?(req.url.startsWith('/api'))?{ ????????//?重寫路徑,替換跨域關鍵詞 ????????req.url?=?req.url.replace(/^/api/,?''); ????????//?將請求轉發至目標服務器 ????????proxy.web(req,?res,?{ ????????????target:?'http://localhost:3000/', ????????????changeOrigin:?true, ????????});???? ????} }); //?監聽端口 server.listen(8080,?()?=>?{ ????console.log('Server?started?on?port?8080'); });
4. 接著編寫目標服務器target.js文件的內容,用于測試跨域訪問:
const?http?=?require('http'); const?server?=?http.createServer((req,?res)?=>?{ ????if?(req.url.startsWith('/login'))?{ ????????res.writeHead(200,?{?'Content-Type':?'text/plain'?}); ????????res.end('我是localhost主機3000端口下的方法,恭喜你訪問成功!'); ????}?else?{ ????????res.writeHead(200,?{?'Content-Type':?'text/plain'?}); ????????res.end('Hello,?world!'); ????} }); server.listen(3000,?()?=>?{ ????console.log('Target?server?is?listening?on?port:3000'); })
5. 打開終端,輸入啟動目標服務器的命令:
node?./target.js?//項目根目錄下執行
6. 再開一個終端啟動代理服務器,等待瀏覽器端發起請求就可以啦:
node?./index.js?//項目根目錄下執行
7. 最后在瀏覽器里訪問http://localhost:8080, 打開控制臺即可查看效果:
可以發現,瀏覽器network模塊的網絡請求確實是訪問的8080端口的方法,但是我們的服務器默默的做了請求轉發的功能,并將請求轉發獲取到的內容返回到了前端頁面上。
其實http-proxy是對node內置庫http的進一步封裝,網絡請求的核心部分還是使用http創建一個服務器對象去訪問的。感興趣的同學可以再讀讀http-proxy的源碼~
除了代理服務器這種繞過瀏覽器同源策略的解決方式外,從前端的角度解決跨域問題還有如下一些常見的方法:
1.借助JSONP
JSONP的原理是通過動態創建<script>標簽</script>,向服務器發送請求并在請求URL中傳遞一個回調函數名(通常是在本地定義的函數名),服務器在返回的數據中將這個回調函數名和實際數據一起封裝成一個JavaScript函數的調用,返回給客戶端,客戶端利用該回調函數對數據進行處理。
JSONP之所以能夠跨域請求數據,是因為瀏覽器對于<script>標簽的請求不會受到同源策略的限制。</script>
需要注意的是,使用JSONP技術的前提是服務器需要支持JSONP的方式,即在返回的數據中包含回調函數名和實際數據的封裝,否則客戶端無法處理返回的數據。
此外,JSONP只支持GET請求,不支持POST等其他HTTP請求方式,因為<script>標簽只支持GET請求。</script>
因此JSONP這種方式在我們的開發中使用的場景不多。
2.使用CORS
CORS全稱為Cross-Origin Resource Sharing,它通過HTTP頭部信息告訴瀏覽器哪些跨域請求是被允許的,從而實現安全的跨域訪問。
CORS解決跨域需要瀏覽器端和服務器端的配合。原理是在服務器端設置HTTP頭部信息,告訴瀏覽器允許哪些源(域名、協議、端口)訪問服務器上的資源,如果請求的源不在允許的列表中,則瀏覽器將拒絕訪
問。
服務器可以通過設置Access-Control-Allow-Origin、Access-Control-Allow-Headers、Access-Control-Allow-Methods等HTTP頭部信息來控制跨域訪問權限。
具體地,當瀏覽器發起跨域請求時,會先發送一個OPTIONS請求(預檢請求),詢問服務器是否允許該跨域請求。
服務器接收到該請求后,根據請求中的HTTP頭部信息判斷是否允許該請求。
如果允許,則返回相應的HTTP頭部信息告知瀏覽器可以繼續發送真正的跨域請求。如果不允許,則返回一個錯誤狀態碼,告訴瀏覽器該請求被拒絕。
預檢請求時,請求頭常見參數有:
請求頭 | 值 |
---|---|
Origin | 表示請求的源地址,即發起跨域請求的域名 |
Access-Control-Request-Method | 表示實際請求采用的HTTP方法 |
Access-Control-Request-Headers | 表示實際請求中所攜帶的額外請求頭信息,比如自定義請求頭等 |
預檢請求時,響應頭常見參數有:
響應頭 | 值 |
---|---|
Access-Control-Allow-Origin | *、origin… |
Access-Control-Allow-Headers | POST, GET, PUT, DELETE, OPTIONS |
Access-Control-Allow-Methods | Content-Type, Authorization.. |
Access-Control-Allow-Credentials | true |
Access-Control-Max-Age | 86400 |
需要注意的是,使用CORS的前提是服務器需要設置相關的HTTP頭部信息,且瀏覽器支持CORS。此外,CORS只支持現代瀏覽器,對于一些老舊的瀏覽器可能不支持CORS。
3.其他方案
比如WebSocket、postMessage等等
總結
近年來,隨著前后端技術的飛速發展,前后端獨立開發逐漸成為主流的開發模式。前后端程序員只需約定好接口,然后獨自進行相應模塊的開發,最后進行接口聯調即可。在接口聯調過程中,開發環境下的跨域就是一個需要解決的問題。
除此之外,當前后端項目打包上云后,前端頁面通過線上地址訪問后臺接口時,線上環境下的跨域也是一個需要解決的問題。
本文講述了幾種常見的跨域解決方案,這些方案各有優缺點,大家可以根據實際情況選擇適合的方案來解決對應的跨域問題~