头图

Preface

Page previews in low-code editors often have to use iframes for external link url introduction, which involves the problem of data communication between the preview page and the editor page. The frequently used solution is to pass the value of postMessage, and postMessage It is also a macro task in eventloop, which will involve the problem of browser message queue processing. This article aims to summarize the related pit practice of postMessage in the project, and also provide some avoidance for children who want to use postMessage to transfer data. Pit ideas.

Scenes

The large screen of the private network self-service project is deployed on another url, so the ui that needs to be previewed has to be nested using iframe, and here a series of information such as token needs to be passed to the large screen, here postMessage is used to transfer the value

Case study

[bug description] In the process of passing postMessage, data cannot be passed by simulating a click event

[bug analysis] postMessage is a macro task. The trigger mechanism will be placed in the browser's message queue first, and then processed. Vue and react will implement their own event mechanism by themselves, instead of triggering the actual browser event mechanism.

[Solution] Use setTimeout processing, put the event processing in the browser idle stage to trigger the processing of the callback function, and you need to pay attention to the size of the message passed

recurrent

Referenced address

Use express to start a static service, the page in the iframe

<!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>
        这是一个前端BFF应用
    </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>

When the message comes back, it will be displayed on the page

vue application

图片

vue-cli launched a simple file to import iframe pages

<template>
  <div id="container">
    <iframe
      ref="ifr"
      id="ifr"
      :name="name"
      :allowfullscreen="full"
      :width="width"
      :height="height"
      :src="src"
      frameborder="0"
    >
      <p>你的浏览器不支持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>

react app

图片

Use create-react-app to start a react application, and try it through functional components and class components.

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"
        frameBorder="0"
      >
        <p>你的浏览器不支持iframes</p >
      </iframe>
    </div>
  )
}

export default Frame

Class component

// 类组件
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>你的浏览器不支持iframes</p >
        </iframe>
      </div>
    )
  }
}

export default OtherFrame

Native application

图片

Using native js to write, you can bind events by creating buttons and binding events by a tag, which has no effect

<!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>原生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>

Source code

The above several examples use simulated click events in order to clearly display the label. It is found that the message information can be obtained through the page click event (by way of the page handle), but both vue and react proxy the event, so that the attachEvent cannot be used. Carry out self-generated label adding event

vue

// on实现原理
// v-on是一个指令,vue中通过wrapListeners进行了一个包裹,而wrapListeners的本质是一个bindObjectListeners的renderHelper方法,将事件名称放在了一个listeners监听器中

Vue.prototype.$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
  }

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;
}

chromium

图片

图片

The implementation of postmessage in chromium mainly realizes the monitoring and distribution of messages through the message in the 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_;
}

} 

Summarize

postMessage seems simple, but in fact it contains the browser’s event loop mechanism and the difference in event processing methods of different VM frameworks. Event processing is a problem worthy of further investigation for the front-end, starting from the single-threaded non-blocking asynchronous paradigm of js The event proxy to the VM framework and various js event libraries (such as EventEmitter, co, etc.) have been running through all aspects of the front-end. The stepping in the project can’t just seek to solve the problem, and more importantly, we can step on To gain a better understanding of the entire programming idea, learn the processing modes of different bigwigs, and use them flexibly, can you improve your technical strength and code elegance, and encourage each other! ! !

refer to


维李设论
1.1k 声望4k 粉丝

专注大前端领域发展