跨領域是開發中常會遇到的場景,也是面試中常會討論的問題。掌握常見的跨域解決方案及其背後的原理,不僅可以提高我們的開發效率,還能在面試中表現的更加游刃有餘。
因此今天就來跟大家從前端的角度來聊聊解決跨域常見的幾種方式。
在講跨域之前,我們先來看看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/ // 端口号不同
跨域問題的出現是受限於瀏覽器的同源策略。
所謂的同源策略,其實是瀏覽器的安全機制,用來限制一個網頁中網路請求僅能夠存取來自相同來源(網域名稱、協定和連接埠號碼皆相同)的資源,主要目的是防止惡意網站透過腳本竊取其他網站的敏感數據,保障使用者的隱私和安全。
解決跨域問題最常使用的方案是使用代理伺服器。
代理伺服器解決跨網域問題其實是抓住了同源策略只受限於瀏覽器存取伺服器,對於伺服器存取伺服器並沒有限制的特點,作為中間伺服器做了一個請求轉送的功能。
具體來說,就是前端工程師編寫的網頁運行在由webpack等腳手架搭建的代理伺服器上,當前端網頁在瀏覽器中發起網絡請求時,其實這個請求是發送到代理伺服器上的,然後代理伺服器會將請求轉送給目標伺服器,再將目標伺服器回傳的回應轉送給客戶端。 代理伺服器在過程中扮演了一個中轉的角色,可以對請求和回應進行一些修改、過濾和攔截,以實現一些特定的功能。因為前端網頁運行在代理伺服器上,所以不存在跨域問題。
那麼在線上環境和開發環境下,代理伺服器是如何做請求轉送的呢?nginx來做反向代理,從而把前端的請求轉送到目標接口上。
nginx是一個輕量級高並發的web伺服器,基於事件驅動,而且跨平台,window和Linux都可以進行設定。它作為代理伺服器來解決開發中的跨域問題的主要方法就是監聽線上前端網址的運行端口,然後碰到包含特殊標記的請求後就進行請求轉發 。
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檔案用於發起跨域請求:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>请求转发测试</title> </head> <body> <h1>请求转发测试</h1> <p id="message"></p> <script> fetch('/api/login') .then(response => response.text()) .then(data => { document.getElementById('message').textContent = data; }); </script> </body> </html>
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的源码~
除了代理服务器这种绕过浏览器同源策略的解决方式外,从前端的角度解决跨域问题还有如下一些常见的方法:
JSONP的原理是通过动态创建