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