<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          postMessage 還能這樣玩

          共 11222字,需瀏覽 23分鐘

           ·

          2020-12-04 21:36

          在日常工作中,消息通信是一個(gè)很常見的場(chǎng)景。比如大家熟悉 B/S 結(jié)構(gòu),在該結(jié)構(gòu)下,瀏覽器與服務(wù)器之間是基于 HTTP 協(xié)議進(jìn)行消息通信:

          然而除了 HTTP 協(xié)議之外,在一些對(duì)數(shù)據(jù)實(shí)時(shí)性要求較高的場(chǎng)景下,我們會(huì)使用 WebSocket 協(xié)議來完成消息通信:

          對(duì)于這兩種場(chǎng)景,相信大家都不會(huì)陌生。接下來,阿寶哥將介紹消息通信的另外一種場(chǎng)景,即父頁面與 iframe 加載的子頁面之間,如何進(jìn)行消息通信。

          為什么會(huì)突然寫這個(gè)話題呢?其實(shí)是因?yàn)樵诮陧?xiàng)目中,阿寶哥需要實(shí)現(xiàn)父頁面與 iframe 加載的子頁面之間的消息通信。另外,剛好近期阿寶哥在寫 源碼分析 專題,所以就到 Github 上搜索 ? 了一番,然后找到了一個(gè)不錯(cuò)的項(xiàng)目 —— ?Postmate。

          在閱讀完 Postmate 源碼之后,阿寶哥覺得該項(xiàng)目的一些設(shè)計(jì)思想挺值得借鑒的,所以就寫了這篇文章來跟大家分享一下。閱讀完本文之后,你將學(xué)到以下知識(shí):

          • 消息系統(tǒng)中握手的作用及如何實(shí)現(xiàn)握手;
          • 消息模型的設(shè)計(jì)及如何實(shí)現(xiàn)消息驗(yàn)證來保證通信安全;
          • postMessage 的使用及如何利用它實(shí)現(xiàn)父子頁面的消息通信;
          • 消息通信 API 的設(shè)計(jì)與實(shí)現(xiàn)。

          好的,廢話不多說,我們先來簡(jiǎn)單介紹一下 Postmate。

          一、Postmate 簡(jiǎn)介

          Postmate 是一個(gè)強(qiáng)大,簡(jiǎn)單,基于 Promise 的 postMessage 庫。它允許父頁面以最小的成本與跨域的子 iframe 進(jìn)行通信。該庫擁有以下特性:

          • 基于 Promise 的 API,可實(shí)現(xiàn)優(yōu)雅而簡(jiǎn)單的通信;
          • 使用 消息驗(yàn)證 來保護(hù)雙向 父 <-> 子 消息通信的安全;
          • 子對(duì)象公開父對(duì)象可以訪問的可檢索的模型對(duì)象;
          • 子對(duì)象可派發(fā)父對(duì)象已監(jiān)聽的事件;
          • 父對(duì)象可以調(diào)用子對(duì)象中的函數(shù);
          • 零依賴。如果需要可以為 Promise API 提供自定義 polyfill 或抽象;
          • 輕量,大小約 1.6 KB(minified & gzipped)。

          接下來阿寶哥將從如何進(jìn)行握手、如何實(shí)現(xiàn)雙向消息通信和如何斷開連接,這三個(gè)方面來分析一下 Postmate 這個(gè)庫。另外,在此期間還會(huì)穿插介紹 Postmate 項(xiàng)目中一些好的設(shè)計(jì)思路。

          二、如何進(jìn)行握手

          TCP 建立連接的時(shí)候,需要進(jìn)行三次握手。同樣,當(dāng)父頁面與子頁面通信的時(shí)候,Postmate 也是通過 “握手” 來確保雙方能正常通信。因?yàn)?Postmate 通信的基礎(chǔ)是基于 postMessage,所以在介紹如何握手之前,我們先來簡(jiǎn)單了解一下 postMessage API。

          2.1 postMessage 簡(jiǎn)介

          對(duì)于兩個(gè)不同頁面的腳本,只有當(dāng)執(zhí)行它們的頁面位于具有相同的協(xié)議、端口號(hào)以及主機(jī)時(shí),這兩個(gè)腳本才能相互通信。window.postMessage() 方法提供了一種受控機(jī)制來規(guī)避此限制,只要正確的使用,這種方法就很安全。

          2.1.1 postMessage() 語法
          otherWindow.postMessage(message,?targetOrigin,?[transfer]);
          • otherWindow:其他窗口的一個(gè)引用,比如 iframe 的 contentWindow 屬性、執(zhí)行 window.open 返回的窗口對(duì)象等。
          • message:將要發(fā)送到其他 window 的數(shù)據(jù),它將會(huì)被結(jié)構(gòu)化克隆算法序列化。
          • targetOrigin:通過窗口的 origin 屬性來指定哪些窗口能接收到消息事件,其值可以是字符串 "*"(表示無限制)或者一個(gè) URI。
          • transfer(可選):是一串和 message 同時(shí)傳遞的 Transferable 對(duì)象。這些對(duì)象的所有權(quán)將被轉(zhuǎn)移給消息的接收方,而發(fā)送一方將不再保有所有權(quán)。

          發(fā)送方通過 postMessage API 來發(fā)送消息,而接收方可以通過監(jiān)聽 message 事件,來添加消息處理回調(diào)函數(shù),具體使用方式如下:

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

          function?receiveMessage(event)?{
          ??let?origin?=?event.origin?||?event.originalEvent.origin;?
          ??if?(origin?!==?"http://semlinker.com")?return;
          }

          2.2 Postmate 握手的實(shí)現(xiàn)

          在電信和微處理器系統(tǒng)中,術(shù)語握手(Handshake,亦稱為交握)具有以下含義:

          • 在數(shù)據(jù)通信中,由硬件或軟件管理的事件序列,在進(jìn)行信息交換之前,需要對(duì)操作模式的狀態(tài)互相達(dá)成協(xié)定。
          • 在接收站和發(fā)送站之間建立通信參數(shù)的過程。

          對(duì)于通信系統(tǒng)來說,握手是在通信電路建立之后,信息傳輸開始之前。握手用于達(dá)成參數(shù),如信息傳輸率,字母表,奇偶校驗(yàn), 中斷過程,和其他協(xié)議特性

          而對(duì)于 Postmate 這個(gè)庫來說,握手是為了確保父頁面與 iframe 子頁面之間可以正常的通信,對(duì)應(yīng)的握手流程如下所示:

          在 Postmate 中,握手消息是由父頁面發(fā)起的,在父頁面中要發(fā)起握手信息,首先需要?jiǎng)?chuàng)建 Postmate 對(duì)象:

          const?postmate?=?new?Postmate({
          ??container:?document.getElementById('some-div'),?//?iframe的容器
          ??url:?'http://child.com/page.html',?//?包含postmate.js的iframe子頁面地址
          ??name:?'my-iframe-name'?//?用于設(shè)置iframe元素的name屬性
          });

          在以上代碼中,我們通過調(diào)用 Postmate 構(gòu)造函數(shù)來創(chuàng)建 postmate 對(duì)象,在 Postmate 構(gòu)造函數(shù)內(nèi)部含有兩個(gè)主要步驟:設(shè)置 Postmate 對(duì)象的內(nèi)部屬性和發(fā)送握手消息:

          以上流程圖對(duì)應(yīng)的代碼相對(duì)比較簡(jiǎn)單,這里阿寶哥就不貼詳細(xì)的代碼了。感興趣的小伙伴可以閱讀 src/postmate.js 文件中的相關(guān)內(nèi)容。為了能夠響應(yīng)父頁面的握手信息,我們需要在子頁面中創(chuàng)建一個(gè) Model 對(duì)象:

          const?model?=?new?Postmate.Model({
          ??//?Expose?your?model?to?the?Parent.?Property?values?may?be?functions,?promises,?or?regular?values
          ??height:?()?=>?document.height?||?document.body.offsetHeight
          });

          其中 Postmate.Model 構(gòu)造函數(shù)的定義如下:

          //?src/postmate.js
          Postmate.Model?=?class?Model?{
          ??constructor(model)?{
          ????this.child?=?window;
          ????this.model?=?model;
          ????this.parent?=?this.child.parent;
          ????return?this.sendHandshakeReply();
          ??}
          }

          在 Model 構(gòu)造函數(shù)中,我們可以很清楚地看到調(diào)用 sendHandshakeReply 這個(gè)方法,這里我們只看核心的代碼:

          現(xiàn)在我們來總結(jié)一下父頁面和子頁面之間的握手流程:當(dāng)子頁面加載完成后,父頁面會(huì)通過 postMessage API 向子頁面發(fā)送 handshake 握手消息。在子頁面接收到 handshake 握手消息之后,同樣也會(huì)使用 postMessage API 往父頁面回復(fù) handshake-reply 消息。

          另外,需要注意的是,為了保證子頁面能收到 handshake 握手消息,在 sendHandshake 方法內(nèi)部會(huì)啟動(dòng)一個(gè)定時(shí)器來執(zhí)行發(fā)送操作:

          //?src/postmate.js
          class?Postmate?{
          ??sendHandshake(url)?{
          ????return?new?Postmate.Promise((resolve,?reject)?=>?{
          ??????const?loaded?=?()?=>?{
          ????????doSend();
          ????????responseInterval?=?setInterval(doSend,?500);
          ??????};

          ??????if?(this.frame.attachEvent)?{
          ????????this.frame.attachEvent("onload",?loaded);
          ??????}?else?{
          ????????this.frame.addEventListener("load",?loaded);
          ??????}
          ??????
          ??????this.frame.src?=?url;
          ????});
          ??}
          }

          當(dāng)然為了避免發(fā)送過多無效的握手信息,在 doSend 方法內(nèi)部會(huì)限制最大的握手次數(shù):

          const?doSend?=?()?=>?{
          ??attempt++;
          ??this.child.postMessage(
          ????{
          ??????postmate:?"handshake",
          ??????type:?messageType,
          ??????model:?this.model,
          ????},
          ????childOrigin
          ??);
          ??//?const?maxHandshakeRequests?=?5;
          ??if?(attempt?===?maxHandshakeRequests)?{
          ?????clearInterval(responseInterval);
          ??}
          };

          在主應(yīng)用和子應(yīng)用雙方完成握手之后,就可以進(jìn)行雙向消息通信了,下面我們來了解一下如何實(shí)現(xiàn)雙向消息通信。

          三、如何實(shí)現(xiàn)雙向消息通信

          在調(diào)用 PostmatePostmate.Model 構(gòu)造函數(shù)之后,會(huì)返回一個(gè) Promise 對(duì)象。而當(dāng) Promise 對(duì)象的狀態(tài)從 pending 變?yōu)?resolved 之后,就會(huì)分別返回 ParentAPIChildAPI 對(duì)象:

          Postmate

          //?src/postmate.js
          class?Postmate?{
          ??constructor({
          ????container?=?typeof?container?!==?"undefined"???container?:?document.body,
          ????model,?url,?name,?classListArray?=?[],
          ??})?{
          ????//?省略設(shè)置?Postmate?對(duì)象的內(nèi)部屬性
          ????return?this.sendHandshake(url);
          ??}
          ??
          ??sendHandshake(url)?{
          ????//?省略部分代碼
          ????return?new?Postmate.Promise((resolve,?reject)?=>?{
          ??????const?reply?=?(e)?=>?{
          ????????if?(!sanitize(e,?childOrigin))?return?false;
          ????????if?(e.data.postmate?===?"handshake-reply")?{
          ??????????return?resolve(new?ParentAPI(this));
          ????????}
          ????????return?reject("Failed?handshake");
          ??????};
          ????});
          ??}
          }

          ParentAPI

          class?ParentAPI{
          ??+get(property:?any)?//?獲取子頁面中Model對(duì)象上的property屬性上的值
          ??+call(property:?any,?data:?any)?//?調(diào)用子頁面中Model對(duì)象上的方法
          ??+on(eventName:?any,?callback:?any)?//?監(jiān)聽子頁面派發(fā)的事件
          ??+destroy()?//?移除事件監(jiān)聽并刪除iframe
          }

          Postmate.Model

          //?src/postmate.js
          Postmate.Model?=?class?Model?{
          ??constructor(model)?{
          ????this.child?=?window;
          ????this.model?=?model;
          ????this.parent?=?this.child.parent;
          ????return?this.sendHandshakeReply();
          ??}

          ??sendHandshakeReply()?{
          ????//?省略部分代碼
          ????return?new?Postmate.Promise((resolve,?reject)?=>?{
          ??????const?shake?=?(e)?=>?{
          ????????if?(e.data.postmate?===?"handshake")?{
          ??????????this.child.removeEventListener("message",?shake,?false);
          ??????????return?resolve(new?ChildAPI(this));
          ????????}
          ????????return?reject("Handshake?Reply?Failed");
          ??????};
          ??????this.child.addEventListener("message",?shake,?false);
          ????});
          ??}
          };

          ChildAPI

          class?ChildAPI{
          ??+emit(name:?any,?data:?any)
          }

          3.1 子頁面 -> 父頁面

          3.1.1 子頁面發(fā)送消息
          const?model?=?new?Postmate.Model({
          ??//?Expose?your?model?to?the?Parent.?Property?values?may?be?functions,?promises,?or?regular?values
          ??height:?()?=>?document.height?||?document.body.offsetHeight
          });

          model.then(childAPI?=>?{
          ??childAPI.emit('some-event',?'Hello,?World!');
          });

          在以上代碼中,子頁面可以通過 ChildAPI 對(duì)象提供的 emit 方法來發(fā)送消息,該方法的定義如下:

          export?class?ChildAPI?{
          ??emit(name,?data)?{
          ????this.parent.postMessage(
          ??????{
          ????????postmate:?"emit",
          ????????type:?messageType,
          ????????value:?{
          ??????????name,
          ??????????data,
          ????????},
          ??????},
          ??????this.parentOrigin
          ????);
          ??}
          }
          3.1.2 父頁面監(jiān)聽消息
          const?postmate?=?new?Postmate({
          ??container:?document.getElementById('some-div'),?//?iframe的容器
          ??url:?'http://child.com/page.html',?//?包含postmate.js的iframe子頁面地址
          ??name:?'my-iframe-name'?//?用于設(shè)置iframe元素的name屬性
          });

          postmate.then(parentAPI?=>?{
          ??parentAPI.on('some-event',?data?=>?console.log(data));?//?Logs?"Hello,?World!"
          });

          在以上代碼中,父頁面可以通過 ParentAPI 對(duì)象提供的 on 方法來注冊(cè)事件處理器,該方法的定義如下:

          export?class?ParentAPI?{
          ??constructor(info)?{
          ????this.parent?=?info.parent;
          ????this.frame?=?info.frame;
          ????this.child?=?info.child;

          ????this.events?=?{};

          ????this.listener?=?(e)?=>?{
          ??????if?(!sanitize(e,?this.childOrigin))?return?false;
          ???//?省略部分代碼
          ??????if?(e.data.postmate?===?"emit")?{
          ????????if?(name?in?this.events)?{
          ??????????this.events[name].forEach((callback)?=>?{
          ????????????callback.call(this,?data);
          ??????????});
          ????????}
          ??????}
          ????};

          ????this.parent.addEventListener("message",?this.listener,?false);
          ??}

          ??on(eventName,?callback)?{
          ????if?(!this.events[eventName])?{
          ??????this.events[eventName]?=?[];
          ????}
          ????this.events[eventName].push(callback);
          ??}
          }

          3.2 消息驗(yàn)證

          為了保證通信的安全,在消息處理時(shí),Postmate 會(huì)對(duì)消息進(jìn)行驗(yàn)證,對(duì)應(yīng)的驗(yàn)證邏輯被封裝到 sanitize 方法中:

          const?sanitize?=?(message,?allowedOrigin)?=>?{
          ??if?(typeof?allowedOrigin?===?"string"?&&?message.origin?!==?allowedOrigin)
          ????return?false;
          ??if?(!message.data)?return?false;
          ??if?(typeof?message.data?===?"object"?&&?!("postmate"?in?message.data))
          ????return?false;
          ??if?(message.data.type?!==?messageType)?return?false;
          ??if?(!messageTypes[message.data.postmate])?return?false;
          ??return?true;
          };

          對(duì)應(yīng)的驗(yàn)證規(guī)則如下:

          • 驗(yàn)證消息的來源是否合法;
          • 驗(yàn)證是否含有消息體;
          • 驗(yàn)證消息體中是否含有 postmate 屬性;
          • 驗(yàn)證消息的類型是否為 "application/x-postmate-v1+json"
          • 驗(yàn)證消息體中的 postmate 對(duì)應(yīng)的消息類型是否合法;

          以下是 Postmate 支持的消息類型:

          const?messageTypes?=?{
          ??handshake:?1,?
          ??"handshake-reply":?1,?
          ??call:?1,
          ??emit:?1,?
          ??reply:?1,?
          ??request:?1,
          };

          其實(shí)要實(shí)現(xiàn)消息驗(yàn)證的提前,我們還需要定義標(biāo)準(zhǔn)的消息體模型:

          {
          ???postmate:?"emit",?//?必填:"request"?|?"call"?等等
          ???type:?messageType,?//?必填:"application/x-postmate-v1+json"
          ???//?自定義屬性
          }

          了解完子頁面如何與父頁面進(jìn)行通信及如何進(jìn)行消息驗(yàn)證之后,下面我們來看一下父頁面如何與子頁面進(jìn)行消息通信。

          3.3 父頁面 -> 子頁面

          3.3.1 調(diào)用子頁面模型對(duì)象上的方法

          在頁面中,通過 ParentAPI 對(duì)象提供的 call 方法,我們就可以調(diào)用子頁面模型對(duì)象上的方法:

          export?class?ParentAPI?{
          ?call(property,?data)?{
          ????this.child.postMessage(
          ??????{
          ????????postmate:?"call",
          ????????type:?messageType,
          ????????property,
          ????????data,
          ??????},
          ??????this.childOrigin
          ????);
          ??}
          }

          ChildAPI 對(duì)象中,會(huì)對(duì) call 消息類型進(jìn)行對(duì)應(yīng)的處理,相應(yīng)的處理邏輯如下所示:

          export?class?ChildAPI?{
          ??constructor(info)?{
          ??//?省略部分代碼
          ????this.child.addEventListener("message",?(e)?=>?{
          ??????if?(!sanitize(e,?this.parentOrigin))?return;
          ??????const?{?property,?uid,?data?}?=?e.data;
          ??????
          ??????//?響應(yīng)父頁面發(fā)送的call消息類型,用于調(diào)用Model對(duì)象上的對(duì)應(yīng)方法
          ??????if?(e.data.postmate?===?"call")?{
          ????????if?(
          ??????????property?in?this.model?&&
          ??????????typeof?this.model[property]?===?"function"
          ????????)?{
          ??????????this.model[property](data);
          ????????}
          ????????return;
          ??????}
          ????});
          ??}
          }

          通過以上代碼我們可知,call 消息只能用來調(diào)用子頁面 Model 對(duì)象上的方法并不能獲取方法調(diào)用的返回值。然而在一些場(chǎng)景下,我們是需要獲取方法調(diào)用的返回值,接下來我們來看一下 ParentAPI 是如何實(shí)現(xiàn)這個(gè)功能。

          3.3.2 調(diào)用子頁面模型對(duì)象上的方法并獲取返回值

          若需要獲取調(diào)用后的返回值,我們需要調(diào)用 ParentAPI 對(duì)象上提供的 get 方法:

          export?class?ParentAPI?{
          ?get(property)?{
          ????return?new?Postmate.Promise((resolve)?=>?{
          ??????//?從響應(yīng)中獲取數(shù)據(jù)并移除監(jiān)聽
          ??????const?uid?=?generateNewMessageId();
          ??????const?transact?=?(e)?=>?{
          ????????if?(e.data.uid?===?uid?&&?e.data.postmate?===?"reply")?{
          ??????????this.parent.removeEventListener("message",?transact,?false);
          ??????????resolve(e.data.value);
          ????????}
          ??????};
          ??????
          ??????//?監(jiān)聽來自子頁面的響應(yīng)消息
          ??????this.parent.addEventListener("message",?transact,?false);

          ??????//?向子頁面發(fā)送請(qǐng)求
          ??????this.child.postMessage(
          ????????{
          ??????????postmate:?"request",
          ??????????type:?messageType,
          ??????????property,
          ??????????uid,
          ????????},
          ????????this.childOrigin
          ??????);
          ????});
          ??}
          }

          對(duì)于父頁面發(fā)送的 request 消息,在子頁面中會(huì)通過 resolveValue 方法來獲取返回結(jié)果,然后通過 postMessage 來返回結(jié)果:

          //?src/postmate.js
          export?class?ChildAPI?{
          ??constructor(info)?{
          ????this.child.addEventListener("message",?(e)?=>?{
          ??????if?(!sanitize(e,?this.parentOrigin))?return;
          ??????const?{?property,?uid,?data?}?=?e.data;
          ??????
          ??????//?響應(yīng)父頁面發(fā)送的request消息
          ??????resolveValue(this.model,?property).then((value)?=>
          ????????e.source.postMessage(
          ??????????{
          ????????????property,
          ????????????postmate:?"reply",
          ????????????type:?messageType,
          ????????????uid,
          ????????????value,
          ??????????},
          ??????????e.origin
          ????????)
          ??????);
          ????});
          ??}
          }

          以上代碼中的 resolveValue 方法實(shí)現(xiàn)也很簡(jiǎn)單:

          const?resolveValue?=?(model,?property)?=>?{
          ??const?unwrappedContext?=
          ????typeof?model[property]?===?"function"???model[property]()?:?model[property];
          ??return?Postmate.Promise.resolve(unwrappedContext);
          };

          此時(shí),我們已經(jīng)介紹了 Postmate 如何進(jìn)行握手及如何實(shí)現(xiàn)雙向消息通信,最后我們來介紹一下如何斷開連接。

          四、如何斷開連接

          當(dāng)父頁面與子頁面完成消息通信之后,我們需要斷開連接。這時(shí)我們可以調(diào)用 ParentAPI 對(duì)象上的 destroy 方法來斷開連接。

          //?src/postmate.js
          export?class?ParentAPI?{
          ?destroy()?{
          ????window.removeEventListener("message",?this.listener,?false);
          ????this.frame.parentNode.removeChild(this.frame);
          ??}
          }

          本文阿寶哥以 Postmate 這個(gè)庫為例,介紹了如何基于 postMessage 來實(shí)現(xiàn)父頁面和 iframe 子頁面之間優(yōu)雅的消息通信。如果你還意猶未盡的話,可以閱讀阿寶哥之前寫的與通信相關(guān)的文章:如何優(yōu)雅的實(shí)現(xiàn)消息通信?你不知道的 WebSocket

          五、參考資源

          • MDN - postMessage
          • Github - postmate
          瀏覽 29
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美黄片免费播放 | 影音先锋在线成人片 | 琪琪先锋 torrent magnet 国产精品久久久久久久久久久久久免费看 | 人人摸人人干人人操 | 欧美强奸网站 |