完美解決前端跨域之 easyXDM 的使用和解析

百姓網技術團隊發表於2017-09-30

前端跨域問題在大型網站中是比較常見的問題。本文詳細介紹了利用 easyXDM 解決前端跨域的原理細節和使用細節,具體使用時可以在文中程式碼例項的基礎上擴充套件完成。

0、背景

因個別網路運營商存在 HTTP 劫持的情況,導致網站某些重要的 iframe 彈窗頁面被插入了第三方廣告,內容完全被遮擋,嚴重影響使用者體驗。公司決定將這些頁面切換為 HTTPS,切換後發現原來 iframe 浮層自動適應大小的功能失效了,原因是主頁面是 HTTP 的,子視窗載入後對父頁面浮層大小的操作跨域了,被瀏覽器限制無法操作。於是就需要跨域解決方案來解決這種情況。

1、跨域問題

介紹一下什麼是跨域問題?網站頁面間發生資料請求和傳輸時,只要兩個網址中的協議名 protocol、主機 host、埠號 port 三個中的任意一個不同,就構成了跨域。跨域的頁面預設情況下不能通過 JavaScript 直接操作對方的頁面物件。

各種跨域方案簡單對比如下:

上述各種跨域方案本文不做展開,有興趣的同學可以參考《深入理解前端跨域方法和原理》( blog.csdn.net/kongjiea/ar… )。

這裡著重推薦 easyXDM ,因為 easyXDM 整合了現有的多種跨域解決方案,而且很好地實現了跨瀏覽器相容、多個跨域通訊並行、跨域請求白名單、通訊響應等功能,能完美地解決各種跨域使用的應用場景。

2、easyXDM 使用例項

父頁面 index.html 核心程式碼:

<div id="container"></div>
<div id="output">
    <p>藍色區域為主頁面內容輸出區</p>
</div>
<script src="easyXDM.min.js"></script>
<script>
var showMsg = function (message) {
    document.getElementById('output').innerHTML += "<p>" + message + "</p>";
};
var rpc = new easyXDM.Rpc({
    isHost: true,
    remote: 'http://127.0.0.1/easyXDM/iframe.html',
    hash: true,
    protocol: '1',
    container: document.getElementById('container'),
    props: {
        frameBorder: 0,
        scrolling: 'no',
        style: {width: '100%', height: '100px'}
    }
},
{
    local: {
        echo: function (message) {
            showMsg(message);
        }
    }
});
</script>複製程式碼

子頁面 iframe.html 核心程式碼:

<p>實線框為子頁面區域</p>
<button id="btn" value="">點選給主頁面發資料</button>
<div id="output"></div>
<script src="easyXDM.min.js"></script>
<script>
var showMsg = function (message) {
    document.getElementById('output').innerHTML += "<p>" + message + "</p>";
};
window.rpc = new easyXDM.Rpc({
    isHost: false,
    //acl: '^(https?:\\/\\/)?([a-zA-Z0-9\\-]+\\.)*baixing.com(\\/.*)?$',
    protocol: '1'
},
{
    remote: {
        echo: {}
    }
});
document.getElementById('btn').onclick = function () {
    rpc.echo('echo from iframe');
};
</script>複製程式碼

訪問 http://localhost/easyXDM/index.html,因為 index.htmliframe.html 兩個頁面的 host 不同,子頁面操作主頁面內容屬於跨域訪問。有了 easyXDM 作為通道,這個操作就可以正常進行了,效果如下圖所示:

實際應用場景中,修改呼叫函式就可以讓子頁面對父頁面做任何想做的事情了。

3、easyXDM 原理解析

3.1 原理說明

easyXDM 對不同的底層通訊方案進行封裝,比如上面例項中使用了 postMessage() 方案來實現跨域雙向通訊。

3.1.1 子頁面傳送資料給主頁面

easyXDM 將方法呼叫操作進行打包後通過 postMessage() 傳送給主頁面,主頁面的 message 處理函式收到資料後交由 easyXDM 進行解析後調起呼叫函式。程式碼呼叫和資料流如下圖所示:

easyxdm request
easyxdm request

傳遞的資料說明:

  • defaultXXX: 為通道識別符號,頁面不重新整理的情況下,這個值不變
  • id: 請求編號,自增,每傳送一次請求加1
  • method: 需要呼叫的方法名
  • params: 呼叫方法的引數,以 JSON 格式表示
  • jsonrpc: 表示 JSON-RPC 訊息版本

3.1.2 主頁面方法返回響應資料

easyXDM 同樣會呼叫 postMessage 將方法響應發回給子頁面,子頁面的 message 處理函式收到資料後交由 easyXDM 進行解析,解析後執行對應的響應處理操作。程式碼呼叫和資料流如下圖所示:

easyxdm response
easyxdm response

傳遞的資料說明:

  • defaultXXX: 為通道識別符號,頁面不重新整理的情況下,這個值不變;與子頁面傳送的資料一致
  • id: 與呼叫方法時傳送的 id 一致
  • result: 方法響應,以 JSON 格式表示
  • jsonrpc: 表示 JSON-RPC 訊息版本

