curTain

最近在学习 QQ 互联第三方登录时有这样一个场景,我们需要新开一个页面,这个页面是 QQ 互联提供的扫码登录的页面,登录成功后,页面会重定向到我们服务端的路由上,后续的流程是:关闭登录页面,原先页面获取用户登录信息,但是新开的页面如何与之前的页面通信呢?原先的页面如何知道登录成功了呢?答案就是页面通信。

体验地址

同源页面间的跨页面通信

1. Broadcast Channel

BroadCast Channel 可以帮我们创建一个用于广播的通信频道。当所有页面都监听同一频道的消息时,其中某一个页面通过它发送的消息就会被其他所有页面收到

实现流程:

  1. 创建 name 相同的 BroadcastChannel 对象
  2. 使用 BC.onmessage 监听事件
  3. 使用 BC.postMessage 触发事件

代码实现:

1
2
3
4
5
6
7
8
9
10
11
// a.html
const bc = new BroadcastChannel('tan');
bc.onmessage = function (e) {
const data = e.data;
console.log(data);
};

// b.html
const bc = new BroadcastChannel('tan');
const msg = { name: 'yuuu..' };
bc.postMessage(msg);

tips

使用 BroadcastChannel.postMessage 方法不能传递含有函数和 Symbol 类型的值

2. Service Worker

Service Worker 是一个可以长期运行在后台的 Worker,能够实现与页面的双向通信。多页面共享间的 Service Worker 可以共享,将 Service Worker 作为消息的处理中心(中央站)即可实现广播效果。

实现流程

  1. 两个页面使用同一个文件注册 worker
  2. 使用 navigator.serviceWorker 进行事件监听
  3. 使用 navigator.serviceWorker.controller 或者实例对象触发事件

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// a.html b.html
navigator.serviceWorker.register('./service.sw.js', { scope: './' }).then(a => {
console.log('------注册成功', a);
a.active.postMessage('------aaaaaa');
});

navigator.serviceWorker.addEventListener('message', function (e) {
console.log('主页aaaa--', e.data);
});

// service.js
this.addEventListener('install', function (event) {
console.log('Service Worker install');
});

this.addEventListener('message', function (e) {
console.log('service worker receive message', e.data);
// 让线程持续进行
e.waitUntil(
self.clients.matchAll().then(function (clients) {
if (!clients || clients.length === 0) {
return;
}
clients.forEach(function (client) {
client.postMessage(e.data);
});
})
);
});

tips

ServicepostMessage 方法无法拷贝函数和 Symbol 值,

ExtendableEvent.waitUntil() 函数的解析

3. LocalStorage

当 LocalStorage 变化时,会触发 storage 事件。利用这个特性,我们可以在发送消息时,把消息写入到某个 LocalStorage 中;然后在各个页面内,通过监听 storage 事件即可收到通知。

实现流程

  1. a 页面监听 storage 事件
  2. b 页面使用 window.localStorage 写入值

实现代码

1
2
3
4
5
6
7
8
9
10
11
// a.html
window.addEventListener('storage', e => {
console.log('写入事件触发', this.localStorage.length);
console.log('-----', e);
});

// b.html
function write() {
console.log('------写入---');
window.localStorage.setItem('name', 'tan--11111-');
}

广播模式小结

通过上述例子,可知广播模式的特点是会向外发送一个事件,其他页面监听到事件发生时即执行对应的函数,且页面之间可以互相发送信息。

4. shared Worker

普通的 Worker 之间是独立运行、数据互不相通;而多个 Tab 注册的 Shared Worker 则可以实现数据共享。

Shared Worker 在实现跨页面通信时的问题在于,它无法主动通知所有页面,因此,需要使用手动去获取或者通过轮询的方式,来实现页面通信。

因为 Shared Worker 不区分触发事件,所以我们需要通过传参实现事件类型的区分。

实现流程

  1. 使用同一份文件实例化 ShardWorker,且命名一致
  2. 显示调用 MessagePort.start 方法,实现使用addEventListener 监听消息
  3. 使用 MessagePort.postMessage 触发线程函数,获取或修改线程中的数据
  4. Worker 文件中触发 postMessage 将数据返回当前页面

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 页面中
// 实例化对象
const sharedWorker = new SharedWorker('./sdw.js', 'tan');
// 写入数据
function send() {
sharedWorker.port.postMessage({ data: 'aa页面--' });
}
// 获取数据
function get() {
sharedWorker.port.postMessage({ get: true });
}
// 事件监听
sharedWorker.port.addEventListener('message', e => {
console.log('-----主页面data:', e);
});
// 端口开启,使 `addEventListener` 起作用
sharedWorker.port.start();

// worker.js
let data = null;

self.addEventListener('connect', function (e) {
const port = e.ports[0];
port.addEventListener('message', function (event) {
if (event.data.get) {
data && port.postMessage(data);
} else {
data = event.data;
}
});
port.start();
});

Shared Worker 是使用共享内存的方式存储数据,也可以使用 Cookie 和 IndexDB 的方式将数据存储在浏览器,再通过轮询的方式实现页面通信

实现代码

1
2
3
4
5
6
function write1() {
document.cookie = 'name=tan; max-age=10';
}
function read() {
console.log('read cookie:', document.cookie);
}

6. window.open + window.opener

实现原理

父页面使用 window.open 打开页面,该方法将返回打开页面 window 的引用,存下这个引用,调用 postMessage 方法可以向该页面发送消息。

使用 window.open 打开的子页面,在 window 对象上有 parentopener 对象,这两个对象都指向父页面的 window,即可使用该对象向父页面发送消息。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// a.html
const openWins = [];
function myOpen() {
const win = window.open('./b.html');
openWins.push(win);
}
function send() {
openWins.forEach(item => {
item.postMessage({ name: 'parent win data' });
});
}
window.addEventListener('message', e => {
console.log('a页面------', e);
});

// b.html
window.addEventListener('message', e => {
console.log('b页面-----', e);
});

非同源页面间的跨页面通信

7. iframe + 同源通信法

因为浏览器的同源策略,上面的方法都不起作用了,有没有其他方法可以实现非同源页面间的通信呢?

在 HTML 中有 iframe 标签,该标签可以创建一个沙箱环境并打开跨域的页面,在 window 对象上存在 frames 属性,该属性存储页面中 iframe 标签创建的对象,通过该属性,可以获取到 iframe 打开页面的 window 对象,通过该对象,我们即可实现非同源页面间的通信,再使用同源页面通信的方法实现与 iframe 同源页面的通信。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- home page -->
<iframe src="http://localhost:4000/iframe.html"></iframe>
<button onclick="send()">send</button>
<script>
window.addEventListener('message', e => {
console.log('-----监听信息:', e);
});
function send() {
window.frames[0].window.postMessage({ data: 'from parent data' }, '*');
}
</script>

<!-- iframe page -->
<script>
const bc = new BroadcastChannel('yu');
window.addEventListener('message', e => {
console.log('接收到的消息;', e);
bc.postMessage({ data: e.data });
});
</script>

<!-- cors page -->
<script>
const bc = new BroadcastChannel('yu');
bc.onmessage = function (e) {
console.log('-----接收到消息--', e);
};
</script>

总结

同源页面的通信方式如下:

  • 广播模式:BroadcastChannel、ServiceWorker、localStorage+StorageEvent
  • 共享存储模式:SharedWorker、IndexDB、Cookie
  • 口口相传模式:window.open + window.opener
  • 基于服务端模式:Websocket、长连接

非同源页面通信可以使用 iframe 作为 实现。

参考材料

AlienZHOU-面试官:前端跨页面通信,你知道哪些方法?


 评论