跨域问题的多种解决方案

1. jsonp

原理:script标签不受同源策略的影响,把链接挂在script标签上,通过回调函数传递数据
优点:兼容性好,前后端分离
缺点:仅支持get请求,安全性较差,容易引发xss攻击

/* server.js */
const express = require('express');
const app = express();
app.get('/say', (req, res) => {
    let { wd, cb } = req.query;
    console.log('客户端:' + wd);//客户端:Hello
    res.end(`${cb}('服务端:Hi')`)
})
app.listen(3333, () => {
    console.log("Server start http://localhost:3333");
})
<script>
    function jsonp({ url, params, cb }) {
        /* 返回一个Promise */
        return new Promise((resolve) => {
            /* 创建script */
            let script = document.createElement('script');
            /* 全局cb函数 */
            window[cb] = function (data) {
                resolve(data);/* 执行返回的数据 */
                document.body.remove(script);/* 执行完毕删除标签 */
            }
            /* 转换url */
            let arr = [];
            params = { ...params, cb };
            for (let key in params)
                arr.push(`${key}=${params[key]}`);
            //http://localhost:3333/say?wd=Hello&cb=show
            script.src = `${url}?${arr.join('&')}`;
            document.body.appendChild(script);
        })
    }
    jsonp({
        url: 'http://localhost:3333/say',
        params: { wd: 'Hello' },
        cb: 'show'
    }).then(data => console.log(data));//服务端:Hi
</script>

2. CORS

原理:通过在服务端添加白名单,放宽对请求源的限制,从而实现跨域

优点:可以发任意请求
缺点:上是复杂请求的时候得先做一个预检,再发真实的请求,发了两次请求会有性能上的损耗。
3333端口下的indexhtml发出ajax请求

<!-- html在3333端口服务器上 -->
<script>
    let xhr = new XMLHttpRequest;
    /* 设置cookie 需要Access-Control-Allow-Credential设置 */
    document.cookie = "name=aeipyuan";
    xhr.withCredentials = true;
    /* 请求4444端口数据 */
    xhr.open('put', 'http://localhost:4444/getData', true);
    /* 需要设置Access-Control-Allow-Headers */
    xhr.setRequestHeader('name', 'A');
    xhr.send();
    xhr.onreadystatechange = function (e) {
        if (xhr.readyState === 4) {
            if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
                console.log(xhr.response);//4444已收到
                // 需要设置Access-Control-Expose-Headers
                console.log(xhr.getResponseHeader('name'));//mike
            }
        }
    }
</script>

4444端口服务器对数据进行处理

const express = require('express');
const app = express();
/* 设置白名单 */
let writeList = ['http://localhost:3333'];
app.use((req, res, next) => {
    /* 获取请求源 */
    let { origin } = req.headers;
    /* 判断请求源 */
    if (writeList.includes(origin)) {
        /* 允许origin访问 */
        res.setHeader('Access-Control-Allow-Origin', origin)
        /* 允许哪个头 */
        res.setHeader('Access-Control-Allow-Headers', 'name')
        /* 允许哪个方法 */
        res.setHeader('Access-Control-Allow-Methods', 'PUT')
        /* 允许携带cookie */
        res.setHeader('Access-Control-Allow-Credentials', true)
        /* 预检的存活时间 */
        res.setHeader('Access-Control-Allow-Max-Age', 6)
        /* 允许前端获取哪个头 */
        res.setHeader('Access-Control-Expose-Headers', 'name')
        /* OPTIONS请求不做处理 */
        if (req.method === 'OPTIONS') {
            res.end();
        }
    }
    next();
})
app.put('/getData', (req, res) => {
    console.log(req.headers);
    res.setHeader('name', 'B');
    res.send('4444已收到');
})
app.listen(4444, () => {
    console.log("Server start http://localhost:4444");
})

3. postMessage实现跨域

原理:将另一个域的网页放到iframe中,利用postMessage进行传值
3333端口下的a.html

<div>AAAAAAA</div>
<iframe id="frame" src="http://localhost:4444/b.html" frameborder="0" onload="load()"></iframe>
<script>
    function load() {
        frame.contentWindow.postMessage(
            'Hello', 'http://localhost:4444/b.html'
        )
    }
    window.onmessage = function (e) {
        console.log('B说:' + e.data);//B说:Hi
    }
</script>

4444端口下的b.html

