前端常用的幾種跨域通信方式實踐:jsonp&cors&postMessage

什麼是“ 前端 ”“ 跨域 ” ?

常見跨域方式有這麼幾種:jsonp、cors,iframe+domain跨域、以及nginx反向代理,還有就是postMessage。
相比之“基於”前端的其餘幾種方法,iframe+domain和cors的方式不太常用 —— 沒錯,cors一般來講是後端設置,但是完全可以讓前端“一力以擔之”。

前文推薦(不瞭解“跨域”的可以先看這裏):
https://blog.csdn.net/qq_43624878/article/details/95853615


jsonp方式解決跨域問題

jsonp是打破第一重限制,(因爲)用了XMLHttpRequest就跨域,那不用這種方式了,我們來看一段jquery的帶jsonp的ajax請求:

$.ajax({
   type : "GET",
   url : "http://api.map.baidu.com/geocoder/v2/",
   data:"address=河南",
   dataType:"jsonp",
   jsonp:"callback",   //回調函數名默認是callback,可以自定義回調函數名字,#但是必須和後臺保持一致#
   jsonpCallback:"showLocation",  //數據返回成功之後,回調函數的名字是隨機生成的,如jquery0122526({....}) 在這裏自己指定一個(會被替換掉——這一行可以不寫)
   success : function(data){
	   alert("成功");
   },
   error : function(data){
   	   alert("失敗");
   }
});

使用這種類型的話,會創建一個查詢字符串參數 callback=? ,這個參數會加在請求的URL後面。服務器端應當在JSON數據前加上回調函數名,以便完成一個有效的JSONP請求 。

上面代碼看似用了ajax請求,其實內部完全不是那麼回事,多了jsonp和jsonpCallback選項,它內部將代碼翻譯並把頁面上的dom操作成這樣:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <script type='text/javascript'>
      // 後端返回直接執行的方法,相當於執行這個方法,由於後端把返回的數據放在方法的參數裏,所以這裏能拿到res。
      window.showLocation = function (res) {
        console.log(res)
        //執行ajax回調
      }
    </script>
    <script src='http://api.map.baidu.com/geocoder/v2/?address=河南&callback=showLocation' type='text/javascript'></script>
  </body>
</html>

這個時候,html頁面的script src標籤回去訪問api.map.baidu.com的服務端,由於script,img這種標籤是不受瀏覽器xmlhttprequest限制的,可以隨意訪問,這個時候對應的後端代碼取得address等參數,然後根據雙方約定好的callback參數,返回一個被包裝後的json。

cors解決跨域

前面說了,這種方式一般是後端設置的:在後臺添加允許跨域:“跨域資源共享”(Cross-origin resource sharing)。它允許瀏覽器向跨源(協議 + 域名 + 端口)服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。
前面也說了,這種方式完全可以前端獨自完成:

CORS——跨域資源共享
我們可以通過添加 共享站www.corsproxy.com(我們也叫作“請求中轉站”)
這主要是通過設置 Access-Control-Allow-Origin 來進行的。

使用方法: 在“共享站”後面加上url即可!

比如我們上面請求的:http://api.map.baidu.com/geocoder/v2/
現在我們這樣來寫:http://www.corsproxy.com/ api.map.baidu.com/geocoder/v2/

postMessage實現跨域通信

這常被用在比如“聊天機器人”上(至少筆者是這麼幹的…)

window.postMessage()方法可以安全地實現跨源通信。通常,對於兩個不同頁面的腳本,只有當執行它們的頁面位於具有相同協議、端口號以及主機(即兩個頁面的模數Document.domain設置爲相同的值)時,這兩個腳本才能互相通信。
window.postMessage()方法提供了一種受控機制來規避此限制,只要正確的使用,就很安全。

本質上說,postMessage()是基於消息事件機制來實現跨域通信,它隸屬於消息窗體本身,比如window以及window內嵌的frame的window,基本使用形式如下(通常被用在“發送方”頁面中):

someWindow.postMessage(message,targetOrigin,[transfer]);
  • someWindow :窗口的一個引用(一般是新窗口),比如 iframe的contentWindow屬性、執行window.open返回的窗口對象,後者是命名過或數值索引的window.frames
  • message :將要發送到其他window的數據。——不受格式限制,無需自己序列化
  • targetOrigin :通過窗口的origin屬性指定哪些窗口能接收到消息事件,此值可以是字符串“*”(表示無限制)
  • transfer :(可選)是一串和message同時傳遞的Transferable對象

