背景
最近遇到一个需求:点击链接打开一个不同源的页面,并且需要传递数据到新页面
思考
- 打开页面 可以通过
window.open
打开,也可以通过点击<a>
标签打开 - 传递数据 两个页面之间传递数据的方式有
方式 | 适用场景 | 局限性 |
---|---|---|
通过 URL 参数传递 | ||
cookie | 同源,发请求时需要带到请求头上 | 有大小限制(4K) |
sessionStorage | 1. 同源 2. 在 session 有效期内使用,不需要持久保存数据 3. 发请求时不需要带上 | 1. 通过复制窗口打开的页面,sessionStorage 不共用 2. 会话结束后,清除数据 3. 只能同源使用 |
localStorage | 1. 同源 2. 需要持久保存数据 3. 需要跨页签共享 4. 发请求时不需要带上 | 1. 有大小限制 2. 只能同源使用 |
postMessage | 可以跨域传递数据 | 不对来源进行限制时,有安全风险 |
MessageChannel | 两个页面(可以不同源)之间通信;web-worker 中可用 | |
BroadcastChannel | 同源的不同浏览器窗口,Tab 页,frame 或者 iframe 下的不同文档之间相互通信;web-worker 中可用 | 只能用于同源页面 |
SharedWorker | 两个页面之间通信;web-worker 中可用 | 只能用于同源页面:developer.mozilla.org/zh-CN/docs/... |
由于需要跨域,所以可能的方案只有:通过 URL 参数传递,postMessage,MessageChannel
1. 通过 URL 传递数据
e.g.
A 页面打开 B 页面
window.open('http://localhost:8081?key=value')
优点 | 简单,直观 |
---|---|
缺点 | 明文显示参数,不安全 URL 参数有长度限制 |
结论 | 能实现功能,但是不建议采用 |
2. 通过 postMessage 传递数据(不借助 MessageChannel)
source: http://localhost:8080 target: http://localhost:8081
通过 window.open
打开新页面
发送
- 通过
window.open
拿到 target 窗口的引用 postMessage
, 注意第二个参数 targetOrigin 要和 target 域完全一致
javascript
const other = window.open("http://localhost:8081");
setTimeout(() => {
other.postMessage(
{
csrf_token: "bearer 666",
},
"http://localhost:8081"
);
}, 5000);
接收
- 用
onmessage
或者addEventListener
监听 message 事件 - 通过
e.origin
获取到 source 的域名,进行判断 - 通过
e.data
获取到数据
javascript
window.addEventListener(
"message",
(e) => {
if (e.origin !== "http://localhost:8080") {
return;
}
this.data = e.data;
},
false
);
结论
- 打开新页面后,延迟发送数据,造成等待,体验不好,且不够稳定(时延设置得过短,可能会偶现接收不到数据的问题)
- 能实现功能,但是不建议采用
3. postMessage+MessageChannel
本方式是对 postMessage
的优化,两个页面通过事件通信,避免手动设置延迟
source: http://localhost:8080 target: http://localhost:8081
- 通过
window.open
打开新页面 - 新页面打开后,向原页面发送消息(表示新页面已就绪,请求传输数据)
- 原页面接收到新页面发送的消息后,向新页面发送数据
- 新页面接收老页面发送的数据后,渲染到页面上
2 和 3 用顺序图表示如下:
细化
- 如何在新页面打开后才向原页面发送消息?
在新页面的 load 事件回调中发送消息 可以通过 window.onload
或者 window.addEventListener
- 新页面如何向原页面发送消息?
使用 window.opener.postMessage
函数
- MessageChannel 的 port1,port2 如何使用?
新页面使用 port1 发送第一条消息(就绪消息) 原页面使用 port1 发送第二条消息(数据消息) 新页面监听 port2 的 message 事件,拿到第二条消息
- 顺序图的 3 步如何实现?
window.opener.postMessage
传入第 3 个参数:[port1] 原页面监听 message
事件时,通过 e.ports
得到 port1,使用 port1 发送第二条消息(数据消息) 新页面监听 port2 的 message
事件,通过 e.data
拿到数据,渲染到页面上
细化后的逻辑如下
新页面打开后,在 load
事件回调中,创建 MessageChannel
对象,获取到 port1 和 port2。向原页面发送消息(表示新页面已就绪,请求传输数据)和 port1(作为 postMessage
的第 3 个参数 transfer) 原页面监听 message
事件,接收到新页面发送的消息后,向新页面发送数据 新页面监听 port2 的 message
事件,通过 e.data 拿到数据,渲染到页面上
实现代码如下
数据发送端
source: http://localhost:8080
javascript
window.addEventListener("message", (e) => {
if (e.origin !== "http://localhost:8081") {
return;
}
if (e.origin !== "http://localhost:8081") {
return;
}
const [port1] = e.ports;
if (e.data === "8081 loaded") {
port1.postMessage({
foo: "bar",
});
}
});
数据接收端
target: http://localhost:8081
javascript
window.addEventListener(
"message",
(e) => {
if (e.origin !== "http://localhost:8080") {
return;
}
this.data = e.data;
},
false
);
window.onload = () => {
const { port1, port2 } = new MessageChannel();
window.opener.postMessage("8081 loaded", "http://localhost:8080", [port1]);
port2.onmessage = (e) => {
this.data = e.data;
};
};
注: port2 可以用 onmessage
,也可以用 addEventListener('message',(e)=>{})
如果用了后者,需要调用 port2.start()
如果用的 onmessage
,会自动调用 port2.start()
port2 用addEventListener('message')
来监听的代码如下
javascript
port2.addEventListener("message", (e) => {
this.data = e.data;
});
port2.start();
效果如下
发送的消息为
json
{
"foo": "bar"
}
结论
postMessage+MessageChannel 可以采用