以下依次對主頁面和子頁面的程式碼做具體說明。

3.2 主頁面呼叫程式碼解析

主頁面呼叫 easyXDM.Rpc() 的時候會初始化通訊元件,同時會建立 iframe 子頁面;具體引數含義介紹如下:

  • isHost: true,表示建立 iframe 子頁面
  • remote: 建立的 iframe 子頁面的 url
  • container: 值為 DOM 物件,建立出來的 iframe 會被包含在 container 中
  • props: 屬性中指定的內容會被附加到 iframe 物件上
  • hash: 為 true 代表通道相關的 xdm_e / xdm_c / xdm_p 引數會在網址 hash 中記錄,為 false 時會變成 url 引數;一般情況下建議設為 true,因為把跨域相關的前端引數傳遞給後端並不是個很好的方式,但可以解決後面的表單提交後的通道保持問題;所以具體場景具體選擇。

通過合理設定以上屬性,就可以將原來寫死在頁面上的 iframe 改為通過 easyXDM.Rpc() 的方式進行載入,從而實現程式碼的靈活嵌入。

上文例項中父頁面 RPC 初始化後的網頁元素如下:

<div id="container">
    <iframe
        name="easyXDM_default5491_provider"
        id="easyXDM_default5491_provider"
        frameborder="0"
        scrolling="no"
        src="http://127.0.0.1/easyXDM/iframe.html#xdm_e=http%3A%2F%2Flocalhost&amp;xdm_c=default5491&amp;xdm_p=1"
        style="width: 100%; height: 100%;">
    </iframe>
</div>複製程式碼

其中 iframe 的 name 和 id 是自動生成的,作用是區分不同的 RPC 通道,也就意味著在一個頁面上可以建立多個跨域呼叫的通道。中間的 xdm_e / xdm_c / xdm_p 引數是初始化後的通道引數。

另外 local 引數配置定義了子頁面可以呼叫的函式方法名和方法實現,方法名、方法引數等都可以任意按需指定。

3.3 子頁面呼叫程式碼解析

iframe 中的 RPC 引數的解析如下:

  • isHost: false,代表這是客戶端,不建立 iframe 頁面
  • protocol: 通訊協議,數字,具體含義見以下通訊協議說明部分,可選
  • acl: 程式碼呼叫方的網址白名單,可選

與主頁面的 local 引數相對應,子頁面的 remote 配置定義了所有子頁面需要呼叫到的主頁面的方法名。只有在 remote 裡定義了,在子頁面上才能通過 RPC 例項呼叫到。

以上正確配置後,函式跨域呼叫就和本地呼叫效果一樣了,具體中間的通訊已經由 easyXDM 來搞定,如同文中的 rpc.echo() 已經可以直接呼叫到主頁面定義的 echo 方法。

3.4 通訊協議說明

關於通訊協議,如在程式碼配置中未指定則會按以下規則依次匹配使用最前面符合的一個

  • 4: 當通訊的兩端屬於同一域時,直接通訊
  • 1: 當存在 windows.postMessagedocument.postMessage 時(IE8+、Firefox 3+、Opera 9+、Chrome 2+、Safari 4+ 支援),使用 postMessage 機制通訊
  • 6: 配置中存在 swf 屬性,並且支援 window.ActiveXObject 時,通過配置的 swf 做通訊
  • 5: Gecko( Firefox 1+ )瀏覽器時,使用 window.frameElement 屬性做通訊
  • 2: 配置中存在 remoteHelper 時,通過配置的 remoteHelper 做通訊
  • 0: 預設,所有瀏覽器都支援;以上規則都不符合時,使用 image 載入機制做通訊

4、更多功能

4.1 增加請求響應處理

index.html 頁面的 echo 函式增加 return 語句返回值:

<script>
new easyXDM.Rpc({
    // ...
},
{
    local: {
        echo: function (message) {
            document.getElementById('output').innerHTML += "<p>" + message + "</p>";
            return {'msg': 'echo done from index'};
        }
    },
    remote: {}
});
</script>複製程式碼

iframe.html 呼叫 RPC 方法時增加回撥函式即可:

<script>
// ...
document.getElementById('btn').onclick = function () {
    rpc.echo('echo from iframe', function (response) {
        showMsg(response.msg);
    }, function (errorObj) {
        alert('error');
    });
};
</script>複製程式碼

效果如下圖所示:

4.2 主頁面呼叫子頁面方法

iframe.html 中 RPC 的 local 中註冊訪問自己頁面內容的方法 pingIframe

window.rpc = new easyXDM.Rpc({
    // ...
},
{
    local: {
        pingIframe: function (message) {
            showMsg(message);
            return {'msg': 'pong from iframe'}
        }
    },
    remote: {
        echo: {}
    }
});複製程式碼

index.html 中 RPC 的 remote 中註冊子頁面的 pingIframe 方法宣告,增加一下按鈕呼叫事件:

