前端和前端聯調的各類姿式,瞭解一下

平時前端都是和後臺聯調,或者在內嵌webview的客戶端上和客戶端聯調,前端和前端聯調是什麼鬼?其實也是存在的,好比另外一個前端寫了一個龐大的模塊(如遊戲、在線ide、可視化編輯頁面等須要沙盒環境的狀況),此時引進來須要使用iframe來使用。在一個大需求裏面,按照模塊化分工的話,顯然iframe裏面的功能由一我的負責,主頁面由另外一我的負責。不一樣的人負責的東西同時展現在頁面上交互,那麼兩個前端開發的過程當中必然有聯調的過程html

背景:父頁面index.html裏面有一個iframe,iframe的src爲子頁面(另外一個html的連接),下文都是基於此狀況下進行前端

傳統方式——iframe的postmessage通訊

// 父頁面的js
    document.querySelector("iframe").onload = () => {
      window.frames[0].postMessage("data from parent", "*");
    };

// 子頁面的js
    window.addEventListener(
      "message",
      e => {
        console.log(e); // e是事件對象,e.data便是父頁面發送的message
      },
      false
    );
複製代碼

這個是比較傳統的方法了。注意的是,addEventListener接收消息的時候,必須首先使用事件對象的origin和source屬性來校驗消息的發送者的身份,若是這裏有差錯,可能會致使跨站點腳本攻擊。並且須要iframe的onload觸發後才能使用postmessagehtml5

iframe的哈希變化通訊

低門檻的一種手段,能夠跨域react

父頁面web

const iframe = document.querySelector("iframe");
    const { src } = iframe;
// 把數據轉字符串,再經過哈希傳遞到子頁面
    function postMessageToIframe(data) {
      iframe.src = `${src}#${encodeURIComponent(JSON.stringify(data))}`;
    }
複製代碼

子iframe頁面json

window.onhashchange = e => {
// 監聽到哈希變化,序列化json
      const data = JSON.parse(decodeURIComponent(location.hash.slice(1)));
      console.log(data, "data >>>>");
    };
複製代碼

打開父頁面,執行postMessageToIframe({ txt: 'i am lhyt' }),便可看見控制檯有子頁面的反饋:跨域

反過來,子頁面給父頁面通訊,使用的是parent:數組

// 子頁面
parent.postMessageToIframe({ name: "from child" })

// 父頁面, 代碼是和子頁面同樣的
    window.onhashchange = () => {
      const data = JSON.parse(decodeURIComponent(location.hash.slice(1)));
      console.log(data, "data from child >>>>");
    };
複製代碼

注意:瀏覽器

  • 父傳子hash通訊,是沒有任何門檻,能夠跨域、能夠直接雙擊打開html
  • 子頁面使用parent的時候,跨域會報錯Uncaught DOMException: Blocked a frame with origin "null" from accessing a cross-origin frame.

onstorage事件

父子iframe頁面通訊

localstorage是瀏覽器同域標籤共用的存儲空間。html5支持一個onstorage事件,咱們在window對象上添加監聽就能夠監聽到變化: window.addEventListener('storage', (e) => console.log(e))安全

須要注意 此事件是非當前頁面對localStorage進行修改時纔會觸發,當前頁面修改localStorage不會觸發監聽函數!!!

// 父頁面
    setTimeout(() => {
      localStorage.setItem("a", localStorage.getItem("a") + 1);
    }, 2000);
    window.addEventListener("storage", e => console.log(e, "parent"));

// 子頁面
window.addEventListener("storage", e => console.log(e, "child"));
複製代碼

打印出來的storageEvent是這樣的:

更騷的操做,本身和本身通訊

都是兩個頁面,要寫兩分html,有沒有辦法不用寫兩個html呢,只須要一個html呢?實際上是能夠的!

給url加上query參數或者哈希,表示該頁面是子頁面。若是是父頁面,那麼建立一個iframe,src是本頁面href加上query參數。父頁面html不須要有什麼其餘標籤,只須要一個script便可

const isIframe = location.search;
    if (isIframe) {
// 子頁面
      window.addEventListener("storage", e => console.log(e, "child"));
    } else {
// 父頁面,建立一個iframe
      const iframe = document.createElement("iframe");
      iframe.src = location.href + "?a=1";
      document.body.appendChild(iframe);
      setTimeout(() => {
        localStorage.setItem("a", localStorage.getItem("a") + 1);
      }, 2000);
      window.addEventListener("storage", e => console.log(e, "parent"));
    }
複製代碼

MessageChannel

MessageChannel建立一個新的消息通道,並經過它的兩個MessagePort 屬性發送數據,並且在 Web Worker 中可用。MessageChannel的實例有兩個屬性,portl1port2。給port1發送消息,那麼port2就會收到。

// 父頁面
    const channel = new MessageChannel();
// 給子頁面的window注入port2
    iframe.contentWindow.port2 = channel.port2;
    iframeonload = () => {
      // 父頁面使用port1發消息,port2會收到
      channel.port1.postMessage({ a: 1 });
    };

// 子頁面,使用父頁面注入的port2
    window.port2.onmessage = e => {
      console.error(e);
    };
複製代碼

MessageChannel優勢: 能夠傳對象,不須要手動序列化和反序列化,並且另外一個port收到的是對象深拷貝

