postMessage踩坑实践

前言

在低代码编辑器中进行页面预览常常不得不用到iframe进行外链的url引入,这就涉及到了预览页面与编辑器页面数据通信传值的问题,常常用到的方案就是postMessage传值,而postMessage本身在eventloop中也是一个宏任务,就会涉及到浏览器消息队列处理的问题,本文旨在针对项目中的postMessage的相关踩坑实践进行总结,也为想要使用postMessage传递数据的童鞋提供一些避坑思路。

场景

专网自服务项目大屏部署在另外一个url上,因而ui需要预览的方案不得不使用iframe进行嵌套,而这里需要将token等一系列信息传递给大屏,这里采用了postMessage进行传值

案例

[bug描述] 通过postMessage传递过程中,无法通过模拟点击事件进行数据传值

[bug分析] postMessage是宏任务,触发机制会先放到浏览器的消息队列中,然后再进行处理,vue、react都会自己实现自己的事件机制,而不触发真正的浏览器的事件机制

[解决方案] 使用setTimeout处理,将事件处理放在浏览器idle阶段触发回调函数的处理,需要注意传递message的大小

复现

引用的地址

使用express启动了一个静态服务,iframe中的页面





    
    
    
    3000



    

这是一个前端BFF应用

当消息回来后会在页面上进行显示

vue应用

postMessage踩坑实践_第1张图片

vue-cli启动了一个简单的引入iframe页面的文件





react应用

postMessage踩坑实践_第2张图片

使用create-react-app启动了一个react应用,分别通过函数式组件及类组件进行了尝试

函数式组件

// 函数式组件
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 (
    
) } export default Frame

类组件

// 类组件
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 (
      
) } } export default OtherFrame

原生应用

postMessage踩坑实践_第3张图片

使用原生js书写,既可以通过创建button绑定事件又可以通过a标签绑定事件,是没有任何影响的





    
    
    
    原生js
    



    
    
    


源码

上面几个示例使用模拟点击事件为了清晰显示标签,发现通过页面点击事件后(通过页面句柄的方式)是可以进行message信息获取的,但是vue和react都对事件进行了代理,从而无法通过attachEvent来进行自生成标签添加事件

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

postMessage踩坑实践_第4张图片

// 合成事件
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

postMessage踩坑实践_第5张图片

postMessage踩坑实践_第6张图片

chromium中关于postmessage的实现主要通过cast中的message实现了消息的监听与分发

#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 channel_id = {0x8000000};
  return ++channel_id;
}
}  // namespace

std::pair
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_;
}

} 

总结

postMessage看似简单,其实则包内含了浏览器的事件循环机制以及不同VM框架的事件处理方式的不同,事件处理对前端来说是一个值得深究的问题,从js的单线程非阻塞异步范式到VM框架的事件代理以及各种js事件库(如EventEmitter、co等),一直贯穿在前端的各个方面,在项目中的踩坑不能只是寻求解决问题就可以了,更重要的是我们通过踩坑而获得对于整个编程思想的认知提升,学习不同大佬的处理模式,灵活运用,才能提升自己的技术实力与代码优雅程度,共勉!!!

参考

你可能感兴趣的:(postMessage踩坑实践)