面试不要再问我跨域有多少种方式?10种带案例效果奉上~

241 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

up-2cb64604b75a777864a110b97e88c361839.png

一、什么是同域?

协议(http/https,tcp,udp,ftp等),域名(www.baidu.com,moblie.baidu.com), 端口(8080,8081,443)等全相同才是同域,一个不同即跨域。

二、浏览器哪些不支持跨域

  • cookie、localStorage、sessionStorage
  • Dom元素也有同源策略 iframe引用其他页面的行为
  • ajax也默认不支持跨域(代理,加Auth的header头等方法解决)

三、实现跨域的方式

  • Jsonp
  • cors(后端提供)
  • postMessage(两个页面,或原生App内嵌H5)
  • window.name
  • location.hash
  • http-proxy
  • nginx
  • websocket

四、栗子🌰

4.1 JSONP

Jsonp就是通过script标签去引入的js,平常打开百度,加载了很多js,这就是jsonp实现跨域的手段:

image.png

image.png 如上图所示,www.baidu.com和js地址并不在同一个域, 通过script标签可以引入资源。

通过百度搜索,抓包到一个接口 www.baidu.com/sugrec?pre=…

这是搜索一个“1”,百度返回的数据

image.png

注意看wd(搜索关键词word)和cb(callback回调方法)是关键,我们把cb改成show,返回的就是show方法了

image.png

基于百度的接口我们自己封装一个jsonp方法

<!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></body>
  <script>
    function jsonp({ url, params, cb }) {
      return new Promise((resolve, reject) => {
        const script = document.createElement('script')
        window[cb] = function (data) {
          resolve(data)
          document.body.removeChild(script)
        }
        const totalParams = { ...params, cb }
        const paramsArr = []
        for (const key in totalParams) {
          paramsArr.push(`${key}=${totalParams[key]}`)
        }
        script.src = `${url}&${paramsArr.join('&')}`
        document.body.appendChild(script)
      })
    }

    jsonp({
      url: 'https://www.baidu.com/sugrec?pre=1&p=3&ie=utf-8&json=1&prod=pc&from=pc_web&sugsid=36425,36455,36367,34812,36422,36167,35979,36055,26350,36468,36314,36447&req=2&csor=1',
      params: {
        wd: '1',
      },
      cb: 'show',
    }).then(res => {
      console.log(res, '得到的结果')
    })
  </script>
</html>

通过jsonp的方式,在本地域,拿到了百度返回的数据

 注意:1.script写在body之后。 2. createElement等代码都写在peomised的箭头函数中

image.png

缺点: 1.只能发送get请求 不支持 post delete put请求 2.只能发送get请求 不支持 post delete put请求

4.1.1 jsonp通过后端去拿数据

  • 起一个express的服务
let express = require('express')
let app = express()
app.get('/say', function (req, res) {
  let { wd, cb } = req.query
  console.log(wd, 'wd')
  res.end(`${cb}('我不爱你')`)
})

app.listen(3000)

  • 把前端jsonp的代码稍微改造一下
script.src =
          url.indexOf('localhost') > -1
            ? `${url}?${paramsArr.join('&')}`
            : `${url}&${paramsArr.join('&')}`
        document.body.appendChild(script)

 jsonp({
      url: 'http://localhost:3000/say',
      params: {
        wd: '我爱你',
      },
      cb: 'show',
    }).then(res => {
      console.log(res, '得到的结果')
    })

image.png

4.2 cors的方式

我们通过express起一个3000端口的服务,和4000端口的服务,3000端口有个静态页发送Ajax请求到4000端口

server1.js

let express = require('express')
let app = express()
app.use(express.static(__dirname))

app.listen(3000, () => {
  'app is listen to 3000'
})

serve2.js

let express = require('express')
let app = express()

app.get('/getData', function (req, res) {
  console.log(req.headers)
  res.end('我不爱你')
})

app.listen(4000)

ajax请求

<!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>
    <script>
      let xhr = new XMLHttpRequest()
      xhr.open('GET', 'http://localhost:4000/getData', true)
      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
          if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
            console.log(xhr.response)
          }
        }
      }
      xhr.send()
    </script>
  </body>
</html>

image.png

注意: 请求时发送到了服务端,是浏览器判断跨域屏蔽掉了响应结果

image.png

4.2.1 Access-Control-Allow-Origin

在localhost:4000 的node服务,添加请求头的中间件方法

let whiteList = ['http://localhost:3000']

app.use(function (req, res, next) {
  let origin = req.headers.origin
  if (whiteList.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin)
  }
  next()
})

localhost:3000可以访问localhost:4000的服务了

image.png

4.2.2 Access-Control-Allow-Headers

截屏2022-05-28 下午4.25.00的副本.png

image.png

  • 通过设置Access-Control-Allow-Headers, 来设置允许哪些头
res.setHeader('Access-Control-Allow-Origin', origin)
    res.setHeader('Access-Control-Allow-Headers', 'x-token,x-name')

截屏2022-05-28 下午4.30.36.png

4.2.3 Access-Control-Allow-Methods

  • 换成put方法又报跨域错误了

截屏2022-05-28 下午4.36.07.png

  • 老样子,继续设置headers
