當前位置: 妍妍網 > 碼農

良苦用心啊!我把7大跨域解決方法原理畫成10張圖,做成圖解!

2024-03-20碼農

前言

大家好,我是林三心。 用最通俗易懂的話講最難的知識點 是我的座右銘, 基礎是進階的前提 是我的初衷。

咱們做前端的,平時跟後端對接介面那是必須的事情,但是可能很多同學忽略了一個對接過程中可能會發生的問題—— 跨域 ,那跨域到底是啥呢?為什麽會跨域呢?又怎麽才能解決呢?

螢幕擷取2021-10-01 上午7.16.06.png

為什麽跨域?

image.png

為什麽會出現跨域問題呢?那就不得不講瀏覽器的 同源策略 了,它規定了 協定號-網域名稱-埠號 這三者必須都 相同 才符合 同源策略

螢幕擷取2021-10-01 上午8.50.11.png

如有有一個 不相同 ,就會出現跨域問題,不符合 同源策略 導致的後果有

  • 1、 LocalStorge、SessionStorge、Cookie 等瀏覽器記憶體無法跨域存取

  • 2、 DOM節點 無法跨域操作

  • 3、 Ajax請求 無法跨域請求

  • 註意點:一個IP是可以註冊多個不同網域名稱的,也就是多個網域名稱可能指向同一個IP,即使是這樣,他們也不符合 同源策略

    螢幕擷取2021-10-01 上午9.02.55.png

    跨域的時機?

    跨域發生在什麽時候呢?我考過很多位同學,得到了兩種答案

  • 1、請求一發出就被瀏覽器的跨域報錯攔下來了(大多數人回答)

  • 2、請求發出去到後端,後端返回數據,在瀏覽器接收後端數據時被瀏覽器的跨域報錯攔下來

  • 那到底是哪種呢?我們可以驗證下,咱們先 npm i nodemon -g ,然後建立一個 index.js ,然後 nodemon index 起一個node服務

    // index.js http://127.0.0.1:8000
    const http = require('http');
    const port = 8000;
    http.createServer(function (req, res{
    const { query } = urllib.parse(req.url, true);
    console.log(query.name)
    console.log('到後端嘍')
    res.end(JSON.stringify('林三心'));
    }).listen(port, function () {
    console.log('server is listening on port ' + port);
    })

    再建立一個 index.html ,用來寫前端的請求程式碼,咱們就寫一個簡單的 AJAX請求

    // index.html http://127.0.0.1:5500/index.html
    <script>
    //步驟一:建立異步物件
    var ajax = new XMLHttpRequest();
    //步驟二:設定請求的url參數,參數一是請求的型別,參數二是請求的url,可以帶參數
    ajax.open('get''http://127.0.0.1:8000?name=前端過來的林三心');
    //步驟三:發送請求
    ajax.send();
    //步驟四:註冊事件 onreadystatechange 狀態改變就會呼叫
    ajax.onreadystatechange = function () {
    if (ajax.readyState == 4 && ajax.status == 200) {
    //步驟五 如果能夠進到這個判斷 說明 數據 完美的回來了,並且請求的頁面是存在的
    console.log(ajax.responseText);//輸入相應的內容
    }
    }
    </script>

    螢幕擷取2021-10-01 下午1.37.01.png

    最終,前端確實是跨域報錯了。但這不是結果,我們要想知道是哪一個答案,關鍵在於看後端的node服務那裏有沒有輸出,就一目了然了。所以,答案2才是對的。

    螢幕擷取2021-10-01 下午1.38.52.png

    螢幕擷取2021-10-01 下午1.41.51.png

    同域情況 && 跨域情況?

    前面提到了 同源策略 ,滿足 協定號-網域名稱-埠號 這三者 都相同 就是 同域 ,反之就是 跨域 ,會導致 跨域報錯 ,下面透過幾個例子讓大家鞏固一下對 同域和跨域 的認識把!

    螢幕擷取2021-10-01 上午9.24.38.png

    解決跨域的方案

    跨域其實是一個很久的問題了,對應的解決方案也有很多,一起接著往下讀吧!!!

    JSONP

    前面咱們說了,因為瀏覽器 同源策略 的存在,導致存在 跨域問題 ,那有沒有不受 跨域問題 所束縛的東西呢?其實是有的,以下這三個標簽 載入資源路徑 是不受束縛的

  • 1、script標簽: <script src="載入資源路徑"></script>

  • 2、link標簽: <link herf="載入資源路徑"></link>

  • 3、img標簽: <img src="載入資源路徑"></img>

  • 而JSONP就是利用了 script src 載入不受束縛,從而可以擁有從 不同的域 拿到數據的能力。但是JSONP需要前端後端配合,才能實作最終的 跨域獲取數據

    JSONP通俗點說就是:利用script的src去發送請求,將一個方法名 callback 傳給後端,後端拿到這個方法名,將所需數據,透過字串拼接成新的字串 callback(所需數據) ,並行送到前端,前端接收到這個字串之後,就會自動執行方法 callback(所需數據) 。老規矩,先上圖,再上程式碼。

    螢幕擷取2021-10-01 下午1.22.08.png

    後端程式碼

    // index.js http://127.0.0.1:8000
    const http = require('http');
    const urllib = require('url');
    const port = 8000;
    http.createServer(function (req, res{
    const { query } = urllib.parse(req.url, true);
    if (query && query.callback) {
    const { name, age, callback } = query
    const person = `${name}今年${age}歲啦!!!`
    const str = `${callback}(${JSON.stringify(person)})`// 拼成callback(data)
    res.end(str);
    else {
    res.end(JSON.stringify('沒東西啊你'));
    }
    }).listen(port, function () {
    console.log('server is listening on port ' + port);
    })

    前端程式碼

    // index.html http://127.0.0.1:5500/index.html
    const jsonp = (url, params, cbName) => {
    returnnewPromise((resolve, reject) => {
    const script = document.createElement('script')
    window[cbName] = (data) => {
    resolve(data)
    document.body.removeChild(script)
    }
    params = { ...params, callback: cbName }
    const arr = Object.keys(params).map(key =>`${key}=${params[key]}`)
    script.src = `${url}?${arr.join('&')}`
    document.body.appendChild(script)
    })
    }
    jsonp('http://127.0.0.1:8000', { name'林三心'age23 }, 'callback').then(data => {
    console.log(data) // 林三心今年23歲啦!!!
    })

    螢幕擷取2021-10-01 下午1.47.29.png

    JSONP的缺點就是,需要前後端配合,並且只支持 get請求方法

    WebSocket

    WebSocket是什麽東西?其實我也不怎麽懂,但是我也不會像別人一樣把MDN的資料直接復制過來,因為復制過來相信大家也是看不懂的。

    我理解的WebSocket是一種協定(跟http同級,都是協定),並且他可以進行跨域通訊,為什麽他支持跨域通訊呢?我這裏找到一篇文章WebSocket憑啥可以跨域?,講的挺好

    螢幕擷取2021-10-01 下午10.02.39.png

    後端程式碼

    先安裝 npm i ws

    // index.js http://127.0.0.1:8000
    const Websocket = require('ws');
    const port = 8000;
    const ws = new Websocket.Server({ port })
    ws.on('connection', (obj) => {
    obj.on('message', (data) => {
    data = JSON.parse(data.toString())
    const { name, age } = data
    obj.send(`${name}今年${age}歲啦!!!`)
    })
    })

    前端程式碼

    // index.html http://127.0.0.1:5500/index.html

    functionmyWebsocket(url, params{
    returnnewPromise((resolve, reject) => {
    const socket = new WebSocket(url)
    socket.onopen = () => {
    socket.send(JSON.stringify(params))
    }
    socket.onmessage = (e) => {
    resolve(e.data)
    }
    })
    }
    myWebsocket('ws://127.0.0.1:8000', { name'林三心'age23 }).then(data => {
    console.log(data) // 林三心今年23歲啦!!!
    })

    結果如下

    螢幕擷取2021-10-01 下午1.47.29.png

    Cors

    Cors,全稱是 Cross-Origin Resource Sharing ,意思是 跨域資源共享 ,Cors一般是由後端來開啟的,一旦開啟,前端就可以跨域存取後端。

    為什麽後端開啟Cors,前端就能跨域請求後端呢?我的理解是:前端跨域存取到後端,後端開啟Cors,發送 Access-Control-Allow-Origin: 網域名稱 欄位到前端(其實不止一個),前端瀏覽器判斷 Access-Control-Allow-Origin 的網域名稱如果跟前端網域名稱一樣,瀏覽器就不會實行跨域攔截,從而解決跨域問題。

    螢幕擷取2021-10-01 下午6.41.11.png

    後端程式碼

    // index.js http://127.0.0.1:8000
    const http = require('http');
    const urllib = require('url');
    const port = 8000;
    http.createServer(function (req, res{
    // 開啟Cors
    res.writeHead(200, {
    //設定允許跨域的網域名稱,也可設定*允許所有網域名稱
    'Access-Control-Allow-Origin''http://127.0.0.1:5500',
    //跨域允許的請求方法,也可設定*允許所有方法
    "Access-Control-Allow-Methods""DELETE,PUT,POST,GET,OPTIONS",
    //允許的header型別
    'Access-Control-Allow-Headers''Content-Type'
    })
    const { query: { name, age } } = urllib.parse(req.url, true);
    res.end(`${name}今年${age}歲啦!!!`);
    }).listen(port, function () {
    console.log('server is listening on port ' + port);
    })

    前端程式碼

    // index.html http://127.0.0.1:5500/index.html
    //步驟一:建立異步物件
    var ajax = new XMLHttpRequest();
    //步驟二:設定請求的url參數,參數一是請求的型別,參數二是請求的url,可以帶參數
    ajax.open('get''http://127.0.0.1:8000?name=林三心&age=23');
    //步驟三:發送請求
    ajax.send();
    //步驟四:註冊事件 onreadystatechange 狀態改變就會呼叫
    ajax.onreadystatechange = function () {
    if (ajax.readyState == 4 && ajax.status == 200) {
    //步驟五 如果能夠進到這個判斷 說明 數據 完美的回來了,並且請求的頁面是存在的
    console.log(ajax.responseText);//輸入相應的內容
    }
    }

    結果如下

    螢幕擷取2021-10-01 下午1.47.29.png

    螢幕擷取2021-10-01 下午7.10.57.png

    Node介面代理

    還是回到 同源策略 ,同源策略它只是瀏覽器的一個策略而已,它是限制不到後端的,也就是 前端-後端 會被同源策略限制,但是 後端-後端 則不會被限制,所以可以透過Node介面代理,先存取已設定Cors的後端1,再讓後端1去存取後端2獲取數據到後端1,後端1再把數據傳到前端

    螢幕擷取2021-10-01 下午8.46.28.png

    後端2程式碼

    // index.js http://127.0.0.1:8000
    const http = require('http');
    const urllib = require('url');
    const port = 8000;
    http.createServer(function (req, res{
    console.log(888)
    const { query: { name, age } } = urllib.parse(req.url, true);
    res.end(`${name}今年${age}歲啦!!!`)
    }).listen(port, function () {
    console.log('server is listening on port ' + port);
    })

    建立一個 index2.js ,並 nodemon index2.js

    後端1程式碼

    // index2.js http://127.0.0.1:8888
    const http = require('http');
    const urllib = require('url');
    const querystring = require('querystring');
    const port = 8888;
    http.createServer(function (req, res{
    // 開啟Cors
    res.writeHead(200, {
    //設定允許跨域的網域名稱,也可設定*允許所有網域名稱
    'Access-Control-Allow-Origin''http://127.0.0.1:5500',
    //跨域允許的請求方法,也可設定*允許所有方法
    "Access-Control-Allow-Methods""DELETE,PUT,POST,GET,OPTIONS",
    //允許的header型別
    'Access-Control-Allow-Headers''Content-Type'
    })
    const { query } = urllib.parse(req.url, true);
    const { methods = 'GET', headers } = req
    const proxyReq = http.request({
    host'127.0.0.1',
    port'8000',
    path`/?${querystring.stringify(query)}`,
    methods,
    headers
    }, proxyRes => {
    proxyRes.on('data', chunk => {
    console.log(chunk.toString())
    res.end(chunk.toString())
    })
    }).end()
    }).listen(port, function () {
    console.log('server is listening on port ' + port);
    })

    前端程式碼

    // index.html http://127.0.0.1:5500
    //步驟一:建立異步物件
    var ajax = new XMLHttpRequest();
    //步驟二:設定請求的url參數,參數一是請求的型別,參數二是請求的url,可以帶參數,動態的傳遞參數starName到伺服端
    ajax.open('get''http://127.0.0.1:8888?name=林三心&age=23');
    //步驟三:發送請求
    ajax.send();
    //步驟四:註冊事件 onreadystatechange 狀態改變就會呼叫
    ajax.onreadystatechange = function () {
    if (ajax.readyState == 4 && ajax.status == 200) {
    //步驟五 如果能夠進到這個判斷 說明 數據 完美的回來了,並且請求的頁面是存在的
    console.log(ajax.responseText);//輸入相應的內容
    }
    }

    結果如下

    螢幕擷取2021-10-01 下午1.47.29.png

    Nginx

    其實 Nginx Node介面代理 是一個道理,只不過Nginx就不需要我們自己去搭建一個中間服務

    螢幕擷取2021-10-01 下午8.47.40.png

    先下載 nginx [1] ,然後將nginx目錄下的nginx.conf修改如下:

    server{
    listen 8888;
    server_name 127.0.0.1;
    location /{
    proxy_pass 127.0.0.1:8000;
    }
    }

    最後透過命令列 nginx -s reload 啟動nginx

    後端程式碼

    // index.js http://127.0.0.1:8000
    const http = require('http');
    const urllib = require('url');
    const port = 8000;
    http.createServer(function (req, res{
    const { query: { name, age } } = urllib.parse(req.url, true);
    res.end(`${name}今年${age}歲啦!!!`);
    }).listen(port, function () {
    console.log('server is listening on port ' + port);
    })

    前端程式碼

    // index.html http://127.0.0.1:5500
    //步驟一:建立異步物件
    var ajax = new XMLHttpRequest();
    //步驟二:設定請求的url參數,參數一是請求的型別,參數二是請求的url,可以帶參數,動態的傳遞參數starName到伺服端
    ajax.open('get''http://127.0.0.1:8888?name=林三心&age=23');
    //步驟三:發送請求
    ajax.send();
    //步驟四:註冊事件 onreadystatechange 狀態改變就會呼叫
    ajax.onreadystatechange = function () {
    if (ajax.readyState == 4 && ajax.status == 200) {
    //步驟五 如果能夠進到這個判斷 說明 數據 完美的回來了,並且請求的頁面是存在的
    console.log(ajax.responseText);//輸入相應的內容
    }
    }

    結果如下

    螢幕擷取2021-10-01 下午1.47.29.png

    postMessage

    場景: http://127.0.0.1:5500/index.html 頁面中使用了 iframe標簽 內嵌了一個 http://127.0.0.1:5555/index.html 的頁面

    雖然這兩個頁面存在於一個頁面中,但是需要 iframe標簽 來巢狀才行,這兩個頁面之間是無法進行通訊的,因為他們 埠號 不同,根據 同源策略 ,他們之間存在 跨域問題

    那應該怎麽辦呢?使用 postMessage 可以使這兩個頁面進行通訊

    螢幕擷取2021-10-01 下午9.28.53.png

    // http:127.0.0.1:5500/index.html
    <body>
    <iframesrc="http://127.0.0.1:5555/index.html"id="frame"></iframe>
    </body>
    <script>
    document.getElementById('frame').onload = function () {
    this.contentWindow.postMessage({ name: '林三心', age: 23 }, 'http:/
    /127.0.0.1:5555')
    window.onmessage = function (e) {
    console.log(e.data) /
    / 林三心今年23歲啦!!!
    }
    }
    </
    script>

    // http://127.0.0.1:5555/index.html
    <script>
    window.onmessage = function (e{
    const { data: { name, age }, origin } = e
    e.source.postMessage(`${name}今年${age}歲啦!!!`, origin)
    }
    </script>

    document.domain && iframe

    場景: a.sanxin.com/index.html b.sanxin.com/index.html 之間的通訊

    其實上面這兩個正常情況下是無法通訊的,因為他們的 網域名稱 不相同,屬於跨域通訊

    那怎麽辦呢?其實他們有一個共同點,那就是他們的二級網域名稱都是 sanxin.com ,這使得他們可以透過 document.domain && iframe 的方式來通訊

    螢幕擷取2021-10-01 下午9.58.55.png

    由於本菜鳥暫時沒有伺服器,所以暫時使用本地來模擬

    // http://127.0.0.1:5500/index.html
    <body>
    <iframesrc="http://127.0.0.1:5555/index.html"id="frame"></iframe>
    </body>
    <script>
    document.domain = '127.0.0.1'
    document.getElementById('frame').onload = function () {
    console.log(this.contentWindow.data) /
    / 林三心今年23歲啦!!!
    }
    </
    script>

    // http://127.0.0.1:5555/index.html
     <script>
    // window.name="林三心今年23歲啦!!!"
    document.domain = '127.0.0.1'
    var data = '林三心今年23歲啦!!!';
    </script>

    結果如下

    螢幕擷取2021-10-01 下午1.47.29.png

    結語

    如果你覺得此文對你有一丁點幫助,點個贊,鼓勵一下林三心哈哈。或者可以加入我的摸魚群,想進學習群,摸魚群,請加我的vx,我會定時直播模擬面試,答疑解惑