qiangdada

个人技术站

欢迎来到 qiangdada 的个人技术站 ~

前端交流群:731175396

公众号:『合格前端』

img

跨域那些事

一、什么是跨域

由于浏览器对安全访问因素的考虑,是不允许 js 跨域调用其他页面的,这里的域我们把它想象成域名,如,一个域名为 https://www.oschina.net ,另外一个域名为 https://www.zhihu.com ,这两者属于不同的域名,它们之间的页面也是不能相互调用的,它属于同源策略所定义限制中的一种。同源策略具体分为以下几类:

不同域名

  • 相同域名不同端口号,如 https://www.oschina.net:8000 和 https://www.oschina.net:8001
  • 同一个域名不同协议,如 http://www.oschina.net和https://www.oschina.net
  • 域名和域名对应的的 IP,如 http://b.qq.com 和 http://10.198.7.85
  • 主域和子域,如 http://www.oschina.net 和 https://test.oschina.net
  • 子域和子域,如 https://test1.oschina.net 和 https://test2.oschina.net

以上情况只要出现了,那么就会产生跨域问题。那么如果解决跨域问题呢,下面的小节会总结一些解决跨域常用的方法

二、跨域解决方案

1、JSONP

对于 JSONP ,有个通俗易懂的解释-JSONP(JSON with Padding)是数据格式 JSON 的一种“使用模式”,可以让网页从别的网域要数据。

由于同源策略,如上所述。但是(中国人讲话是很有文化的),HTML<script> 元素是一个例外,它并不遵循同源策略,利用 <script> 元素的这个开放策略,网页可以得到从其他来源动态产生的 JSON 数据,而这种使用模式就是所谓的 JSONP 。用 JSONP 抓到的数据并不是 JSON ,而是任意的JavaScript,用 JavaScript解释器运行而不是用 JSON 解析器解析。来来来,我来举个栗子 🌰 吧 前端浏览器页面

<script>
function jsonpCallBack (res, req) {
  console.log(res, req);
}
</script>
<script type="text/JavaScript" src="http://localhost/test/jsonp.php?callback=jsonpCallBack&data=getJsonpData"></script>

另一个域名服务器请求接口

<?php
  /*后端获取请求字段数据,并生成返回内容*/
  $data = $_GET["data"];
  $callback = $_GET["callback"];
  echo $callback."('success', '".$data."')";
?>

测试结果如下

这种方案需要注意的是他支持 GET 这一种HTTP请求类型,还有尤为重要的就是其他域要有一定可靠性,不然你的网站会 GG 的。当然有时我们还可以通过一个方法来动态生成需要的 JSONP

2、跨域资源共享(CORS-Cross Origin Resource Sharing)

CORS,它是 JSONP 模式的现代升级版,与 JSONP 不同的是,CORS 除了 GET 要求方法以外也支持其他的 HTTP 要求。浏览器 CORS 请求分成两种

  1. 简单请求
  2. 协商模型/预检请求(Preflighted Request),即非简单请求

如何区分请求具体属于哪一种呢,下面我总结了几点:

i. 请求方式

  • GET
  • HEAD
  • POST

ii. HTTP 的头信息子段

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain,其中’text/plain’默认支持,其他两种则需要预检请求和服务器协商

满足以上两大点的即为简单请求,否则为非简单请求。具体请求处理的不同,大家可以去查阅下 MDN https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS ,那里有详细的解析及用法。

3、document.domain + iframe(适用于主域名相同的情况)

从上面的同源策略我们可以知道,浏览器这边是认为主域和子域、子域和子域,它们属于不同的域,那么我们如果需要让主域和子域之间可以进行通信,需要做的就是通过修改 document.domain ,把它们改成相同的 domain

在域名为 server.example.com 中的 a.html

document.domain = 'example.com';
var $iframe = document.createElement('iframe');
$iframe.src = 'server.child.example.com/b.html';
$iframe.style.display = 'none';
document.body.appendChild($iframe);
$iframe.onload = function(){
    var doc = $iframe.contentDocument || $iframe.contentWindow.document;
    //在这里操作 doc,也就是操作 b.html
    $iframe.onload = null;
};

在域名为 server.child.example.com 中的 b.html

document.domain = 'example.com'

这种形式方便归方便,但也有其方便带来的隐患

  • 安全性,当一个站点被攻击后,另一个站点会引起安全漏洞
  • 若页面中引入多个 iframe ,要想操作所有 iframe ,domain 需要全部设置成一样的

4、window.name + iframe

window 对象的 name 属性是一个很特别的属性,它可以在不同页面甚至不同域名加载后依旧存在。使用步骤如下:

  1. 首先在页面 A 中利用 iframe 加载其他域中的页面 B
  2. 在页面 B 中将需要传递的数据赋给 window.name
  3. iframe 加载完成后,页面 A 中修改 iframe 地址,将其变成同一个域下的地址,然后获取 iframe 中页面 B 的 window.name 属性

示例代码如下: 首先我们在域名为 http://127.0.0.1 下建立好 B 页面,在 B 页面的 <script> 标签中将需要传递的数据赋给 window.name

window.name = '页面B中传递给页面A的数据';

然后我们域名为 http://127.0.0.1:9000 的 A 页面,这里我们需要做的一件事就是利用 iframe 加载页面 B,并将其域名进行修改,变成和页面 A 一样的域名

