前言
在低代碼編輯器中進行頁面預覽常常不得不用到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等),一直貫穿在前端的各個方面,在項目中的踩坑不能只是尋求解決問題就可以了,更重要的是我們通過踩坑而獲得對於整個編程思想的認知提升,學習不同大佬的處理模式,靈活運用,才能提升自己的技術實力與代碼優雅程度,共勉!!!
參考
- mdn官方文檔
- chromium源碼
- iframe,我們來談一談
- vue父組件異步獲取數據傳給子組件