[秃破前端面试] —— 跨域实践总结

前言

年前年后跳槽季,准备从面试内容入手看看前端相关知识点,旨在探究一个系列知识点,能力范围以内的深刻探究一下。重在实践,针对初级前端和准备面试的同窗,争取附上实际的代码例子以及相关试题~系列名字就用【秃破前端面试】—— 由于圈内你们共识,技术与发量成正比。😄但愿你们早日 破瓶颈~html

关于面试题或者某个知识点的文章太多了,这里笔者只是想把我的的总结用代码仓库的形式记录下来并输出文章,毕竟理论不等于实践,知其然也要知其因此然,实践用过才能真正理解~前端

相关系列同类型文章:node

其余类型:jquery

什么是跨域

今天这篇咱们来好好讲讲跨域实践~为何要加上实践,由于跨域这东西,相信你们理论上看得足够多了,若是做为面试来讲,可能说出来几个方案就够了,面试官也不会让实际写代码,可是你真的使用过吗?你真的了解其中的实现原理吗?基于此观点,写了以下这篇实践为主的跨域文章。git

具体来说,看上面的文章《Web安全相关》应该大致了解了。若是没了解,在这里就再简要概述一下。github

在前端页面请求 url 地址的时候,该 url 与浏览器上的 url 地址必须处于同域上,也就是域名、端口以及协议三者相同。若是其中任何一个不一样,就属于跨域范畴。面试

直接代码截图来看更为直观:ajax

// express 起了一个小型服务,而且写了一个接口 /list
app.get('/list', (req, res) => {
  const list = [
    {
      name: 'luffy',
      age: 20,
      email: 'luffy@163.com' 
    }, {
      name: 'naruto',
      age: 24,
      email: 'naruto@qq.com'
    }
  ]
  res.status(200).json(list);
});
复制代码

浏览器访问一下:express

再写一个html页面调用这个接口:npm

<script>
  window.onload = function() {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', 'http://localhost:3000/list');
    xhr.onreadystatechange = function () { 
      if (xhr.readyState === 4 && xhr.status === 200) {
        const resData = JSON.parse(xhr.responseText);
        console.log(resData);
      }
    };
    xhr.send();
  }
</script>
复制代码

能够看到,这就是跨域,相信刚学前端又不太懂后台的小伙伴常常会见到。

跨域:简而言之,咱们一般所说的跨域就是指在浏览器同源策略的限制下,浏览器不容许执行其余网站的脚本。

解决跨域的方式

解决跨域的方式多种多样,不过其实说白了,咱们平时用到的也就那么两三种,可是既然是总结,咱们就把各类奇淫技巧都整理一下~

我的以为,在团队项目开发过程当中,前端并非很适合作跨域处理,大部分场景跨域都应该是由后端处理的,因此这里也只是简单讨论这个跨域方案的发展历程。

最流行的跨域解决方案 —— CORS

当下项目中若是涉及到跨域,实际上都应该是后端经过设置 CORS 来解决的。CORS 是目前最主流的跨域解决方案,跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不一样源服务器上的指定的资源。

// 在node端设置一下请求头,容许接收全部源地址的请求
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header('Access-Control-Allow-Headers', '*');
复制代码

重启服务,刷新一下页面:

能够看到,获取到了数据,跨域解决完成~

通常来讲简单点,Node.js 能够直接使用社区成熟的 cors 方案

最经典的跨域解决方案 —— JSONP

接下来要说的就是 JSONP 解决方案,说它最经典一点都不为过,虽然如今大部分项目并不会使用它来解决跨域,可是只要是面试,涉及到跨域,基本都会问到这个知识点。

原理,非同源限制的标签

同源限制是跨域的本质,也就是没有同源限制这么个东西,那么也就不存在跨域了。事实上,存在一些标签没有同源限制 —— <script>/<link>/<img>。JSONP 利用的原理就是这些标签来解决跨域的问题。

第一步:假设后台有一个接口 /jsonp/list

