前端面试必问难题跨域问题的深入思考

401 阅读11分钟

跨域问题真的是前端招聘的一个必问考点,本人在最近的字节笔试中也遇到了这个问题,之前一直从其他人的博客中学习和理解跨域,今天觉得有必要自己在此做一下复盘,更好地理解消化这部分知识点。本篇文章我将对目前可行的所有跨域方法做介绍,原理,简单的实现方法以及优缺点做全方面的论述,如果对你有帮助的话,欢迎点赞留言。

跨域问题的出现源于浏览器的同源策略,同源策略是浏览器的安全机制,它规定了在某一个源的文件应该在哪一个范围访问另一个源的文件。具体地说,它要求不同源资源的协议、域名、端口号三者必须都要一致,任何一个有不同则不允许访问资源。这种机制可以有效防止恶意文件的攻击比如XSS,CSRF等,但同样也给正常的资源访问需求带来了麻烦,因为前后端资源请求非常频繁,同源机制限制的行为主要有:cookie, indexDB, localStorage无法读取;DOM和JS对象无法读取;AJAX请求发送后相应结果被浏览器拦截了三种。那么我们在实际应用场景中怎么实现跨域获取资源呢?目前我们有以下9种方法实现跨域,下面让我们一起来学习一下吧,但要注意的是协议、端口号不同造成的跨域前端是解决不了的,我们能解决的是域名不同导致的跨域问题。

跨域的分析和判断

http://www.withtimesgo.com/a.js
http://www.withtimesgo.com/b.js
http://www.withtimesgo.com/images/house.png
以上三种都是可以访问的,不涉及跨域,因为协议、域名和端口号都一致,只是资源的路径不同而已。

http://www.withtimesgo.com:8000/a.js
http://www.withtimesgo.com/b.js
这两种就涉及跨域了,因为端口不一致,Http默认端口为80端口

http://www.withtimesgo.com/a.js
https://www.withtimesgo.com/b.js
这同样涉及到跨域,因为协议不一样

http://www.withtiemsgo.com/a.js
http://192.168.4.11/b.js
第二个使用了IP表示,虽然ip对应该域名,但是依然涉及到跨域,因为域名不同

http://www.withtimesgo.com/a.js
http://subdomain.withtimesgo.com/b.js
http://withtimesgo.com/c.js
主域相同,但是子域不同,所以涉及到跨域

http://www.withtimesgo.com/a.js
http://www.juejin.com/b.js
域名不同,所以涉及到跨域

跨域解决方案

  • document.domain+iframe
  • location.hash+iframe
  • window.name+iframe
  • posetMessage
  • JSONP
  • CORS
  • nginx代理
  • nodejs中间件代理
  • websocket协议跨域

document.domain + iframe

原理:两个页面通过设置document.domain为基础主域,从而实现同域。 这种跨域方式仅限于主域相同,子域不同的场景中,这既是它的优点(简单易用)也是它的局限性(功能受限)。 实现:

// 父页
<iframe src="http://child.withtimsgo.com/b.html"></iframe>
<script>
    document.domain = 'withtimesgo.com';
    var count = 0;
</script>

子页
<script>
    document.domain = 'withtimesgo.com';
    console.log(window.parent.count);
</script>

location.hash + iframe

原理:A页面和B页面不同域,但需要相互访问资源,我们可以让他们通过第三方C页辅助实现。不同域的页面间使用location.hash传值,同域之间直接用js访问即可。这里的C页需要和A/B之中的某一个页面同域才可以,假定C页和A页同域,那么A和B之间只能通过location.hash值通信,B和C也是如此,但是C可以通过parent.parent访问A页面的所有对象。

讲到这里,我们很容易产生一个疑问,location.hash是什么东西啊?这里引用一下Damonare的文章简单介绍一下,感兴趣的朋友可以自己深入学习这部分内容。因为父窗口可以对iframe进行URL读写,iframe也可以读写父窗口的URL,URL有一部分被称为hash,就是#号及其后面的字符,它一般用于浏览器锚点定位,Server端并不关心这部分,应该说HTTP请求过程中不会携带hash,所以这部分的修改不会产生HTTP请求,但是会产生浏览器历史记录。此方法的原理就是改变URL的hash部分来进行双向通信。每个window通过改变其他 window的location来发送消息(由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于父窗口域名下的一个代理iframe),并通过监听自己的URL的变化来接收消息。

