preface

Page preview in a low-code editor often has to use iframe to import the external link URL, which involves the problem of data transmission between the preview page and the editor page. The most commonly used scheme is postMessage transmission, which itself is also a macro task in eventloop. The purpose of this article is to summarize some of the practices associated with postMessage in the project, and to provide some ideas for avoiding the pitfalls of using postMessage to deliver data.

scenario

The large screen of the private network self-service project is deployed on another URL, so the UI preview scheme has to use iframe for nesting. In this case, a series of information such as token needs to be transmitted to the large screen. In this case, postMessage is used for value transmission

case

[Bug Description] Data cannot be transferred through simulated click events during postMessage transmission

Vue and React implement their own event mechanisms, but do not trigger the actual event mechanism of the browser

[Solution] Use setTimeout processing, put the event processing in the idle phase of the browser to trigger the callback function processing, need to pay attention to the size of message passing

repetition

Referenced address

Start a static service, a page in iframe, using Express

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta  name="viewport" content="width=device-width, Initial-scale =1.0"> <title>3000</title> </head> <body> <h1> This is a front-end BFF application </h1> <div id="oDiv"> </div> <script> console.log('name', window.name) oDiv.innerHTML = window.name; // window.addEventListener('message', function(e){ // console.log('data', e.data) // oDiv.innerHTML = e.data; // }) </script> </body> </html>Copy the code

The message is displayed on the page when it comes back

Vue applications

Vue-cli launches a simple file that introduces the IFrame page

