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