实现:

  1. www.withtimesgo1.com/a.html
<iframe id="iframe" src="http://www.withtimesgo2.com/b.html"></iframe>
<script>
    var iframe = document.getElementById('iframe');
    
    //向b.html传hashsetTimeout(function(){
        iframe.src = iframe.src + '#user=bill';
    }, 2000);
    
    function onCallback(res){
        alert('data from page c' + res);
    }
</script>
  1. www.withtimesgo2.com/b.html
<iframe id="iframe" src="http://www.withtimesgo1.com/c.html"></iframe>
<script>
    var iframe = document.getElementById('iframe');
    window.onhashchange =function(){
        iframe.src = iframe.src + location.hash;
    }
</script>
  1. www.withtimesgo1.com/c.html
<script>
    window.onhashchange = function(){
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=',''));
    };
</script>

通过以上实现代码我们可以总结出来,不同域的A和B想要通信,需要借助第三方代理页面C实现,C页面必须和A/B其中一个同域。整个流程为A->B->C->A。更加详细地说,A页面通过改变其中iframe的src传递hash值到B页面,B页面监听到url变化触发相应的操作。B需要把数据传回给A页面,但是由于A/B页面的域名不同,所以不能使用parent.location.hash传值,需要使用A域名下的代理iframe也就是C页面实现传值。B页面创建隐藏的iframeC页面,并创建监听hash变化的函数从而通过改变iframe的url将location.hash传递给C页面,C页面监听到url的变化后,修改A页面的URL,这里因为A/C属于同源,所以可以直接修改parent.parent.location.hash值,A页面也可以监听hash变化从而做相应的操作。这样就实现了不同域名之间A/B页面的数据传递。它的优点是实现简单,缺点是产生了不必要的浏览器记录;有的浏览器不支持onhashchange事件导致兼容性不好;数据直接放在url中,数据容量、类型和安全性都不理想。

window.name + iframe

原理: window.name是window对象的一个属性,这种方法的仰仗该属性的特点:在一个窗口的生命周期内,它所打开的所有页面都共享一个window.name值,每一个页面都可以读写window.name的值。也就是说,只要是在同一个浏览器窗口内打开的所有页面,不管是不是同域,用的都是同一个window.name。

实现:

  1. www.withtimesgo1.com/a.html
var proxy = function(url, callback){
    var state = 0;
    var iframe = document.createElement('iframe');
    
    // 加载跨域的页面
    iframe.src = url;
    
    iframe.onload = function(){
        if(state === 1){
           //第二次onload(同域proxy页),读取同域window.name的数据
            callback(iframe.contentWindow.name);
           //销毁整个iframe
            destroyFrame();
        }else if(state === 0){
           // 第一次跨域页,切换到同域代理页面,改变state iframe.contentWindow.location = 'http://www.withtimesgo1.com/proxy.html';
            state = 1;
        }
    }
    document.body.appendChild(iframe);
    
    function destroyFrame(){
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
};

// 访问跨域资源data
proxy('http://www.withtimesgo2.com/b.html', function(data){
    alert(data);
});
  1. proxy.html (www.withtimesgo1.com/proxy.html)
代理页,和a.html同域,内容空即可

3)www.withtimesgo2.com/b.html

<script>
    window.name = "hello!"
</script>

这种方法的优点是方法巧妙,与 document.domain方法相比,不再要求域名后缀必须一致,从而实现可以从任意页面获取 string数据。它的缺点也很明显,就是只能获取string类型的数据。

到目前为止,我们总结了三种跨域方法,都是和iframe有关的,也就是都是双向通信的。其中两个和代理页面有关联,我们把他们放在一起记忆,加深理解,同时,这三种方法应用有限,因为都不是特别理想的跨域解决方案,各有各的问题,下面我们再来看几个应用广泛,功能强大,单项跨域的解决方案,这些解决方案一般用来获取数据。

JSONP

在大型网络应用中,很多时候企业会把静态资源放到另一台或者很多台独立域名的服务器中以分担服务器压力,其实也就是内容分发网络CDN。这就使得我们需要从不同的域获得静态资源比如js,csss,img等。

