1. 什么是跨域?

浏览器有一个同源策略:如果两个 url 的协议、域名、端口三者完全相同,那就称之为同源。

同源之间获取资源是不受限制的,如果不满足同源(即协议、域名、端口有一个条件不同),那么获取资源就会受到限制,此时我们称之为跨域。

总结来说:如果两个url之间需要进行通信,但是不满足同源策略,此时就发生了跨域。

下面是一个例子:(所有url场景取自新浪首页,即url_1)

/*
    url_1: https://www.sina.com.cn/
    url_2: https://www.sina.com.cn/api/hotword.json
    url_3: https://sspapi.zenyou.71360.com/js?i=537&o=2&ran=7160123520
    url_4: https://b.zenyou.71360.com/bid/zhendao
    
    在 url_1 向 url_2 发送一个请求,此时不发生跨域
    在 url_1 向 url_3 发送一个请求,此时产生跨域问题,因为 url_1 url_3 之间域名不同
    在 url_1 向 url_4 发送一个请求,此时产生跨域问题,因为 url_1 url_4 之间域名不同
*/

2. HTTP 请求的理解

要了解跨域, 首先要知道HTTP 协议的两个概念: 简单请求和预检请求

2.1 简单请求

某些请求不会触发cors预检请求,这样的请求称之为简单请求,值得注意的是,该术语并不属于fetch规范;

对于简单请求,浏览器直接发出CORS请求,具体来说,就是在头信息之中,增加一个Origin字段;在返回头信息中,增加一个Access-Control-Allow-Origin: * 字段(后端不作处理时)

简单请求并不会触发跨域, 只有非简单请求才会触发跨域.

满意所有下述条件,则该请求可视为简单请求:

1. 使用下列方法
 1.1 GET
    1.2 HEAD
    1.3 POST
2. HTTP头部只包含如下字段(这些字段统称为对 CORS 安全的首部字段集合)
 2.1 Accept
    2.2 Accept-Language
    2.3 Content-Language
    2.4 Content-Type (需要注意额外的限制)
    2.5 DPR
    2.6 Downlink
    2.7 Save-Data
    2.8 Viewport-Width
    2.9 Width
3. Content-Type 的值仅限于下列三者之一:
 3.1 text/plain
 3.2 multipart/form-data
 3.3 application/x-www-form-urlencoded
4. 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器
5. 请求中没有使用 ReadableStream 对象

2.2 非简单请求

非简单请求, 就是字面上的意思; 需要注意的是, 开发过程中基本上所有的请求都是非简单请求. 在浏览器发送非简单请求时, 并不是直接发送请求到服务器, 而是会先发送一个预检请求到服务器.

当一个请求满足下列任意一个条件时,它会变成非简单请求(即该请求会先发送一个options请求到服务器)

1. 使用下列方法
 1.1 PUT
    1.2 DELETE
    1.3 CONNECT
    1.4 OPTIONS
    1.5 TRACE
    1.6 PATCH
2. 人为设置了“对CORS 安全的首部字段集合”之外的其他首部字段(这些字段统称为对 CORS 安全的首部字段集合)
 2.1 Accept
    2.2 Accept-Language
    2.3 Content-Language
    2.4 Content-Type (需要注意额外的限制)
    2.5 DPR
    2.6 Downlink
    2.7 Save-Data
    2.8 Viewport-Width
    2.9 Width
3. Content-Type 的值不属于下列三者之一:
 3.1 text/plain
 3.2 multipart/form-data
 3.3 application/x-www-form-urlencoded
4. 请求中的XMLHttpRequestUpload 对象注册了任意多个事件监听器
5. 请求中使用了ReadableStream对象

2.3 预检请求

预检请求, 即option 请求; 他表示当浏览器需要向服务器发送非简单请求时, 提前发送的一个请求.

预检请求的作用是确认服务器与浏览器之间是否能够进行通信, 以及为后续的请求做准备

PS: 对于预检请求, 这里只知道在跨域问题中可以用于确认一个请求是否发生了跨域, 至于在其他方面的应用, 暂时还没有接触过.

3. 跨域演示

express(app.js):

const express = require('express')

const log = console.log.bind(console)
const app = express()

app.get('/helloworld', (request, response) => {
    response.send('hello')
})

const main = () => {
    let server = app.listen(2300, () => {
        let host = server.address().address
        let port = server.address().port

        log(`应用实例,访问地址为 http://${host}:${port}`)
    })
}

if (require.main === module) {
    main()
}

html(crossOriginDemo.html):

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>跨域demo</title>
    <style type="text/css">
        .ajaxButton {
            width: 100px;
            height: 50px;
            background: blue;
            color: #fff;
            display: flex;
            justify-content: center;
            align-items: center;
        }
    </style>
</head>
<body>
    <div class="ajaxButton">发送请求</div>
    <script>
        const log = console.log.bind(console)

        const ajax = (method, url, data, headers, callback) => {
            let r = new XMLHttpRequest()
            r.open(method, url, true)

            // 设置 headers
            Object.entries(headers).forEach(([k, v]) => {
                r.setRequestHeader(k, v)
            })
            r.onreadystatechange = () => {
                if (r.readyState === 4) {
                    callback(r.response)
                }
            }
            if (method === 'POST') {
                data = JSON.stringify(data)
            }
            r.send(data)
        }
        
        let ele = document.querySelector('.ajaxButton')
        ele.addEventListener('click', function () {
            let url = 'http://localhost:2300/helloworld'
            let method = 'GET'
            let data = {}
            let headers = {
                'Content-Type': 'application/json',
            }
            ajax(method, url, data, headers, (r) => {
                log('cors r is', r)
            })
        })
    </script>
</body>
</html>

跨域原因:

由于页面使用的端口号是63342, 而服务器 设置的端口号是2300, 这破坏了同源策略, 因此发生了跨域, 不能够正常请求到数据.

点击页面按钮之后, 可以看到跨域提示:

4. 跨域的解决方案

  1. cors 模块: https://www.cnblogs.com/oulae/p/12784186.html
  2. json: https://www.cnblogs.com/oulae/p/12784187.html
  3. webpack devServer 代理: https://www.cnblogs.com/oulae/p/12784188.html
  4. 自定义node 转发: https://www.cnblogs.com/oulae/p/12784189.html
  5. nginx
  6. postMessage
  7. iframe

5. 跨域demo

跨域Demo演示

6. 测试/生产环境中的跨域问题

  1. webpack dev Server(develop server)
  2. nginx
  3. cors
    上面的这三个内容是在web 项目中常见的跨域解决方案, 但是, 如果webpack 使用webpack dev Server 解决跨域, 那么这个解决方式只能在本地运行时解决跨域问题, 如果后端在部署项目的时候没有部署webpack dev server(即没有使用在服务器建一个中转服务器, 并配置dev server), 那么还是会有跨域问题; webpack dev Server 是前端跨域解决方案, 但是不适用于生产环境和测试环境, 只适用于本机

cors 模块的配置和nginx 的配置一般是后端完成的.

因此测试/生产环境中的跨域问题, 大多数情况下都是后端去解决.

7. 参考链接

  1. MDN
  2. 跨域资源共享 CORS 详解
  3. 项目地址