iframe解决跨域ajax请求的方法

iframe跨域的基本前提是,一个页面可以嵌套非同源站点的html文件,以及某一个域名下的html页面可以通过脚本向同域名服务器发出ajax请求。当一个域名为domain1下的页面A想要向domain2发出ajax请求时,由于同源策略的限制无法直接请求到数据,但是可以在页面A中动态添加一个display设置为none的iframe,该iframe的src为domain2下的html页面B,由页面B来发出ajax请求,因为页面B是domain2下的所以可以成功发出请求。当B页面拿到数据之后再传递给A页面。

可以利用HTML5的postMessage来向A页面传递数据。
将通过一个demo来具体说明如何操作,该demo涉及到两个域名 http://localhost:3000 以及 http://127.0.0.1:3001。向 http://localhost:3000/a.html中嵌入 http://127.0.0.1:3001/b.html页面,在 http://127.0.0.1:3001/b.html发出本域名下的ajax请求。

先举一个简单的例子。

a.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<iframe src="http://127.0.0.1:3001/b.html"></iframe>
<script>
    window.addEventListener('message',e => {
      console.log(e.data);
    })
</script>
</body>
</html>

b.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="http://code.jquery.com/jquery-3.3.1.min.js"></script>
</head>
<body>
<script>
    $.ajax({
      url: 'http://127.0.0.1:3001/data.json'
    }).done(data => {
      window.parent.postMessage(data,'*');
    })
</script>
</body>
</html>

a页面中直接嵌入b页面,b页面加载后执行脚本,向同源服务器发出请求,拿到数据后发送给a页面。
这里强调一下:
1.代码中写的是window.parent而非window
2.代码中的第二个参数写为’*’,也可以指定精确的目标origin

当然实际开发中一个页面要发出很多个请求,并且根据用户操作不同可能要发出不一样的请求,所以需要进一步的封装。

a页面通过b页面发出请求,这需要将发送请求的url以及请求的类型和请求参数等信息传递到b页面,可以直接通过嵌入的iframe的src来传递,然后b页面根据location.search的值来获取以上的信息。

我们的目标是构建一个形如iframeAjax(params,cb)的函数。
其中params中,包括了b页面的url,ajax请求的最终目标url,ajax请求的种类,以及ajax请求的参数。
格式如下:

let {targetUrl, queryUrl, type, data = {}} = params

刚才已经提到过,a页面通过设置iframe的src来向b页面传递各种参数。
我们将type以及queryUrl合并到data中:

Object.assign(data, {type, queryUrl});

接下来就可以构建完整的iframe的src

let url = targetUrl + '?' + serialize(data);    //虽然叫url但实际是iframe的src

然后我们封装一个建立frame的方法。

function createIframe(url, cb) {
    let iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = url;
    document.body.appendChild(iframe);

    function handleIframe(e) {
      cb(e.data);
      document.body.removeChild(iframe);
      window.removeEventListener('message', handleIframe);
    }

    window.addEventListener('message', handleIframe);
}

在这个方法中,我们创建了一个iframe设置其display为none使其不可见,设置了其src,并且把它添加到了页面上。又给window添加了事件监听,当收到b页面传来的数据之后,调用数据处理的函数cb并且传入数据作为其参数。当cb执行完毕之后,移除iframe,并移除事件监听。

b页面中我们的任务是,拿到location.search的并正确的解析为一个对象,该对象包含了{type,queryUrl,data} 其中type是ajax的类型,queryUrl是ajax的目标url,data是请求的参数。

通过一个parseSearch的方法解析location.search。

function parseSearch() {
    let search = location.search.slice(1);
    let keyValuePairArr = search.split('&');
    let obj = {};

    keyValuePairArr.forEach(pair => {
      let [key, value] = pair.split('=');
      obj[decodeURIComponent(key)] = decodeURIComponent(value);
    });
    return obj;
}

我们还需要一个方法来移除其中的type和queryUrl,只剩下ajax请求的参数。

function getParams(obj) {
    delete obj.type;
    delete obj.queryUrl;
    return obj;
}

然后我们就可以解析location.search得到信息对象,并且发出ajax请求来拿到服务器的数据了。

$.ajax({
    type: obj.type,
    url: obj.queryUrl,
    data: getParams(obj)
  }).done(data => {
    window.parent.postMessage(data, '*');
});

再次巩固一下整个流程,domain1下的a页面向domain2发出动态ajax请求的中间过程:
在a页面下封装b页面完整url,ajax请求的type,ajax请求的最终url,以及ajax请求的数据等信息。通过设置iframe的src将这些信息带到b页面,b页面解析这些信息,最后向服务器发出ajax请求并将结果通过postMessage的API带回给a页面,a页面收到数据后处理数据,处理完毕移除iframe,移除事件监听。