<div>BBBBBB</div>
<script>
    window.onmessage = function (e) {
        console.log('A说:' + e.data);//A说:Hello
        e.source.postMessage('Hi', e.origin);//给A发送消息
    }
</script>

4. window.name传值

原理:先用iframe的window.name存储跨域页面要传入的数据,然后将iframe的src属性改变为同源src,实现获取name存储的值

举例:
A,B页面在3333端口下,C页面在4444端口下,目标是实现a页面获取c页面数据
第一步,A页面用iframe标签引入C页面
第二步,C页面设置window.name=数据
第三步,将iframe的src由C页面切换为B页面(同源)
第四步,获取iframe页面的window.name属性
<!-- a.html -->
<iframe src="http://localhost:8082/c.html" frameborder="10" onload="load()" id="frame"></iframe>
<script>
    let first = true;
    function load() {
        if (first) {
            let frame = document.getElementById('frame');
            frame.src = "http://localhost:8081/b.html";//切换src
            first = false;
        } else {
            console.log(frame.contentWindow.name)
        }
    }
</script>
<!-- c.html -->
<script>
    window.onload = function () {
        window.name = "传给A的数据"
    }
</script>

5. hash传值

原理:和window.name相似,A使用iframe引入C并给C传hash值,C使用iframe引入B并给B传hash值,B和A同源,所以把hash值赋给A,A监听到hash变化输出hash值

<!-- a.html -->
<iframe id="frame" src="http://localhost:4444/c.html#A2C" frameborder="0"></iframe>
<script>
    window.onhashchange = function () {
        console.log('C传入数据:' + location.hash)//C传入数据:#C2B2A
    }
</script>
<!-- b.html -->
<script>
    window.parent.parent.location.hash = location.hash;/* 将hash传给A */
</script>
<!-- c.html -->
<script>
    console.log('A传入的数据:' + location.hash);//A传入的数据:#A2C
    /* 创建iframe */
    let iframe = document.createElement('iframe');
    iframe.src = "http://localhost:3333/b.html#C2B2A";
    document.body.appendChild(iframe);
</script>

6. Websocket

原理:Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了
开启服务

/* server.js */
let WebSocket = require('ws');
/* 创建服务 */
let wss = new WebSocket.Server({ port: 3333 });
wss.on('connection', ws => {
    ws.on('message', e => {
        console.log(e);//前端数据
        ws.send('后台数据');
    })
})

传送数据

<script>
    let socket = new WebSocket('ws://localhost:3333');
    socket.onopen = function () {
        socket.send('前端数据');
    }
    socket.onmessage = ({ data }) => {
        console.log(data);//后台数据
    }
</script>

7. domain实现跨域

不同的页面可能放在不同的服务器上,这些服务器域名不同,但是拥有相同的上级域名,比如id.qq.com、www.qq.com、user.qzone.qq.com,它们都有公共的上级域名qq.com ,设置页面documen.domain为上级域名即可实现跨域

<!-- http://a.aeipyuan.cn:3333/a.html  -->
<div>AAAAA</div>
<iframe id="frame" src="http://b.aeipyuan.cn:4444/b.html" frameborder="0" onload="load()"></iframe>
<script>
    document.domain = "aeipyuan.cn";
    function load() {
        console.log('b页面数据:' + frame.contentWindow.a);//b页面数据:100
    }
</script>
<!-- http://b.aeipyuan.cn:4444/b.html -->
<div>BBBBB</div>
<script>
    document.domain = "aeipyuan.cn";
    window.a = 100;
</script>

8. nginx实现

在conf文件配置以下参数,了解较浅,日后补充

location / {  
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
    if ($request_method = 'OPTIONS') {
        return 204;
    }
} 

9. webpack配置实现跨域

//方式1  webpack.config.js
devServer: {
    port: 8081,
    contentBase: './build',
    hot: true,
    proxy: {
        '/api': {
            target: 'http://localhost:8888',//目标域
            pathRewrite: { '/api': '' }/* 路径重写 */
        }
    }
}
//方式2 server.js 直接在8888端口访问webpack打包文件
let express = require('express');
let app = express();
/* webpack */
let webpack = require('webpack');
let config = require('../webpack.config.js');
let compiler = webpack(config);
//中间件
let middle = require('webpack-dev-middleware');
app.use(middle(compiler));
/* 请求 */
app.get('/user', (req, res) => {
    res.json({ name: "aeipyuan" })
})
app.listen(8888);