function proxy (url, callback) {
  var flag = true
  var $iframe = document.createElement('iframe')
  var loadCallBack = function () {
    if (flag) {
      // 这里我们还得在域名为 http://127.0.0.1:9000 建立一个tmp.html文件当做缓存界面
      $iframe.contentWindow.location = 'http://127.0.0.1:9000/tmp.html';
      flag = false;
    }
    // 修改localtion后,每次触发onload事件会重置src,相当于重新载入页面,然后继续触发onload。
    // 这里是针对该问题做的处理
    else {
      callback($iframe.contentWindow.name);
      $iframe.contentWindow.close();
      document.body.removeChild($iframe);
      $iframe.src = '';
      $iframe = null;
    }
  };

  $iframe.src = url;
  $iframe.style.display = 'none';
  // 事件绑定兼容简单处理
  // IE 支持iframe的onload事件,不过是隐形的,需要通过attachEvent来注册
  if ($iframe.attachEvent) {
    $iframe.attachEvent('onload', loadCallBack);
  }
  else {
    $iframe.onload = loadCallBack;
  }

  document.body.appendChild($iframe);
}
proxy('http://127.0.0.1/bop/test.html', function(data){
  console.log(data);
});

测试结果如图

5、HTML5 中的 postMessage(适用于两个 iframe 或两个页面之间)

postMessage 隶属于 html5,但是它支持 IE8+ 和其他浏览器,可以实现同域传递,也能实现跨域传递。它包括发送消息 postMessage 和接收消息 message 功能。

postMessage 调用语法如下

otherWindow.postMessage(message, targetOrigin, [transfer])
  • otherWindow : 其他窗口的一个引用,比如 iframecontentWindow 属性、执行 window.open 返回的窗口对象、或者是命名过或数值索引的 window.frames
  • message : 将要发送到其他 window 的数据,类型为 string 或者 object
  • targetOrigin : 通过窗口的 origin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串”*“(表示无限制)或者一个URI
  • transfer (可选) : 一串和 message 同时传递的 Transferable 对象

接收消息 message 的属性有:

  • data :从其他 window 中传递过来的数据
  • origin :调用 postMessage 时消息发送方窗口的 origin
  • source :对发送消息的窗口对象的引用

示例如下:域名 http://127.0.0.1:9000 页面 A 通过 iframe 嵌入了 http://127.0.0.1 页面 B,接下来页面 A 将通过 postMessage 对页面 B 进行数据传递,页面 B 将通过 message 属性接收页面 A 的数据

页面 A 发送消息代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>页面A</title>
</head>
<body>
  <h1>hello jsonp</h1>
  <iframe src="http://127.0.0.1/b.html" id="iframe"></iframe>
</body>
</html>
<script>
window.onload = function() {  
  var $iframe = document.getElementById('iframe');  
  var targetOrigin = "http://127.0.0.1";  
  $iframe.contentWindow.postMessage('postMessage发送消息', targetOrigin);  
}; 
</script>

页面 B 接收消息代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>页面B</title>
</head>
<body>
  <h1>hello jsonp</h1>
</body>
</html>
<script>
var onmessage = function (event) {
  var data = event.data;     //消息
  var origin = event.origin; //消息来源地址
  var source = event.source; //源Window对象
  if(origin === "http://127.0.0.1:9000"){
    console.log(data, origin, source);
  }
};
// 事件兼容简单处理
if (window.addEventListener) {
  window.addEventListener('message', onmessage, false);
}
else if (window.attachEvent) {
  window.attachEvent('onmessage', onmessage);
}
else {
  window.onmessage = onmessage;
}
</script>

运行结果如下

6、location.hash + iframe(适用于两个 iframe 之间)

对于 location.hash,我们先看一张图先

相信看完图,大家也大概清楚了 location.hash 到底是用来干啥子的。没错,它可以用来获取或设置页面的标签值 如 http://127.0.0.1:9000/#hello ,它的 location.hash 值则为 ‘#hello’。它一般用于浏览器锚点定位,HTTP 请求过程中却不会携带 hash,所以这部分的修改不会产生 HTTP 请求,但是会产生浏览器历史记录,这对我们进行跨域通信给予了帮助。我们可以通过修改 URL 的 hash 部分来进行双向通信。

示例如下:域名 http://127.0.0.1:9000 页面 A 通过 iframe 嵌入了 http://127.0.0.1页面 B ,接下来页面 A 和页面 B 将通过 location.hash 进行双向通信

页面 A 代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>页面A</title>
</head>
<body>
  <iframe src="http://127.0.0.1/bop/test.html#locationHash" id="ifr"></iframe>
</body>
</html>

页面 B 代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>页面B</title>
</head>
<body>
  <h1>hello localtionHash</h2>
</body>
</html>
<script>
try {
  parent.location.hash = 'data';
} catch (e) {
  // ie、chrome的安全机制无法修改parent.location.hash,所以要借助于父窗口域名下的一个代理iframe
  var $ifrproxy = document.createElement('iframe');
  $ifrproxy.style.display = 'none';
  // 注意proxy.html必须是域名为 http://127.0.0.1:9000 下的页面
  $ifrproxy.src = "http://127.0.0.1:9000/proxy.html#locationHashChange";
  document.body.appendChild($ifrproxy);
}
</script>

代理页面代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Proxy页面</title>
</head>
<body>
    
</body>
<script>
// 因为parent.parent和自身属于同一个域,所以可以改变其location.hash的值
parent.parent.location.hash = self.location.hash.substring(1);
</script>
</html>

运行结果如图

以上内容便是文章的所有内容了,希望多多少少可以帮助小伙伴们去更好的理解跨域

回到顶部

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开 支付宝 或者 微信 扫一扫,即可进行扫码打赏哦