我們可以通過如下方式監聽message(這通常被用在“接收方”頁面中):

window.addEventListener("message", receiveMessage, false);

function receiveMessage(event){
  let origin = event.origin || event.originalEvent.origin; 
  if (origin !== "http://aaa:8080")
    return;
  // ...
  console.log(event.data)
}
// 派發消息的頁面
winB.postMessage(_({text: '休息休息'}), origin)

其中,event中有幾個核心屬性需要注意 如下:

  • data :從其他window中傳遞過來的對象
  • origin :調用postMessage時消息發送方窗口的origin。這個字符串由“協議、😕/、域名、:端口號”拼接而成
  • source :對發送消息的窗口對象的引用

跨域實踐:聊天機器人

此demo只展示核心部分,使用postMessage完成。

我們分別有兩個HTML:a.html和b.html,然後用node分別代理兩個不同頁面,設置不同端口:

//依賴一個http模塊,相當於java中的import
// a.js
var http = require('http');
var fs = require('fs');
var { resolve } = require('path');
//創建一個服務器對象
server = http.createServer(function (req, res) {
//設置請求成功時響應頭部的MIME爲純文本
res.writeHeader(200, {"Content-Type": "text/html"});
//向客戶端(頁面)輸出字符
let data = fs.readFileSync(resolve(__dirname, './a.html'))
res.end(data);
});
//讓服務器監聽本地8000端口開始運行
server.listen(8000,'127.0.0.1');
console.log('http://127.0.0.1:8000')

// b.js
// ...
server.listen(8001,'127.0.0.1');

我們將a.html代理在8000端口下,將b.html代理在8001端口下。
搭建頁面層級:這裏將b頁面以iframe的形式嵌入到a頁面:
在這裏插入圖片描述

(a頁面)

<body>
    <div class="wrap">
        <iframe src="http://127.0.0.1:8001" frameborder="0" id="b"></iframe>
        <div class="control">
            <input type="text" placeholder="請輸入內容" id="ipt">
            <span id="send">發送</span>
        </div>
    </div>
    <script>
        window.onload = function() {
            let origin = 'http://127.0.0.1:8001';
            let _ = (data) => JSON.stringify(data);   //對象函數 & es6箭頭表達式
            let winB = document.querySelector('#b').contentWindow;
            let sendBtn = document.querySelector('#send');
            sendBtn.addEventListener('click', (e) => {
                let text = document.querySelector('#ipt');
                winB.postMessage(_({text: text.value}), origin)
                text.value = '';
            }, false)
            winB.postMessage(_({text: ''}), origin)
        }
    </script>
</body>

通過iframe的contentWindow來拿到b頁面窗體的引用,然後在發送按鈕的點擊事件中觸發postMessage將數據發送給B。

(b頁面)

<body>
    <div class="content">
            <h4>Mxc只能機器人</h4>
            <div class="content-inner"></div>
    </div>
    <script>
        // 語料庫-略
        const pool = [];
        window.addEventListener("message", receiveMessage, false);
        let content = document.querySelector('.content-inner');
        let initContentH = content.scrollHeight;
        let _ = (data) => JSON.stringify(data);
        function createChat(type, mes) {
            let dialog = document.createElement('div');
            dialog.className = type === 0 ? 'dialog robot' : 'dialog user';
            let content =  type === 0 ? `
                <span class="tx">${type === 0 ? 'lab' : 'user'}</span>
                <span class="mes">${mes}</span>
            ` : `
                <span class="mes">${mes}</span>
                <span class="tx">${type === 0 ? 'lab' : 'user'}</span>
            `;
            dialog.innerHTML = content;
            return dialog
        }

        function scrollTop(el, h) {
            if(el.scrollHeight !== h) {
                el.scrollTop = h + 100;
            }
        }

        function receiveMessage(event){
            // 兼容其他瀏覽器
            let origin = event.origin || event.originalEvent.origin; 
            if(origin === 'http://127.0.0.1:8000') {
                let data = JSON.parse(event.data);
                if(data && !data.text) {
                    mes = { text: '你好,我是機器人Lab,請問有什麼可以幫到您的嗎?' };
                    event.source.postMessage(_(mes), event.origin)
                    content.appendChild(createChat(0, mes.text))
                }else {
                    content.appendChild(createChat(1, data.text))
                    scrollTop(content, initContentH)
                    
                    setTimeout(() => {
                        content.appendChild(createChat(0, '正在解決'))
                        scrollTop(content, initContentH)
                    }, 2000);
                } 
            }
        }
    </script>
</body>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章