目录
- 1,需求
- 2,难点
- 3,思路
- 浏览器不同源的页面之间如何跨域通信?
- 4,实现
- 第1版
- 第2版
- 最终版
- 其他的问题
- 1,页面路径需完全一致。
- 2,事件注册问题
1,需求
现在有2个项目,页面路径不同源。
- ToC 的收银台项目
类似在PC端京东淘宝,支付最后一步的收银台页面,可以选择不同支付工具付款。
- ToB 的后台管理项目
可以对收银台项目整体做一些配置:样式,支付工具相关的等等,配置项很多。
需求
- 想要在后台管理项目中增删配置项后,能够实时预览收银台项目最终的展示效果
- 展示效果符合预期,则提交修改配置项的审批电子流
- 审批通过,上线
2,难点
因为2个项目的页面路径不同源,传递数据是个问题。
3,思路
首先,后台管理项目需要增加【预览】按钮,收银台项目需要增加【预览】页面。
-
需要后端参与。收银台项目的相关配置项,本来就是通过接口获取的。所以再增加一个预览数据的接口,在预览页面调用获取数据。
-
不需要后端参与。前端直接在2个页面之间通信。
第1种思路没有什么好说的,重点来说下第2种。
浏览器不同源的页面之间如何跨域通信?
-
通过 url 传参。最简单直接,不过传递的数据大小有限。
-
postMessage,传递的数据大小我实测可以超200MB!(不知道极限,因为没再往上测试)
简单来说,我们可以获取从当前页面A通过window.open
打开的页面B的引用 targetWindowB
,然后在A页面通过 targetWindowB.postMessage()
向B页面分发消息。
再介绍下 window.open
简单说明:window.open
有3个参数,我们只关注前2个
strUrl
:新页面的地址strWindowName
:新页面的名称,如果指定了该参数,则再次调用window.open(strUrl, strWindowName)
时,不会再打开第2个新页面,而是跳转到打开的第1个页面并重新加载。(效果下面会有展示)
另外需要注意:调用window.open()
方法以后,远程 URL 不会被立即载入,载入过程是异步的。
会有什么问题,看下实现过程就知道了。
4,实现
通过 vite 创建2个项目模拟,A 会向 B 发送数据。启动后的页面地址分别是:
- A页面(后台管理项目 manage)
http://localhost:5173
- B页面(收银台项目 cashier)
http://localhost:5174
第1版
A页面后台管理项目 manage
<script setup>
const cashierUrl = "http://localhost:5174";
const data = { name: "下雪天的夏风" };
let cashierWindow;
function init() {
cashierWindow = window.open(cashierUrl, "cashierWindow");
if (cashierWindow) {
cashierWindow.postMessage(data, cashierUrl);
}
}
</script>
<template>
<h1>manage</h1>
<button @click="init">发送预览数据</button>
</template>
B页面收银台项目 cashier
<script setup>
window.addEventListener(
"message",
function (event) {
if (event.origin !== "http://localhost:5173") return;
if (event.data) {
console.log(event.data);
}
},
false
);
</script>
<template>
<h1>cashier</h1>
</template>
效果:
可以看到收银台项目并没有接收到消息!
原因就是:调用window.open()
方法以后,远程 URL 不会被立即载入,载入过程是异步的。
换句话说,因为B页面还没有加载完成,message
事件还没有被绑定时,A页面已经把消息发送了。
第2版
延迟发送消息。
function init() {
cashierWindow = window.open(cashierUrl, "cashierWindow");
setTimeout(() => {
if (cashierWindow) {
const data = { name: "下雪天的夏风" };
cashierWindow.postMessage(data, cashierUrl);
}
}, 1000);
}
效果:
B页面成功收到消息!
问题来了,因为这个测试用例比较简单,所以 1s B页面就会加载完成。
可面对复杂的页面+网络问题,A页面如何知道B页面已经加载完成了(message
事件绑定了)?
答案是:此时B页面可以通过 window.opener 获取 A页面的引用,使用 postMessage
向A页面发送数据!
实现思路:
- B页面加载完成后,通过
window.opener.postMessage()
向A页面发送一个约定字段。 - A页面接收到约定字段后,再向B页面发送目标数据。
最终版
A页面后台管理项目 manage
<script setup>
import { ref } from "vue";
const cashierUrl = "http://localhost:5174";
const cashierLoaded = ref(false);
let cashierWindow;
function init() {
cashierWindow = window.open(cashierUrl, "cashierWindow");
if (cashierLoaded.value) {
requestData();
} else {
window.addEventListener("message", receiveMessage, false);
}
}
function receiveMessage(event) {
if (event.origin !== cashierUrl) return;
cashierLoaded.value = event.data === "__done__";
requestData();
}
const data = { name: "下雪天的夏风" };
function requestData() {
cashierWindow.postMessage(data, cashierUrl);
}
</script>
<template>
<h1>manage</h1>
<button @click="init">发送预览数据</button>
</template>
B页面收银台项目 cashier
<script setup>
const manageUrl = "http://localhost:5173";
if (window.opener) {
window.opener.postMessage("__done__", manageUrl);
}
window.addEventListener(
"message",
function (event) {
if (event.origin !== manageUrl) return;
if (event.data) {
console.log(event.data);
}
},
false
);
</script>
<template>
<h1>cashier</h1>
</template>
效果
其他的问题
1,页面路径需完全一致。
2个不同源页面通信时,要注意设置的 url 要完全一致才能接收到消息。例如 http://localhost:5174
和 http://localhost:5174/
是不一样的!
2,事件注册问题
看下面的代码
function init() {
cashierWindow = window.open(cashierUrl, "cashierWindow");
if (cashierLoaded.value) {
requestData();
} else {
window.addEventListener("message", receiveMessage, false);
}
}
function receiveMessage(event) {
if (event.origin !== cashierUrl) return;
cashierLoaded.value = event.data === "__done__";
requestData();
}
init
方法中,每次都要执行window.open
吗,不能把cashierWindow
保存起来调用requestData
吗?
也可以这样做。但这个例子中是为了每次执行后,默认跳转到 B页面并刷新。
init
方法中,每次都要注册message
事件吗,万一打开的B页面加载较慢,又返回到A页面再次点击发送数据,岂不是又会再次注册事件吗?
确实会再次注册事件,不过没关系,因为注册相同的事件监听器,多余的监听器会被移除,只保留一个。参考
只保留一个的前提是:事件回调函数不能是匿名函数,否则还是会注册多个!所以把receiveMessage
提取出来了。
我们来验证下最终版代码的效果:
而如果监听 message
事件这样写,
window.addEventListener(
"message",
// function receiveMessage(event) { // 效果一样
function(event) {
if (event.origin !== cashierUrl) return;
cashierLoaded.value = event.data === "__done__";
requestData();
},
false
);
再来看下效果:
以上。如果对你有帮助,可以点赞支持下!