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頁面。html

能夠利用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請求。jquery

先舉一個簡單的例子。ajax

a.html:json

<!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服務器

固然實際開發中一個頁面要發出不少個請求,而且根據用戶操做不一樣可能要發出不同的請求,因此須要進一步的封裝。app

a頁面經過b頁面發出請求,這須要將發送請求的url以及請求的類型和請求參數等信息傳遞到b頁面,能夠直接經過嵌入的iframe的src來傳遞,而後b頁面根據location.search的值來獲取以上的信息。dom

咱們的目標是構建一個形如iframeAjax(params,cb)的函數。
其中params中,包括了b頁面的url,ajax請求的最終目標url,ajax請求的種類,以及ajax請求的參數。
格式以下:async

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請求的兩種方法。

相關文章
相關標籤/搜索