前端和前端聯調的各種姿勢,了解一下

  • 2019 年 12 月 17 日
  • 筆記

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

背景:父頁面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觸發後才能使用postmessage

iframe的哈希變化通信

低門檻的一種手段,可以跨域

父頁面

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

子iframe頁面

    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可以傳遞任何「可拷貝的值」。全局注入就可以為所欲為了,但也是最危險的,需要做好防範