<template> <div id="container"> <iframe ref="ifr" id="ifr" :name="name" :allowfullscreen="full" :width="width" <p> Your browser does not support iframes</p > </iframe> </div> </template> <script> export default { props: { src: { type: String, default: '', }, width: { type: String | Number, }, height: { type: String | Number, }, id: { type: String, default: '', }, content: { type: String, default: '', }, full: { type: Boolean, default: false, }, name: { type: String, default: '', }, }, mounted() { // this.postMessage() this.createPost() }, methods: { createPost() { const btn = document.createElement('a') btn.setAttribute('herf', 'javascript:; ') btn.setAttribute( 'onclick', "document.getElementById('ifr').contentWindow.postMessage('123', '*')" ) btn.innerHTML = 'postMessage' document.getElementById('container').appendChild(btn) btn.click() // document.getElementById('container').removeChild(btn) }, postMessage() { document.getElementById('ifr').contentWindow.postMessage('123', '*') } }, } </script> <style> </style>Copy the code

The react application

Use create-react-app to start a React application, using functional components and class components

Functional component

// functional component import {useRef, useEffect } from 'react' const createBtn = () => { const btn = document.createElement('a') btn.setAttribute('herf', 'javascript:; ') btn.setAttribute( 'onclick', "document.getElementById('ifr').contentWindow.postMessage('123', '*')", ) btn.innerHTML = 'postMessage' document.getElementById('container').appendChild(btn) btn.click() // document.getElementById('container').removeChild(btn) } const Frame = (props) => { const { name, full, width, height, src } = props const ifr = useRef(null) useEffect(() => { createBtn() }, []) return ( <div id="container"> <iframe id="ifr" width="100%" height="540px" src="http://localhost:3000" Your browser does not support iframes</p > </iframe> </div>)} export default FrameCopy the code

Class components

Import React from 'React' const createBtn = () => {const BTN = document.createElement('a'); btn.setAttribute('herf', 'javascript:; ') btn.setAttribute( 'onclick', "document.getElementById('ifr').contentWindow.postMessage('123', '*')", ) btn.innerHTML = 'postMessage' document.getElementById('container').appendChild(btn) btn.click() // document.getElementById('container').removeChild(btn) } class OtherFrame extends React.Component { constructor(props) { super(props) } componentDidMount() { createBtn() } render() { return ( <div id="container"> <iframe id="ifr" Width ="100%" height="540px" SRC ="http://localhost:3000" frameBorder="0" > <p> Your browser does not support iframes</p > </iframe> </div>)}}  export default OtherFrameCopy the code

Native applications

Writing with native JS, you can either create button binding events or bind events with an A tag without any impact

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta  name="viewport" content="width=device-width, Initial-scale =1.0"> <title> native JS </title> <script> </script> </head> <body> <iframe id="ifr" SRC ="http://localhost:3000" width="100%" height="540px" frameborder="0"></iframe> <! -- <script> window.onload = function() { const btn = document.createElement('button'); btn.innerHTML = 'postMessge' btn.addEventListener('click', function() { ifr.contentWindow.postMessage('123', "*") }) document.body.appendChild(btn) btn.click() // document.body.removeChild(btn) } </script> --> <script> window.onload = function () { const btn = document.createElement('a') btn.setAttribute('herf', 'javascript:; ') btn.setAttribute( 'onclick', "document.getElementById('ifr').contentWindow.postMessage('123', '*')" ) btn.innerHTML = 'postMessage' document.body.appendChild(btn) btn.click() // document.body.removeChild(btn) } </script> </body> </html>Copy the code

The source code

The previous examples used mock click events to display tags clearly, and found that it was possible to fetch messages from page clicks (via page handles). However, both Vue and React proxies for events, so attachEvent cannot be used to generate tag adding events

vue

A package on wrapListeners is a notice on the Vue, and the essence of wrapListeners is a bindObjectListeners' renderHelper method, $on = function(event, fn) {if(array.isarray (event)) {for(let I =0, l=event.length; i < l; i++) { this.$on(event[i], fn) } } else { (this._events[event] || this._events[event] = []).push(fn) } return this; } Vue.prototype.$off = function (event, fn) { // all if (! arguments.length) { this._events = Object.create(null) return this } // array of events if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { this.$off(event[i], fn) } return this } // specific event const cbs = this._events[event] if (! cbs) { return this } if (! fn) { this._events[event] = null return this } // specific handler let cb let i = cbs.length while (i--) { cb = cbs[i] if (cb === fn || cb.fn === fn) { cbs.splice(i, 1) break } } return this } \ Vue.prototype.$emit = function (event) { let cbs = this._events[event] if (cbs) { cbs = cbs.length > 1 ? toArray(cbs) : cbs const args = toArray(arguments, 1) } return this }Copy the code

react

Function createSyntheticEvent(Interface: EventInterfaceType) {function SyntheticBaseEvent(reactName: string | null, reactEventType: string, targetInst: Fiber, nativeEvent: {[propName: string]: mixed}, nativeEventTarget: null | EventTarget, ) { this._reactName = reactName; this._targetInst = targetInst; this.type = reactEventType; this.nativeEvent = nativeEvent; this.target = nativeEventTarget; this.currentTarget = null; for (const propName in Interface) { if (! Interface.hasOwnProperty(propName)) { continue; } const normalize = Interface[propName]; if (normalize) { this[propName] = normalize(nativeEvent); } else { this[propName] = nativeEvent[propName]; } } const defaultPrevented = nativeEvent.defaultPrevented ! = null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false; if (defaultPrevented) { this.isDefaultPrevented = functionThatReturnsTrue; } else { this.isDefaultPrevented = functionThatReturnsFalse; } this.isPropagationStopped = functionThatReturnsFalse; return this; } Object.assign(SyntheticBaseEvent.prototype, { preventDefault: function() { this.defaultPrevented = true; const event = this.nativeEvent; if (! event) { return; } if (event.preventDefault) { event.preventDefault(); } else if (typeof event.returnValue ! == 'unknown') { event.returnValue = false; } this.isDefaultPrevented = functionThatReturnsTrue; }, stopPropagation: function() { const event = this.nativeEvent; if (! event) { return; } if (event.stopPropagation) { event.stopPropagation(); } else if (typeof event.cancelBubble ! == 'unknown') { event.cancelBubble = true; } this.isPropagationStopped = functionThatReturnsTrue; }, persist: function() { }, isPersistent: functionThatReturnsTrue, }); return SyntheticBaseEvent; }Copy the code

chromium

The implementation of PostMessage in Chromium mainly realizes message monitoring and distribution through message in CAST


#include "components/cast/message_port/cast_core/message_port_core_with_task_runner.h"




#include "base/bind.h"

#include "base/logging.h"

#include "base/sequence_checker.h"

#include "base/threading/sequenced_task_runner_handle.h"




namespace cast_api_bindings {




namespace {

static uint32_t GenerateChannelId() {

// Should theoretically start at a random number to lower collision chance if

// ports are created in multiple places, but in practice this does not happen

static std::atomic<uint32_t> channel_id = {0x8000000};

return ++channel_id;

}

} // namespace




std::pair<MessagePortCoreWithTaskRunner, MessagePortCoreWithTaskRunner>

MessagePortCoreWithTaskRunner::CreatePair() {

auto channel_id = GenerateChannelId();

auto pair = std::make_pair(MessagePortCoreWithTaskRunner(channel_id),

MessagePortCoreWithTaskRunner(channel_id));

pair.first.SetPeer(&pair.second);

pair.second.SetPeer(&pair.first);

return pair;

}




MessagePortCoreWithTaskRunner::MessagePortCoreWithTaskRunner(

uint32_t channel_id)

: MessagePortCore(channel_id) {}




MessagePortCoreWithTaskRunner::MessagePortCoreWithTaskRunner(

MessagePortCoreWithTaskRunner&& other)

: MessagePortCore(std::move(other)) {

task_runner_ = std::exchange(other.task_runner_, nullptr);

}




MessagePortCoreWithTaskRunner::~MessagePortCoreWithTaskRunner() = default;




MessagePortCoreWithTaskRunner& MessagePortCoreWithTaskRunner::operator=(

MessagePortCoreWithTaskRunner&& other) {

task_runner_ = std::exchange(other.task_runner_, nullptr);

Assign(std::move(other));




return *this;

}




void MessagePortCoreWithTaskRunner::SetTaskRunner() {

task_runner_ = base::SequencedTaskRunnerHandle::Get();

}




void MessagePortCoreWithTaskRunner::AcceptOnSequence(Message message) {

DCHECK(task_runner_);

DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);

task_runner_->PostTask(

FROM_HERE,

base::BindOnce(&MessagePortCoreWithTaskRunner::AcceptInternal,

weak_factory_.GetWeakPtr(), std::move(message)));

}




void MessagePortCoreWithTaskRunner::AcceptResultOnSequence(bool result) {

DCHECK(task_runner_);

DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);

task_runner_->PostTask(

FROM_HERE,

base::BindOnce(&MessagePortCoreWithTaskRunner::AcceptResultInternal,

weak_factory_.GetWeakPtr(), result));

}




void MessagePortCoreWithTaskRunner::CheckPeerStartedOnSequence() {

DCHECK(task_runner_);

DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);

task_runner_->PostTask(

FROM_HERE,

base::BindOnce(&MessagePortCoreWithTaskRunner::CheckPeerStartedInternal,

weak_factory_.GetWeakPtr()));

}




void MessagePortCoreWithTaskRunner::StartOnSequence() {

DCHECK(task_runner_);

DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);

task_runner_->PostTask(FROM_HERE,

base::BindOnce(&MessagePortCoreWithTaskRunner::Start,

weak_factory_.GetWeakPtr()));

}




void MessagePortCoreWithTaskRunner::PostMessageOnSequence(Message message) {

DCHECK(task_runner_);

DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);

task_runner_->PostTask(

FROM_HERE,

base::BindOnce(&MessagePortCoreWithTaskRunner::PostMessageInternal,

weak_factory_.GetWeakPtr(), std::move(message)));

}




void MessagePortCoreWithTaskRunner::OnPipeErrorOnSequence() {

DCHECK(task_runner_);

DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_);

task_runner_->PostTask(

FROM_HERE,

base::BindOnce(&MessagePortCoreWithTaskRunner::OnPipeErrorInternal,

weak_factory_.GetWeakPtr()));

}




bool MessagePortCoreWithTaskRunner::HasTaskRunner() const {

return !!task_runner_;

}




}

Copy the code

conclusion

PostMessage seems to be simple, but it actually contains the browser’s event loop mechanism and the different VM frameworks’ event handling. Event handling is a deep problem for the front end. From js single-threaded non-blocking asynchronous paradigm to the VM framework event agent and a variety of js event store (such as EventEmitter, co, etc.), has been through in all aspects of the front, hit the pit in the project not only seek to solve the problem, more important is that we get by on pit for cognitive ascension of the whole programming thought, Learning the processing mode of different bosses, flexible use, to enhance their own technical strength and code elegance, mutual encouragement!!

reference

  • MDN official document

  • Chromium source

  • Iframe, let’s talk about it

  • The vUE parent gets data asynchronously and passes it to the child