我们在 Service Worker 中监听了message事件,获取页面(从 Service Worker 的角度叫 client)发送的信息。然后通过self.clients.matchAll()获取当前注册了该 Service Worker 的所有页面,通过调用每个client(即页面)的postMessage方法,向页面发送消息。这样就把从一处(某个Tab页面)收到的消息通知给了其他页面。
处理完 Service Worker,我们需要在页面监听 Service Worker 发送来的消息:
navigator.serviceWorker.addEventListener('message', function(e) {
const data = e.data;
...
})
几种跨页面通信的方案
之前根据浏览器原理的学习可以知道页面的每一个tab都是由单独的进程去控制的,有自己独立的内存空间,存储信息,数据,状态等。
当我们使用场景需要进行页面通讯的时候,比如:我有一个列表页,然后用新开页签打开这个列表中的一项的详情,然后我在列表页修改了一下它的名称,那么详情页展示的名称也应该响应的修改;再比如我做站内通讯,当前页面有2条消息提示,新开一个页面以后,在新开页已读了两条消息,那原先页面的消息也应该变成已读状态。
那么怎么做这种跨页面的通讯呢?
一、同源页面间的跨页面通讯
1.BroadCast Channel
BroadCast Channel 可以帮我们创建一个用于广播的通信频道。当所有页面都监听同一频道的消息时,其中某一个页面通过它发送的消息就会被其他所有页面收到。它的API和用法都非常简单。
首先创建一个
BroadcastChannel
实例,它只有一个参数,就是命名广播名称的字符串然后要发送消息时只需要调用它的
postMessage
方法就可以需要接收消息的页面通过
onmessage
事件来进行监听对错误事件的监听通过
onmessageerror
事件来进行监听如果想要关闭
BroadcastChannel
可以通过两种方式,一种是将onmessage
事件清除掉,但是这种方式只是不再接收信息,实例还是存在的。另一种是通过它提供的
close
方法进行关闭。对浏览器的支持程度
2.Service Worker
Service Worker 是一个可以长期运行在后台的 Worker,能够实现与页面的双向通信。多页面共享间的 Service Worker 可以共享,将 Service Worker 作为消息的处理中心(中央站)即可实现广播效果。
首先,需要在页面注册 Service Worker:
sw.js
是对应的Service Worker脚本。Service Worker 本身并不自动具备“广播通信”的功能,需要我们添加些代码,将其改造成消息中转站:我们在 Service Worker 中监听了
message
事件,获取页面(从 Service Worker 的角度叫 client)发送的信息。然后通过self.clients.matchAll()
获取当前注册了该 Service Worker 的所有页面,通过调用每个client(即页面)的postMessage
方法,向页面发送消息。这样就把从一处(某个Tab页面)收到的消息通知给了其他页面。处理完 Service Worker,我们需要在页面监听 Service Worker 发送来的消息:
3.LocalStorage
LocalStorage 作为前端最常用的本地存储,大家应该已经非常熟悉了;但
StorageEvent
这个与它相关的事件有些同学可能会比较陌生。当 LocalStorage 变化时,会触发
storage
事件。利用这个特性,我们可以在发送消息时,把消息写入到某个 LocalStorage 中;然后在各个页面内,通过监听storage
事件即可收到通知。当我们需要发送信息的时候,在对应的页面调用
localStorage.setItem
方法即可。这里需要注意一个问题就是storage事件只会在值真正发生改变的时候触发,如果两次修改的值是相同的,则不会触发,所以最好在修改的时候添加一个时间戳作为事件触发的依凭。
4.Shared Worker
Shared Worker 是 Worker 家族的另一个成员。普通的 Worker 之间是独立运行、数据互不相通;而多个 Tab 注册的 Shared Worker 则可以实现数据共享。
Shared Worker 在实现跨页面通信时的问题在于,它无法主动通知所有页面,因此,我们会使用轮询的方式,来拉取最新的数据。思路如下:
让 Shared Worker 支持两种消息。一种是 post,Shared Worker 收到后会将该数据保存下来;另一种是 get,Shared Worker 收到该消息后会将保存的数据通过
postMessage
传给注册它的页面。也就是让页面通过 get 来主动获取(同步)最新消息。具体实现如下:首先,我们会在页面中启动一个 Shared Worker,启动方式非常简单:
然后,在该 Shared Worker 中支持 get 与 post 形式的消息:
之后,页面定时发送 get 指令的消息给 Shared Worker,轮询最新的消息数据,并在页面监听返回信息:
最后,当要跨页面通信时,只需给 Shared Worker postMessage即可:
注意,如果使用
addEventListener
来添加 Shared Worker 的消息监听,需要显式调用MessagePort.start
方法,即上文中的sharedWorker.port.start()
;如果使用onmessage
绑定监听则不需要。5.IndexedDB
除了可以利用 Shared Worker 来共享存储数据,还可以使用其他一些“全局性”(支持跨页面)的存储方案。例如 IndexedDB 或 cookie。
其思路很简单:与 Shared Worker 方案类似,消息发送方将消息存至 IndexedDB 中;接收方(例如所有页面)则通过轮询去获取最新的信息。
首先第一步是创建或打开一个数据库连接:
open()
方法接收两个参数,第一个是数据库名称,如果数据库名称不存在则会创建一个新的数据库,第二个参数是数据库版本号,不传默认为1,要注意的是它可以是一个非常大的整数。但是,它不能是一个小数,否则它将会被转为最近的整数,同时有可能导致onUpgradeneeded
事件不触发(bug)。。当数据库建立连接时,会返回一个
IDBOpenDBRequest
对象。在连接建立成功时,会触发
onsuccess
事件,其中函数参数event
的target
属性就是request
对象。而在数据库创建或者版本更新时,会触发
onupgradeneeded
事件。打开数据库以后要创建一个存储空间:
只能在
onupgradeneeded
回调函数中创建存储空间,而不能在数据库打开后的success
回调函数中创建。通过
createObjectStore
能够创建一个存储空间。接受两个参数:customers
。keyPath
值为存储对象的某个属性,这个属性能够在获取存储空间数据的时候当做key值使用。autoIncrement
指定了key
值是否自增(当key值为默认的从1开始到2^53的整数时)。而
createIndex
能够给当前的存储空间设置一个索引。它接受三个参数:unique
的值为true
表示不允许索引值相等。然后对数据库进行操作:
在
IndexedDB
中,我们也能够使用事务来进行数据库的操作。事务有三个模式(常量已经弃用):readOnly
,只读。readwrite
,读写。versionchange
,数据库版本变化。我们创建一个事务时,需要从上面选择一种模式,如果不指定的话,则默认为只读模式。具体示例如下:
事务函数
transaction
的第一个参数为需要关联的存储空间,第二个可选参数为事务模式。与上面类似,事务成功时也会触发onsuccess
函数,失败时触发onerror
函数。我们做页面通讯的方式就是在要发送信息的页面修改indexedDB中的某条数据的值,然后在需要接收通讯的页面通过定时获取的方式来获得改变的值。
6.window.open + window.opener
当我们使用
window.open
打开页面时,方法会返回一个被打开页面window
的引用。而在未显示指定noopener
时,被打开的页面可以通过window.opener
获取到打开它的页面的引用 —— 通过这种方式我们就将这些页面建立起了联系(一种树形结构)。首先,我们把window.open打开的页面的window对象收集起来:
当我们需要发送消息的时候,作为消息的发起方,一个页面需要同时通知它打开的页面与打开它的页面:
作为接收方需要通过监听
message
事件来进行相应的处理:7.visibilitychange
小结
同源网站的跨页面通信主要通过两种形式,一种是广播的形式,将发送的信息交给一个处理中心,然后由处理中心发给对应的页面。另一种是通过共享存储的方式,将信息存到公共空间,然后再有需要的页面通过定时获取的方式来得到信息的改变。
二、非同源页面之间的通信
随着业务的发展,我们可能会有两个不同域名的产品线,他们之间做通信要通过什么方式呢?
要实现该功能,可以使用一个用户不可见的 iframe 作为“桥”。由于 iframe 与父页面间可以通过指定
origin
来忽略同源限制,因此可以在每个页面中嵌入一个 iframe (例如:http://sample.com/bridge.html
),而这些 iframe 由于使用的是一个 url,因此属于同源页面,其通信方式可以复用上面第一部分提到的各种方式。页面与 iframe 通信非常简单,首先需要在页面中监听 iframe 发来的消息,做相应的业务处理:
然后,当页面要与其他的同源或非同源页面通信时,会先给 iframe 发送消息:
这个方法有两个参数,一个是要发送的消息,第二个参数为指定的url,*表示所有页面。