头图

前言

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

场景

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

案例

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

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

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

复现

引用的地址

使用express启动了一个静态服务,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>

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

vue应用

图片

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

<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应用

图片

使用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 (
    <div id="container">
      <iframe
        id="ifr"
        width="100%"
        height="540px"
        src="http://localhost:3000"
        frameBorder="0"
      >
        <p>你的浏览器不支持iframes</p >
      </iframe>
    </div>
  )
}

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 (
      <div id="container">
        <iframe
          id="ifr"
          width="100%"
          height="540px"
          src="http://localhost:3000"
          frameBorder="0"
        >
          <p>你的浏览器不支持iframes</p >
        </iframe>
      </div>
    )
  }
}

export default OtherFrame

原生应用

图片

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

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

源码

上面几个示例使用模拟点击事件为了清晰显示标签,发现通过页面点击事件后(通过页面句柄的方式)是可以进行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

图片

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

图片

图片

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

} 

总结

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

参考


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

专注大前端领域发展