res.setHeader('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE')
  • ok

image.png

4.2.4 Access-Control-Max-Age

PUT请求实际上是发送了2次,第一次是options,预处理判断是否可以发put,第一次无意义,在后端提前end, 6s内不需要重复预检测

image.png

 res.setHeader('Access-Control-Max-Age', 6000)
    if (req.method === 'OPTIONS') {
      res.end()
    }

现在只发送一次了

4.2.5 Access-Control-Allow-Credentials

xhr如果带cookie,也需要后端设置header去放行

image.png

image.png

res.setHeader('Access-Control-Allow-Credentials', true)

image.png

4.2.6 Access-Control-Expose-Headers

如果想获取服务端的响应头,直接获取会拒绝

console.log(xhr.getResponseHeader('name'))

image.png 接下来继续在服务端设置头

image.png

image.png

4.3 postMessage

起两个serve,两个html,html1,iframe去引用index2,资源加载好后,通过onload,然后postMessage的方式去通信

index1.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>
    <h1>index1.html</h1>
    <iframe src="http://localhost:4000/index2.html" onload="load()" id="frame">
    </iframe>
    <script>
      function load() {
        let frame = document.getElementById('frame')
        frame.contentWindow.postMessage('i love u', 'http://localhost:4000')
      }
      window.onmessage = function (e) {
          console.log(e.data, 'index1.html')
      }
    </script>
  </body>
</html>

index2.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>
    <h3>index2.html</h3>
    <script>
      window.onmessage = function (e) {
        console.log(e.data, 'index2.html')
        e.source.postMessage('i love u too', e.origin)
      }
    </script>
  </body>
</html>

效果

image.png

4.4 window.name

起两个服务,3个页面,有两个页面同域,一个不同域。同域的一个页面iframe去引用不同域的,不同域的把一个属性挂载到window.name上。然后通过改变iframe的src到同域的页面,由于name不会消失,实现了跨域。 a.html, b.html同域。 c不同

a.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>
    <h1>a.html</h1>
    <iframe
      src="http://localhost:4000/c.html"
      onload="load()"
      id="frame"
      frameborder="0"
    >
    </iframe>
    <script>
      let first = true
      function load() {
        if (first) {
          let frame = document.getElementById('frame')
          frame.src = 'http://localhost:3000/b.html'
          first = false
        } else {
          console.log(frame.contentWindow.name)
        }
      }
    </script>
  </body>
</html>

b.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>
    <h3>b.html</h3>
    <script></script>
  </body>
</html>

c.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>
    <h3>c.html</h3>
    <script>
      window.name = 'i love u'
    </script>
  </body>
</html>

image.png

4.5 window.hash

起2个服务,3个页面,2个同域,1个不同。a.html跨域请求c.html。c把hash带到b,b 通过parent.location.hash获得hash, 由于a中监听了hashchange事件。所以实现了跨域 a.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>
    <h1>a.html</h1>
    <iframe
      src="http://localhost:4000/c.html#iloveyou"
      id="frame"
      frameborder="0"
    >
    </iframe>
    <script>
      window.onhashchange = function () {
        console.log(location.hash)
      }
    </script>
  </body>
</html>

b.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>
    <h3>b.html</h3>
    <script>
      window.parent.parent.location.hash = location.hash
    </script>
  </body>
</html>

c.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>
    <h3>c.html</h3>
    <script>
      console.log(location.hash)
      let iframe = document.createElement('iframe')
      iframe.src = 'http://localhost:3000/b.html#iloveutoo'
      document.body.appendChild(iframe)
    </script>
  </body>
</html>

image.png

4.6 window.domain

和window.name实现方式类似

4.7 Websocket

a.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>
    <h1>a.html</h1>

    <script>
      let socket = new WebSocket('ws://localhost:3000')
      socket.onopen = function (e) {
        socket.send('i love u')
      }
      socket.onmessage = function (data) {
        console.log(data, 'a.html')
      }
    </script>
  </body>
</html>

server

let express = require('express')
let app = express()
let WebSocket = require('ws')
let wss = new WebSocket.Server({ port: 3000 })

wss.on('connection', function (ws) {
  ws.on('message', function (data) {
    console.log(JSON.stringify(data), 'DATA')
    ws.send('i love u too')
  })
})

截屏2022-05-29 下午9.40.43.png

4.8 nginx

mac安装nginx, windows直接下载包就好了

# 安装
brew install nginx
# 启动
brew services start nginx
# 打开配置目录
open /usr/local/etc/nginx

image.png

通过加配置,实现跨域,修改nginx.conf,然后restart

 location ~.*\.json {
            root  json;
            add_header "Access-Control-Allow-Origin" "*";
        }

4.9 常用的webpack中的代理

module.exports = {
  proxy: {
        '/apis': {    //将www.exaple.com印射为/apis
            target: 'https://www.exaple.com',  // 接口域名
            secure: false,  // 如果是https接口,需要配置这个参数
            changeOrigin: true,  //是否跨域
            pathRewrite: {
                '^/apis': ''   //需要rewrite的,
            }              
        }
  }
}

4.10 谷歌浏览器跨域插件

image.png