原理:<script>标签引入JS文件是不受同源机制限制的,所以根据这个特性,我们可以利用<script>标签引入一个JS或者其他后缀的文件,此文件返回一个回调函数。

实现:

<script>
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = 'http://www.withtimesgo2.com/login?user=bill&callback=handleCallback';
    document.head.appendChild(script);
    
    function handleCallback(res){
        alert(JSON.stringify(res));
    }
</script>

服务端返回

handleCallback({'status': true, 'user': 'bill'});

JSONP方法的优点是不受同源机制限制,这点比AJAX好;兼容性好,适合很多老版本浏览器;不需要XMLHttpRequest或ActiveX的支持;并且在请求完毕后可以通过回调函数的方式回传结果。但是缺点是它只允许GET方法获取数据;它只支持跨域HTTP请求这种单一情况,不能解决不同域的两个页面之间如何进行JS互相调用的问题。

CORS跨域资源共享

CORS是目前最流行最强大的跨域解决方案。它的兼容性尚可,大部分浏览器都已经支持CORS,实现CORS的关键在于服务端,服务端需要设置Access-Control-Allow-Origin来开启CORS,这个属性决定了什么域名可以访问资源,如果这里设置通配符,则表示所有网站都可以访问资源。

原理:CORS是W3C标准,允许浏览器向不同源的服务器发出XMLHttpRequest请求,克服掉AJAX只能同源的问题。CORS过程是浏览器自动完成的,CORS对于开发者来说和使用AJAX一样,因为浏览器一旦发现需要跨域,自动会添加信息,所以只要浏览器和服务器都允许CORS,就可以自动实现跨域了。

这里还有一点需要注意,虽然前端这里没有什么工作,但是CORS请求要知道被分成了两种,一种是简单请求,另一种是复杂请求。浏览器针对两种请求的处理方式不同。


简单请求:

  • 请求方式为HEAD, GET,POST
  • Content-Type的值仅限于下列三者之一:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
  • http头信息不超出下面的字段:Accept、Accept-Language 、 Content-Language、 Last-Event-ID

复杂请求: 不符合以上条件的请求就算做是复杂请求了。 复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求,通过该请求来知道服务端是否允许跨域请求。

nodejs中间件代理

node.js中间件代理和nginx反向代理非常类似,都是借助代理服务器的方式实现跨域。 原理: 同源机制是浏览器和服务器之间的限制,而如果是服务器和服务器之间则没有同源限制,所以可以借助代理服务器实现跨域获得资源。整个过程大致为客户端向代理服务器请求资源,代理服务器转发到目标服务器,代理服务器从目标服务器获得数据,最后再将数据返回给客户端。

具体实现这里不再赘述,很多大神已经都写的非常详细了,大家可以用的时候参考。

nginx反向代理

nginx反向代理和node.js中间件代理方式非常类似,也是借助一个代理服务器实现跨域。它是最简单的跨域方式,只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。

具体实现这里也不再赘述,大家可以参考相关文章学习。

websocket

终于,我们到了最后一个跨域方式——websocket协议跨域。websocket协议是HTML5的新协议,它的作用是实现浏览器和服务器的全双工通信,同时允许跨域通信。Websoket是应用层的协议,和HTTP一样是基于TCP协议,但是Websocket和HTTP不同的是websocket的server和client都能主动向对方发送或者接收数据,他们的通信是双向的。Websocket在建立连接时需要用到HTTP,但连接之后也就不再需要了。原生的API不是很好用,可以使用socket.io来实现跨域。

实现:

<script>
    let socket = new WebSocket('ws://localhost:3030');
    socket.onopen = function(){
        // 向服务器发送数据
        socket.send("hello");
    }
    socket.onmessage = function(e){
        // 接收服务器的数据并打印
        console.log(e.data)
    }
</script>

// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');
let wss = new WebSocket.Server({port: 3030});
wss.on('connection',function(ws){
    ws.on('message',function(data){
        console.log(data);
        ws.send("I am Tom");
    });
})

至此,我们学习了九种跨域方式,现在我们应该对跨域这一块内容有了更深入的理解,如果面试官跟你聊起整个问题,我想我们可以侃侃而谈了吧。如果对你有帮助的话,欢迎点赞留言哦,谢谢。