SharedWorker

是worker的一種,此worker能夠被多個頁面同時使用,能夠從幾個瀏覽上下文中訪問,例如幾個窗口、iframe、worker。它具備不一樣的全局做用域——只有一部分普通winodow下的方法。讓多個頁面共享一個worker,使用該worker做爲媒介,便可實現通訊

worker的代碼

// 存放全部的鏈接端口
const everyPorts = [];
onconnect = function({ ports }) {
// onconnect一觸發,就存放到數組裏面
  everyPorts.push(...ports);
// 每次鏈接全部的端口都加上監聽message事件
  [...ports].forEach(port => {
    port.onmessage = function(event) {
// 每次收到message,對全部的鏈接的端口廣播,除了發消息的那個端口
      everyPorts.forEach(singlePort => {
        if (port !== singlePort) {
          singlePort.postMessage(event.data.data);
        }
      });
    };
  });
};
複製代碼

父頁面js代碼

const worker = new SharedWorker("./worker.js");
    window.worker = worker;
    worker.port.addEventListener(
      "message",
      e => {
        console.log("parent:", e);
      },
      false
    );
    worker.port.start();
    setTimeout(() => {
      worker.port.postMessage({
        from: "parent",
        data: {
          a: 111,
          b: 26
        }
      });
    }, 2000);
複製代碼

iframe子頁面的js代碼:

const worker = new SharedWorker("./worker.js");
    worker.port.onmessage = function(e) {
      console.log("child", e);
    };
    worker.port.start();
    setTimeout(() => {
      worker.port.postMessage({ data: [1, 2, 3] });
    }, 1000);
複製代碼

正常狀況下,postMessage發生的時機應該是所有內容onload後執行最好,否則對方還沒load完,還沒綁定事件,就沒有收到onmessage了

SharedWorker也是能夠傳對象的哦

直接注入對象和方法

上面不少例子,都用了contentWindow,既然contentWindow是iframe本身的window,那麼咱們就能夠隨意注入任何內容,供iframe調用了。前端和客戶端聯調,經常使用的方法之一就是注入函數。子頁面調用父頁面的方法,由於有parent這個全局屬性,那麼父頁面的window也是能夠拿到的了

// 父頁面
    document.querySelector("iframe").contentWindow.componentDidMount = () => {
      console.log("iframe did mount");
    };

// 子頁面
    window.onload = () => {
      // 假設這裏有react一系列流程運行...
      setTimeout(() => {
        // 假設如今是react組件didmount的時候
        window.componentDidMount && window.componentDidMount();
      }, 1000);
    };
複製代碼

下面,基於給iframe的window注入方法,來設計一個簡單的通訊模塊

  • 父頁面主動調子頁面, 子頁面被父頁面調
  • 父頁面被子頁面調,子頁面調父頁面

父頁面下,給window掛上parentPageApis對象,是子頁面調用方法的集合。並給子頁面注入一個callParentApi的方法來調父頁面的方法。

const iframe = document.querySelector("iframe");

    window.parentPageApis = window.parentPageApis || {};
    // 父頁面本身給本身注入子頁面調用的方法
    Object.assign(window.parentPageApis, {
      childComponentDidMount() {
        console.log("子頁面did mount");
      },
      changeTitle(title) {
        document.title = title;
      },
      showDialog() {
        // 彈窗
      }
    });

// 給子頁面注入一個callParentApi的方法來調父頁面
    iframe.contentWindow.callParentApi = function(name, ...args) {
      window.parentPageApis[name] && window.parentPageApis[name].apply(null, args);
    };

    iframe.contentWindow.childPageApis =
      iframe.contentWindow.childPageApis || {};
    Object.assign(iframe.contentWindow.childPageApis, {
      // 父頁面也能夠給子頁面注入方法
    });
    setTimeout(() => {
// 調用子頁面的方法
      callChildApi("log", "父頁面調子頁面的log方法打印");
    }, 2000);
複製代碼

子頁面也給父頁面注入callChildApi方法,並把本身的一些對外的方法集合寫在childPageApis上

window.childPageApis = window.childPageApis || {};
    Object.assign(window.childPageApis, {
      // 子頁面本身給本身注入方法
      log(...args) {
        console.log(...args);
      }
    });
    window.parent.window.callChildApi = function(name, ...args) {
      window.childPageApis[name] && window.childPageApis[name].apply(null, args);
    };
    window.onload = () => {
      // 假設這裏有react一系列流程運行...
      setTimeout(() => {
        // 假設如今是react組件didmount的時候,告訴父頁面
        window.callParentApi("childComponentDidMount");
      }, 1000);
    };
複製代碼

最後

以上的storage、SharedWorker的方案,也適用於「不一樣tab通訊」這個問題。總的來講,SharedWorker比較安全,注入全局方法比較靈活,哈希變換通訊比較簡單。postmessage、哈希變化、storage事件都是基於字符串,MessageChannel、SharedWorker能夠傳遞任何「可拷貝的值」。全局注入就能夠隨心所欲了,但也是最危險的,須要作好防範

關注公衆號《不同的前端》,以不同的視角學習前端,快速成長,一塊兒把玩最新的技術、探索各類黑科技

相關文章
相關標籤/搜索