<button id="btn" value="">點選給子頁面發資料</button>
<script>
// ...
var rpc = new easyXDM.Rpc({
    // ...
},
{
    local: {
        // ...
    },
    remote: {
        pingIframe: {}
    }
});

document.getElementById('btn').onclick = function () {
    rpc.pingIframe('ping from index', function(response){
        showMsg(response.msg);
    }, function(errorObj){
        alert('error');
    });
};
</script>複製程式碼

效果如下圖所示:

4.3 主頁面與多個頁面通訊

要做多頁面通訊,只要重複一下類似的相關程式碼呼叫即可。本例項中,複製上面的 iframe.htmliframe2.html 並簡單修改裡面的文字做區分;同時修改 index.html 程式碼如下:

<div id="container"></div>
<button id="btn" value="">點選給子頁面1發資料</button>
<button id="btn2" value="">點選給子頁面2發資料</button>
<div id="output">
    藍色區域為主頁面內容輸出區
</div>
<script src="easyXDM.min.js"></script>
<script>
var showMsg = function (message) {
    document.getElementById('output').innerHTML += "<p>" + message + "</p>";
};
var generateRpc = function (url) {
    return new easyXDM.Rpc({
        isHost: true,
        remote: url,
        hash: true,
        protocol: '1',
        container: document.getElementById('container'),
        props: {
            frameBorder: 0,
            scrolling: 'no',
            style: {width: '100%', height: '100px'}
        }
    },
    {
        local: {
            echo: function (message) {
                showMsg(message);
                return {'msg': 'echo done from index'};
            }
        },
        remote: {
            pingIframe: {}
        }
    });
};
var bindRpc = function(rpc, btnId) {
    document.getElementById(btnId).onclick = function () {
        rpc.pingIframe('ping from index', function (response) {
            showMsg(response.msg);
        }, function (errorObj) {
            alert('error');
        });
    };
};

var rpc1 = generateRpc('http://127.0.0.1/easyXDM/iframe.html');
bindRpc(rpc1, 'btn');

var rpc2 = generateRpc('http://127.0.0.1/easyXDM/iframe2.html');
bindRpc(rpc2, 'btn2');
</script>複製程式碼

效果如下圖所示:

4.4 iframe 切換頁面後保持 RPC 通訊

hash 設定為 false 時不做額外處理的情況下,當提交子頁面裡的 form 或點選子頁面裡的超連結開啟新頁面後,會發現與父視窗的通訊走不通了。究其原因,是因為切換頁面後,通訊通道相關的 xdm_e / xdm_c / xdm_p 引數丟掉了,導致無法保持通訊。解決辦法就是,在新開啟的頁面網址中將通道引數傳遞過去。為方便起見,引入 jQuery 庫,程式碼如下:

/* 使用方法:
 * 1. 將以下程式碼加入到子頁面中
 * 2. 在子頁面的 form 或 a 標籤中增加 easyxdm 類名,將 easyXDM 引數通過網址
 * 傳遞給新頁面以保持頁面跳轉後跨域通訊能保持
 */
$(document).ready(function () {
    $('form.easyxdm').each(function () {
        var $form = $(this);
        var action = $form.attr('action');
        $form.attr('action', action + window.location.hash);
    });
    $('a.easyxdm').each(function () {
        var $link = $(this);
        var href = $link.attr('href');
        $link.attr('href', href + window.location.hash);
    });
});複製程式碼

5、easyXDM 庫的除錯

使用 easyXDM 庫過程中如果遇到一些未知錯誤,可以通過載入除錯庫來做前端除錯,步驟如下:

  1. 從 easyXDM GitHub 庫 ( github.com/oyvindkinse… ) 拉取完整分支
  2. src 目錄複製到自己的程式碼目錄下
  3. 在引入 easyXDM 庫的地方改為引入 easyXDM.debug.js
  4. 之後就可以利用 Chrome 瀏覽器進行 JavaScript 除錯了。具體除錯方法本文不做展開,有興趣的同學可以參考《前端 Chrome 瀏覽器除錯總結》( www.jianshu.com/p/b25c5b88b… ) 的 “Sources 資源頁面的斷點除錯” 部分。

本文完整程式碼下載:pan.baidu.com/s/1cpRlim

6、尾註

因為 easyXDM 庫本身 README.md 已經很久沒有維護更新,導致一些引數含義無法找到;文件對於原理實現未做講解,筆者在使用過程遇到了不少問題,只能通過程式碼除錯和閱讀程式碼的方式深入瞭解其實現原理來解決。本文即是筆者使用 easyXDM 的一些總結,供各位看官參考。

7、參考文件:

  1. easyXDM官網 easyxdm.net
  2. easyXDM GitHub庫 github.com/oyvindkinse…

作者:南智敏
簡介:百姓網營收技術團隊成員。

本文僅為作者個人觀點,不代表百姓網立場。
題圖作者:Pic2.me


本文在 “百姓網技術團隊” 微信公眾號首發,掃碼立即訂閱:

完美解決前端跨域之 easyXDM 的使用和解析

相關文章