// 按钮获取数据
<button onclick="loadJsonpData()">JSONP获取数据</button>

<script>
  function loadJsonpData() {
    const script = document.createElement('script');
    script.src='http://localhost:3000/jsonp/list';
    document.body.appendChild(script);
  }
</script>
复制代码

点击按钮,就会向<body>标签内部插入一个<script>标签,浏览器遇到<script>就会执行里面的内容。关键点,浏览器会执行脚本里面的内容。

第二步:先后端约定好执行函数的名称

第一步提到过了,把指定的 url 经过<script>标签加载到页面里,会执行脚本里面的内容。那么想一下咱们跨域请求的目的 —— 获取数据。也就是说,里面的内容应该是个可执行函数,而且把咱们想要的数据传递过来。所以,如今先后端须要约定一个执行函数的名称!

假设这边约定该函数名称为callbackData

第三步:前端定义callbackData

约定好了执行函数名称,前端就得定义它,由于后台返回的是一段可执行代码,若是前端没定义,就会报callbackData undefined的错误。

// 定义 callback 函数,获取后台的 data
function callbackData(data) {
    console.log(data, 98989);
}
复制代码

第四步:后台返回携带数据的可执行代码

先后端约定好了名称,而且前端定义好了函数,参数是想要拿到的数据,后台只须要把数据包在执行函数里响应回去就能够了。

注意,JSONP 的接口不一样于正常接口,它返回的不是 json 格式的数据,而是一段可执行字符串,这个字符串会被前端执行。

app.get('/jsonp/list', (req, res) => {
  const list = [
    {
      name: 'luffy',
      age: 20,
      email: 'luffy@163.com' 
    }, {
      name: 'naruto',
      age: 24,
      email: 'naruto@qq.com'
    }
  ]
  // 把数据塞进执行函数里面
  const resData = `callbackData(${JSON.stringify(list)})`;
  res.send(resData); // 这里不能使用res.json而是res.send
});
复制代码

咱们来执行一下看看:

能够看到,点击按钮,浏览器 Network JS 会请求新插入的<script>的地址,该地址响应的内容是事先定义好的callbackData(resData)

而在前端,由于定义了callbackData(data),因此控制台能够看到,打印了后台响应过来的内容。

上面就是 jsonp 的基本过程,不知道给你们解释没解释清楚,其实真的很简单,只不过之前在看的时候感受全部人讲的都很官方,并无实际操做,让不少人会误解或者看不懂,这里我就经过实际代码来说解,相信会很容易理解~

事实上,jsonp 还能够进行封装,而后能够实现的很漂亮~哈哈。好比jquery就内置支持 jsonp。

// jquery jsonp
$.ajax({
	url: "http://cross-domain/get_data",
	dataType: "jsonp", // 指定服务器返回的数据类型
	jsonp: "callback", // 指定参数名称
	jsonpCallback: "callbackName" // 指定回调函数
}).done(function(resp_data) {
	console.log("Ajax Done!");
});
复制代码

这里我就不封装了,由于懂得原理就好了,如今的前端应该不多使用了,只用来面试了。

另外,虽然我常用的是<script>标签,可是其实<img>标签也是能够的。

最简单的跨域解决方案 —— NGINX

这个就很少作介绍了,说实话,并不算先后端跨域解决方案,而是属于运维层级的,而且若是面试被问到跨域相关,面试官应想获得的应该也不是这个答案。

一个简易版 NGINX 解决跨域配置大概以下:

server
{
    listen 3003;
    server_name localhost;
    location /ok {
        proxy_pass http://localhost:3000;

        # 指定容许跨域的方法,*表明全部
        add_header Access-Control-Allow-Methods *;

        # 预检命令的缓存,若是不缓存每次会发送两次请求
        add_header Access-Control-Max-Age 3600;
        # 带cookie请求须要加上这个字段,并设置为true
        add_header Access-Control-Allow-Credentials true;

        # 表示容许这个域跨域调用(客户端发送请求的域名和端口) 
        # $http_origin动态获取请求客户端请求的域 不用*的缘由是带cookie的请求不支持*号
        add_header Access-Control-Allow-Origin $http_origin;

        # 表示请求头的字段 动态获取
        add_header Access-Control-Allow-Headers 
        $http_access_control_request_headers;

        # OPTIONS预检命令,预检命令经过时才发送请求
        # 检查请求的类型是否是预检命令
        if ($request_method = OPTIONS){
            return 200;
        }
    }
}
复制代码

