背景:有两个项目,一个基于开发,一个基于
Vue
开发。
React
项目中某个页面(称为
Vue
)通过
父页面
标签嵌入了
iframe
项目的某个页面(称为
React
)。现在子页面要和父页面要进行通信,传递数据。
子页面
文章目录
- 关键技术
- 最佳实践
-
- 初步方案
- 改善方案
- 寻求其他方案
- 终极方案
- 展望方案
- 总结
- 源码
关键技术
跨域方式还挺多的,本次主要通过
H5
提供的postMessage()方法解决上面提到的问题。
最佳实践
初步方案
- Vue 父页面核心代码
// PostMessageVueSide.vue
<template>
<div class="container border-2 border-gray-400 border-solid">
<h1>这是基于Vue的页面(父页面)</h1>
<p><span class="text-red-500">传递的数据:</span>{{message}}</p>
<div class="w-full h-full">
<iframe class="w-full h-full" ref="parentPage" scrolling="no" src="http://localhost:3001"></iframe>
</div>
</div>
</template>
<script>
export default {
data () {
return {
message: 'vue page data'
}
},
mounted() {
this.$refs.parentPage.contentWindow.postMessage(this.message, '*');
}
}
</script>
<style lang="scss" scoped>
.container {
min-height: calc(100vh - 100px);
}
</style>
基于Vue的项目启动服务后的地址在http://localhost:8080/。
http://localhost:3001是基于React的项目启动服务后的地址。
当页面挂在完成,我们通过
$fefs
获取
iframe
实例,并通过
contentWindow.postMessage
向子页面发送数据。
- React 子页面核心代码
// PostMessageReactSide.jsx
import React, {useEffect, useState} from 'react';
import './Home.scss'
const Home = () => {
const [message, setMessage] = useState('');
useEffect(() =>{
window.addEventListener('message', (e) =>{
console.log(e.data);
setMessage(e.data);
})
}
);
return (
<div className="container">
<p>这是基于React的页面(子页面)</p>
<p><span className="received-data">收到的数据:</span>{message}</p>
</div>
)
}
export default Home;
React
界面基于React Hook实现。
在
useEffect
函数中我们监听
message
事件,并试图获取从父页面发来的数据。
- 界面效果
从网上看到的大多数基于
postMessage
跨域的教程基本上都是这个流程。
但是,从页面效果可看到,我们的子页面并没有拿到从父页面拿到数据。
为什么呢?请往下看:
我们进行
debug
,可以看到当我们准备要向子页面发送数据时,此时子页面还没加载出来。
因此,如果我们想要在子页面拿到父页面的数据,必须等子页面加载完成后,父页面再向子页面发送数据。
改善方案
通过
load
事件,在页面加载完成时,再发送数据。
// PostMessageVueSide.vue
mounted() {
console.log('mounted');
const parentPage = this.$refs.parentPage;
parentPage.addEventListener('load', ()=> {
parentPage.contentWindow.postMessage(this.message, '*');
})
// this.$refs.parentPage.contentWindow.postMessage(this.message, '*');
}
通过
debug
可以看到当我们准备从父页面发送数据时,子页面已经渲染出来。
因此,接下来我们就拿到了父页面的数据:
在实际工作的项目中,
React
界面往往没有这么简单,页面需要加载的东西很多。
在实际工作中用上面的方案可能是下面的结果:
所以,用这种方案,我们在子页面还是不能拿到数据。
寻求其他方案
我们还可以用定时器来延时发送数据。这里我们延时设置为
3000ms
// PostMessageVueSide.vue
mounted() {
console.log('mounted');
setTimeout(()=>{
this.$refs.parentPage.contentWindow.postMessage(this.message, '*');
}, 3000);
}
这种方案是可行的,数据是能拿到。
但因为子页面加载较快,过了一会儿数据才显示。
这种方案的缺陷很明显,我们不确定子页面究竟要多少
ms
才能加载完成。
终极方案
既然上面我们不确定子页面究竟要多少
ms
才能加载完成。那我们可以等子页面加载完成时向父页面发送一个信号,告诉父页面“我已经加载完成,你可以向我发送数据了”。
那这里我们就需要从子页面通过
postMessage
向父页面发送数据了。
在子页面中我们在
useEffect
中发送数据通知父页面,子页面已加载完成。
// PostMessageReactSide.jsx
const Home = () => {
const [message, setMessage] = useState('');
useEffect(() => {
// 页面加载已完成,通知父页面
console.log('child page mounted');
window.parent.postMessage({retcode: 200}, '*');
});
useEffect(() =>{
window.addEventListener('message', (e) =>{
console.log(e.data);
setMessage(e.data);
})
}
);
return (
<div className="container">
<p>这是基于React的页面(子页面)</p>
<p><span className="received-data">收到的数据:</span>{message}</p>
</div>
)
}
父页面中监听
message
,当拿到子页面的数据时(说明子页面已加载完成),我们就向子页面发送数据。
// PostMessageVueSide.vue
mounted() {
console.log('parent page mounted');
window.addEventListener('message', (e) => {
if (e.data.retcode === 200) {
this.$refs.parentPage.contentWindow.postMessage(this.message, '*');
}
})
}
刷新页面:
完美解决了问题。
展望方案
父子页面来回传递数据容易造成数据混乱,难以管理,数据流不明确。
我们可以设计一个
顶层数据点
,让父页面和子页面都从这个
顶层数据点
拿数据及修改数据等。这样可以保证父子界面拿到的数据是同步的。
例如,父子页面都可以从后台调统一接口拿数据,而不用让数据在父子页面间来回传递。
总结
用
postMessage
解决跨域问题最关键的点就是,父页面要在子页面加载完成时再发送数据,否则子页面可能收不到数据。
源码
- https://gitee.com/LynnHg/best-practice-demo