postMessage踩坑实践

1,993 阅读5分钟

前言

在低代码编辑器中进行页面预览常常不得不用到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等),一直贯穿在前端的各个方面,在项目中的踩坑不能只是寻求解决问题就可以了,更重要的是我们通过踩坑而获得对于整个编程思想的认知提升,学习不同大佬的处理模式,灵活运用,才能提升自己的技术实力与代码优雅程度,共勉!!!

参考