理论上的跨域解决方案 —— window.name

说它是理论上的跨域解决方案也就是说它确实能实现跨域传递数据,可是却不多被应用。 查阅了一下,MDN 是这么说的,(如 SessionVars 和 Dojo's dojox.io.windowName ,该属性也被用于做为 JSONP 的一个更安全的备选,来提供跨域通讯(cross-domain messaging)。可是这俩框架我也确实孤陋寡闻了,仍是有应用的而且兼容性仍是很好的,除了万年不变的 IE 不必定支持,其余的浏览器都支持。

咱们先来简单了解一下什么是window.name

  • 每一个浏览器窗口(Tab页)都有独立的window.name与之对应
  • 在一个窗口(Tab页)的从打开到关闭以前,窗口载入的全部页面同时共享一个window.name,该窗口下每一个页面对window.name都有读写的权限。
  • window.name是当前窗口(Tab页)的属性,并不会由于页面跳转而发生改变。
  • window.name容量大概是2MB,存储格式为字符串

提及来略显苍白,仍是来实际例子看看吧。

上图,在http://127.0.0.1:3006设置了window.name = aaaaa,以后页面跳转到了http://127.0.0.1:3008,根据浏览器同源策略,这是跨域 场景,而也正确拿到了上一个页面设置的window.name。上面提到了,每个窗口共享,那么若是是不一样端口呢?

// 代码改为新窗口打开
<a target='_blank' href='http://127.0.0.1:3008/'>跳转到3008端口</a>
复制代码

能够看到,window.name确实是窗口(Tab页)之间独立的,新窗口打开,window.name初始化是空字符串。

上面这两张图则是window.name的存储形式,设置了Array aObject b,可见window.name在存储的时候会调用该对象自身的toString()以后再存储。

固然,若是面试官真的问到这里了,这么回答其实也没什么大问题,可是仍是存在瑕疵的,由于存在特殊状况,就是 ES6 新增的基本类型Symbol

实际使用window.name进行跨域数据获取

上面说了那么多,其实都是简单的介绍window.name特性及使用方法,事实上,并无涉及到跨域获取数据,好比说第一个 Demo,我在 A 设置 window.name 而后跳转到 B 能拿到 A 设置的值,这叫跨域吗?我跳转的时候把值加在参数上岂不是更方便,因此并非实际场景。下面咱们就来一个实际场景:

【问题描述】: 存在两个不一样域页面 A 和 B, 经过 window.name 实现 A 加载的时候获取到 B 页面设置的 window.name

先来思考一下,B 页面作的事情无非就是把数据设置在window.name里,那么咱们加载 A 页面的时候去 B 页面获取,这个时候又不能先跳转到 B 而后再回到 A,由于获取数据是一个异步不刷新页面的场景。嗯,说到这差很少就知道了,确定是得经过 iframe 来实现了,也就是 A 页面开一个同域的 iframe —— 假设是 proxy.html,咱们称之为中转页,中转页内咱们再打开 B 页面,获取 B 的window.name事先设置好的 data 便可。整个过程大体以下:

下面是代码部分:

http://127.0.0.1:3006/a-data.html -> A 页面
http://127.0.0.1:3006/a-data.html -> proxy 数据中转页面 -> 只是个空页面
http://127.0.0.1:3008/b-data.html -> B 页面
___________________________________________

// b-data.html
<script>
  const data = [
    {
      name: 'luffy',
      age: 20
    }, {
      name: 'naruto',
      age: 22
    }
  ]
  window.onload = function() {
    window.name = JSON.stringify(data);
  }
</script>

// a-data.html
<script>
  const currentDomain = 'http://127.0.0.1:3006'; // 当前域
  const corssDomain = 'http://127.0.0.1:3008'; // 跨域
  window.onload = function() {
    let flag = false; // 是否获取数据
    iframe = document.createElement('iframe'),
    loadData = ()=> {
      if (flag) {
        // 读取B的数据
        let data = iframe.contentWindow.name;    
        console.log(data, 66666);
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
      } else {
          flag = true;
          // 加载同域代理文件
          iframe.contentWindow.location = `${currentDomain}/proxy.html`; 
      }  
    };
    iframe.src = `${corssDomain}/b-data.html`;
    if (iframe.attachEvent) {
      iframe.attachEvent('onload', loadData);
    } else {
      iframe.onload  = loadData;
    }
    document.body.appendChild(iframe);
  }
</script>
复制代码

从上图能够看到,A 获取到了 B 页面传递过来的数据,怎么说呢,太复杂了,须要增长一个 iframe 而且还须要一个中转页,因此 -> 综上所述, 我的以为,window.name确实是个理论解决方案,跨域必须依赖window.name的种种特性,可是必须是在浏览器同一窗口下进行,且须要配合iframe以及同域中转页。

传说中的嫡系方案 HTML5 postMessage

这算是一个比较高级的方式了,为何呢?由于前面的 JSONP 或者 window.name,是奇思妙想而来的,人家官方设计它并非拿来进行跨域的或者说是钻了设计的漏洞,至于 CORS 和 NGINX 则是非前端范畴。因此 HTML5 出了这个 postMessage 专门用来正正经经作“安全”跨域的,亲儿子和捡来的儿子的区别能同样吗😂?可是也不知道为啥,我以为可能这种方式使用的也比较少吧,或许我孤陋寡闻,可是确实没怎么看人用过。

原理otherWindow.postMessage(message, targetOrigin, [transfer]);

postMessage 的原理是依赖于一个其余窗口的引用,而这个引用能够是window.open()返回的,也能够是一个 iframe 的 contentWindow,还能够是命名事后有索引的window.frames,因此可能在其余地方你也会看到 postMessage 和 iframe 在一块儿使用。

postMessage使用起来也是比较简单,而且官方文档介绍的也是比较详细,感兴趣的能够仔细阅读阅读,我这里直接就实践上代码了:

// A页面

let opener;
function openB () {
    opener = window.open('http://127.0.0.1:3008/index.html')
}

// 发送消息
function postMsg() {
    const msg = document.getElementById('chatB').value;
    opener.postMessage(msg, "http:/127.0.0.1:3008");
}
复制代码

A 页面的逻辑很简单,就是咱们经过 A 页面使用window.open()打开 B,而后使用获取到的 targetWindow 进行两个页面间的通讯。

// B 页面
window.addEventListener("message", receiveMessage, false);

function receiveMessage(event) {
    // Chrome浏览器兼容
    const origin = event.origin || event.originalEvent.origin; 
    if (origin !== "http://127.0.0.1:3006") {
      return;
    }
    const { data } = event; // 获取到 A 的数据
    // 下面是你的逻辑
    ...
}
复制代码

在 B 页面,咱们监听message事件,而后判断是不是目标域,若是是目标域,获取到数据进行操做。

这里强调一下为何说安全,由于 A 与 B 进行通讯以前,都会判断是不是否是目标域,若是不是是不会进行操做的,是前端开发者可控的状态。

咱们来看一下效果:

只能用一句哎呦,不错呦~来形容了,既然已经完成了 A 跟 B 发消息,那么就送佛送到西,咱把 B 到 A 的也写完,也算是一个简易版聊天系统了。

OK,看起来仍是很不错的,毕竟官方方案,弄的很成熟,并且发送数据也能够是多种多样的格式,这应该是最早进的了。可是,原谅我,即便是写完了这个小 Demo,我也仍是想象不到它的真实贴切的使用场景,局域网聊天?有可能,你若是跟我说实时通讯,那我确定是不信的,由于下面还会介绍更牛的大佬~若是有人用得多,或者真实场景使用过,能够留言交流下,让我涨涨见识😄

其余跨域解决方案

document.domain

这个就更没啥人用了,也只是存在于书本上或者文档里,由于场景比较局限限制比较大。他的限制却是很少,可是限制性很大。想要使用这种方式实现跨域,A与B必须知足以下条件:

A: http://aaa.xxx.com:port
B: http://bbb.xxx.com:port
复制代码

也就是,A 与 B 的一级域名相同,二级域名不一样,而且协议和端口号也必须相同。

在知足上述条件基础之上,两个页面彼此设置window.domain = 'xxx.com',就能够进行通讯了。。。原谅我穷B一个没有域名给你们展现了。可是说实话也不必,这种方式何须呢。。。

WebScoket

好了,真正的大佬在这里,来了,上面再讲 postMessage 的时候,作了个 A 和 B 聊天的小 Demo,也说到了,若是真的是聊天通信场景,大佬级别的确定是 Webscoket 啊。

为啥 Websocket 能处理跨域?

这个问题该怎么说呢。首先,把 Websocket 放在这里其实算是做弊了,为何呢?由于咱们所说的跨域是指,浏览器和服务端基于 HTTP 协议进行通讯时出现同源限制了。而 Websocket 根本就不是基于 HTTP 协议的,它是位于 TCP/IP 上层,跟 HTTP 协议同层的浏览器通讯协议。

如今写个文章太难了,还得会画画😂

它大概是上面这个样子的,Webscoket 与 HTTP 是同层协议,因此 HTTP 的限制对于 Webscoket 来讲,人家根本不鸟你,同级关系,你凭啥管我~可是呢,还有个小箭头,是指 WebSocket 在创建握手链接时(TCP三次握手),数据是经过 HTTP 协议传输的,可是在创建链接以后,真正的数据传输阶段是不须要 HTTP 协议参与的。关于 Websocket 的这里就不涉及太多了,由于本文是说跨域的,既然上面写了个通信,那么就拿 Websoket 一样写一个来看看区别,先上效果图(简单的客户端和服务端聊天):

能够看到,Websocket 的重要之处其实并不在于解决跨域,事实上应该也没人用它解决跨域。它的重要之处在于,它提供了客户端主动与服务端主动推送消息的能力。若是是使用 HTTP,咱们通常都只是,客户端发起一个请求,服务端响应这个请求,没办法作到彼此主动推送消息,所以,若是不使用 Websocket 的时候,通常都是经过一个 AJAX 长轮训,设置定时器不断的去发送请求更新数据,这样作其实浪费性能而且也不是很优雅。因此 Websocket 归纳起来的优势就是:

  • 没有同源限制,不跨域
  • 全双工通讯,双端都能主动推送消息
  • 实时性更好,灵活性更高

Websocket 的适用场景也就是那些实时性很高的应用,好比通信类,股票基金类,基于位类等应用。

笔者了解的并非不少,更多相关信息请查阅官方文档以及其余相关文章。

相关题目

1. 什么是跨域,为何会跨域

2. 说说解决跨域的几种方式

3. 说说 JSONP 的实现原理及过程

总结

虽然上面罗列了那么多跨域解决方案,但实际上仍是 CORS 和 JSONP 这两种是最经常使用的,而且面试中也常常被深问。

那么对比一下 CORS 和 JSONP:

CORS JSONP
优势 比较简便,既支持 post 又支持 get 利用的原生标签特性,兼容性特别好
缺点 低版本IE不兼容 只支持 get 方式,而且须要先后端约定

写到这里跨域相关的实践总结基本上是写完了,好累啊,由于除了原理还要想场景写代码,确实不容易,但愿能对你们有所帮助吧~

代码地址👇这里

若是以为还不错,点个 star 和赞不胜感激。

相关文章
相关标签/搜索