跨域及解决方案

363 阅读3分钟

前言

在前后端交互中,经常会碰到请求跨域,那什么是跨域呢,以及如何解决跨域,本文将进行探讨分享。

什么是跨域

因为浏览器的同源策略,非同源的情况下即会产生跨域现象。会导致跨域的场景有 当 URL 的协议、主域名、子域名、端口中任意一个不相同的时候,就会产生跨域

跨域其实请求发送出去了,并且服务器也进行响应了,但是浏览器拦截了返回结果。

跨域的解决方案

(1)jsonp

原理:利用 script 标签没有跨域限制的特性,向服务器发送请求。它只支持 GET 方法。

实现流程

  • 封装一个函数名为 jsonp 的方法,接收 url、params、callback 三个参数,返回一个 promise 。
  • 内部实现,首先在 window 上创建一个跟传入的回调函数名一样的函数,用于接收服务器返回的数据,并且在接收完后,把动态创建的 script 删除,把 window 上挂载的此方法设为 null
  • 创建一个 script 标签,把传入的url、参数和回调函数,拼接成 script 的 src
  • 最后服务器处理,并返回一个以传入的回调函数名的方法,并把需要传递给客户端的数据,作为函数的参数。
客户端实现
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style></style>
  </head>
  <body>
    <button onclick="send()">发送jsonp</button>
    <script>
      function jsonp({ url, params, callback = 'show' }) {
        return new Promise((resolve, reject) => {
          const script = document.createElement('script')
          // 创建回调函数
          window[callback] = function (res) {
            resolve(res)
            document.body.removeChild(script)
            window[callback] = null
          }
          // 处理参数
          params = {
            callback,
            ...params,
          }

          let arr = []
          for (const key in params) {
            arr.push(`${key}=${params[key]}`)
          }
          script.src = `${url}?${arr.join('&')}`
          document.body.appendChild(script)
        })
      }

      function send() {
        jsonp({
          url: 'http://localhost:2000/say',
          params: {
            id: 1,
          },
        }).then((res) => {
          console.log(res) // {id: 1, name: "sky"}
        })
      }
    </script>
  </body>
</html>

服务端实现

const express = require('express')

const app = express()

app.get('/say', (req, res) => {
  const { callback, id } = req.query
  if (id === '1') {
    let obj = { id: 1, name: 'sky' }
    res.end(`${callback}(${JSON.stringify(obj)})`)
  } else if (id === '2') {
    let obj = { id: 2, name: 'jeeny' }
    res.end(`${callback}(${JSON.stringify(obj)})`)
  } else {
    res.end(`${callback}('不存在此id')`)
  }
})

app.listen(2000)

(2)CORS(跨源资源共享)

实现 CORS 的关键是后端,只要后端配置就能够实现跨域。服务端设置 Access-Control-Allow-Origin 就可以开启 CORS,该属性表示哪些域名可以访问。

(3) postMessage

它可以解决以下2种情况:

  • 页面与嵌套的 iframe 传递消息
父级页面 01.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h2>我是01.html</h2>
    <button onclick="handleMessage()">发送消息</button>
    <iframe
      src="http://localhost:5000/02.html"
      style="width: 300px; height: 300px"
    ></iframe>

    <script>
    // iframe 地址不能用系统文件地址格式,可以开启一个服务
      function handleMessage() {
        const iframe = document.getElementsByTagName('iframe')[0]
        iframe.contentWindow.postMessage(
          '我是发送到02页面的内容',
          'http://localhost:5000/02.html'
        )
      }
      window.addEventListener('message', (res) => {
        // 接收其他页面传过来的内容
        console.log(res)
      })
    </script>
  </body>
</html>

子页面 02.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h2>我是02.html</h2>
    <button onclick="handleMessage()">发送消息</button>

    <script>
      function handleMessage() {
        window.parent.postMessage('返回给01页面的消息')
      }
      window.addEventListener('message', (res) => {
        // 接收其他页面传过来的内容
        console.log(res)
      })
    </script>
  </body>
</html>

  • 页面与通过 window.open 打开的页面通信,注意:父页面向子页面发送消息时,需子页面已经被 window.open 打开,或者打开的同时,设置一个异步,延迟100ms左右再发送消息
父页面01.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h2>我是01.html</h2>
    <button onclick="handleMessage()">发送消息</button>

    <script>
      function handleMessage() {
        const iframe = window.open('http://localhost:5000/02.html')
        setTimeout(() => {
          iframe.postMessage('我是发送到02页面的内容')
        }, 100)
      }
      window.addEventListener('message', (res) => {
        // 接收其他页面传过来的内容
        console.log(res)
      })
    </script>
  </body>
</html>

子页面 02.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h2>我是02.html</h2>
    <button onclick="handleMessage()">发送消息</button>

    <script>
      function handleMessage() {
        window.opener.postMessage('返回给01页面的消息')
      }
      window.addEventListener('message', (res) => {
        // 接收其他页面传过来的内容
        console.log(res)
      })
    </script>
  </body>
</html>

(4)通过 nginx 反向代理

// proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;
    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

(5)通过 node 中间件代理

(6)通过 window.name + iframe,只适用于 iframe

(7)通过 location.hash + iframe,只适用于 iframe

(8)通过 document.domain + iframe

此方式只能用于二级域名,协议、端口相同的情况下,比如 a.baidu.com 和 b.baidu.com,只需要给需要通信的页面加上 document.domain ='baidu.com'

父页面 01.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h2>我是01.html</h2>
    <button onclick="handleMessage()">获取内容</button>
    <iframe
      src="http://localhost:5000/02.html"
      style="width: 300px; height: 300px"
    ></iframe>

    <script>
      document.domain = 'baidu.com'
      function handleMessage() {
        const iframe = document.getElementsByTagName('iframe')[0]
        iframe.contentWindow.a
      }
    </script>
  </body>
</html>

子页面02
<script>
  document.domain = 'baidu.com'
  var a = 3
</script>