除了利用postMessage API之外,还可以通过iframe + window.name来实现跨域ajax请求。
当a页面中有一个b页面的iframe时,两个页面共享window.name。在b页面拿到数据后赋值给window.name。但是由于不使用postMessage API,数据无法传输给a页面。我们可以将location.href转为domain1(a页面所在的域名)下的c页面,由c页面调用a页面的回调方法。

先举一个简单的例子:

a页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<iframe src="http://127.0.0.1:3001/b.html"></iframe>
<script>
    function print(data) {
      console.log(data);
    }
</script>
</body>
</html>

b页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="http://code.jquery.com/jquery-3.3.1.min.js"></script>
</head>
<body>
<script>
    $.ajax({
      url: 'data.json'
    }).done(data => {
      window.name = JSON.stringify(data);
      location.href = 'http://localhost:3000/c.html';
    })
</script>
</body>
</html>

c页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
    parent.print(JSON.parse(window.name));
</script>
</body>
</html>

很简单的例子,a页面中定义一个方法print,并嵌入一个frame,frame为b页面,b页面向服务器拿到数据后,赋值给window.name,然后再打开c页面,在c页面调用父页面a中的print方法,并将拿到的数据作为参数传入。
不知道为啥,ajax请求返回的对象不经处理直接赋值最终输出结果是[object Object]。因此在这里先stringify之后再parse。

封装之后的代码如下:

aa页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
  function serialize(params) {
    let paramArr = [];
    for (let [key, value] of Object.entries(params)) {
      paramArr.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
    }
    return paramArr.join('&');
  }

  function iframeAjax(params, cb) {
    let funcName = randomFuncName();
    window[funcName] = cb;

    let {midUrl, targetUrl, queryUrl, type, data = {}} = params;

    Object.assign(data, {targetUrl, queryUrl, type, funcName});

    let url = midUrl + '?' + serialize(data);

    createIframe(url);
  }

  function randomFuncName() {
    return 'iframe' + Math.floor(Math.random() * 100000 + 100000);
  }

  function createIframe(url) {
    let iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = url;
    document.body.appendChild(iframe);
  }

  iframeAjax({
    midUrl: 'http://127.0.0.1:3001/bb.html',
    targetUrl: 'http://localhost:3000/c.html',
    queryUrl: 'http://127.0.0.1:3001/async-post',
    type: 'post',
    data: {name: '黄天浩'}
  }, function (data) {
    console.log(data);
  });
</script>
</body>
</html>

bb页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="http://code.jquery.com/jquery-3.3.1.min.js"></script>
</head>
<body>
<script>
  function parseSearch() {
    let search = location.search.slice(1);
    let keyValuePairArr = search.split('&');
    let obj = {};

    keyValuePairArr.forEach(pair => {
      let [key, value] = pair.split('=');
      obj[decodeURIComponent(key)] = decodeURIComponent(value);
    });
    return obj;
  }

  function getData(obj) {
    let dataObj = {};
    let dataKeys = Object.keys(obj).filter(key => key !== 'type' && key !== 'queryUrl' && key !== 'targetUrl' && key !== 'funcName');
    dataKeys.forEach(key => {
      dataObj[key] = obj[key];
      delete obj[key];
    });
    return dataObj;
  }

  let obj = parseSearch();

  let data = getData(obj);

  $.ajax({
    type: obj.type,
    url: obj.queryUrl,
    data
  }).done(data => {
    window.name = JSON.stringify(data);
    //window.name = data;
    location.href = obj.targetUrl + '?funcName=' + obj.funcName;
  });
</script>
</body>
</html>

c页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
  function getFuncName() {
    let search = location.search;
    let reg = /\?funcName=(.+)/;
    return reg.exec(search)[1];
  }

  let funcName = getFuncName();
  parent[funcName](JSON.parse(window.name));
  //parent[funcName](window.name);

  delete parent[funcName];
  let iframes = [...parent.document.getElementsByTagName('iframe')];
  iframes.forEach(iframe => {
    parent.document.body.removeChild(iframe);
  })
</script>
</body>
</html>

不同于使用postMessage,当通过window.name通信时,由于传入的回调函数是在页面c执行,因此需要把回调函数的函数名作为参数传过来,并且也要把页面c的url地址作为参数传入。数据仍然是通过bb页面获得,bb页面仅需向c页面传递funcName即可,c页面拿到函数名,并通过window.name传递从服务器拿到的数据,在c页面执行aa页面的回调函数,回调函数执行完毕后移除aa页面iframe以及该函数。

注意:不能在bb页面直接调用parent页面aa的函数 由于aa与bb是非同源的,因此会报错。必须由一个与aa页面同源的页面cc页面,来执行在aa页面中注册的函数。

以上是我所知道的iframe解决跨域ajax请求的两种方法。

    原文作者:James
    原文地址: https://segmentfault.com/a/1190000013536703
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