站长工具同大全站,推荐网站建设公司,网站建设教程软件,crm客户管理系统方案戳蓝字「前端技术优选」关注我们哦#xff01; 引言在浏览器中#xff0c;我们可以同时打开多个Tab页#xff0c;每个Tab页可以粗略理解为一个“独立”的运行环境#xff0c;即使是全局对象也不会在多个Tab间共享。然而有些时候#xff0c;我们希望能在这些“独立”的Tab页… 戳蓝字「前端技术优选」关注我们哦 引言在浏览器中我们可以同时打开多个Tab页每个Tab页可以粗略理解为一个“独立”的运行环境即使是全局对象也不会在多个Tab间共享。然而有些时候我们希望能在这些“独立”的Tab页面之间同步页面的数据、信息或状态。正如下面这个例子我在列表页点击“收藏”后对应的详情页按钮会自动更新为“已收藏”状态类似的在详情页点击“收藏”后列表页中按钮也会更新。这就是我们所说的前端跨页面通信。你知道哪些跨页面通信的方式呢如果不清楚下面我就带大家来看看七种跨页面通信的方式。一、同源页面间的跨页面通信以下各种方式的 在线 Demo 可以戳这里 浏览器的同源策略在下述的一些跨页面通信方法中依然存在限制。因此我们先来看看在满足同源策略的情况下都有哪些技术可以用来实现跨页面通信。1. BroadCast ChannelBroadCast Channel 可以帮我们创建一个用于广播的通信频道。当所有页面都监听同一频道的消息时其中某一个页面通过它发送的消息就会被其他所有页面收到。它的API和用法都非常简单。下面的方式就可以创建一个标识为AlienZHOU的频道const bc new BroadcastChannel(AlienZHOU);各个页面可以通过onmessage来监听被广播的消息bc.onmessage function (e) {const data e.data;const text [receive] data.msg —— tab data.from;console.log([BroadcastChannel] receive message:, text);};要发送消息时只需要调用实例上的postMessage方法即可bc.postMessage(mydata);Broadcast Channel 的具体的使用方式可以看这篇《【3分钟速览】前端广播式通信Broadcast Channel》。2. Service WorkerService Worker 是一个可以长期运行在后台的 Worker能够实现与页面的双向通信。多页面共享间的 Service Worker 可以共享将 Service Worker 作为消息的处理中心(中央站)即可实现广播效果。Service Worker 也是 PWA 中的核心技术之一由于本文重点不在 PWA 因此如果想进一步了解 Service Worker可以阅读我之前的文章【PWA学习与实践】(3) 让你的WebApp离线可用。首先需要在页面注册 Service Worker/* 页面逻辑 */navigator.serviceWorker.register(../util.sw.js).then(function () {console.log(Service Worker 注册成功);});其中../util.sw.js是对应的 Service Worker 脚本。Service Worker 本身并不自动具备“广播通信”的功能需要我们添加些代码将其改造成消息中转站/* ../util.sw.js Service Worker 逻辑 */self.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); }); }) );});我们在 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;const text [receive] data.msg —— tab data.from;console.log([Service Worker] receive message:, text);});最后当需要同步消息时可以调用 Service Worker 的postMessage方法/* 页面逻辑 */navigator.serviceWorker.controller.postMessage(mydata);3. LocalStorageLocalStorage 作为前端最常用的本地存储大家应该已经非常熟悉了但StorageEvent这个与它相关的事件有些同学可能会比较陌生。当 LocalStorage 变化时会触发storage事件。利用这个特性我们可以在发送消息时把消息写入到某个 LocalStorage 中然后在各个页面内通过监听storage事件即可收到通知。window.addEventListener(storage, function (e) {if (e.key ctc-msg) {const data JSON.parse(e.newValue);const text [receive] data.msg —— tab data.from;console.log([Storage I] receive message:, text); }});在各个页面添加如上的代码即可监听到 LocalStorage 的变化。当某个页面需要发送消息时只需要使用我们熟悉的setItem方法即可mydata.st (new Date);window.localStorage.setItem(ctc-msg, JSON.stringify(mydata));注意这里有一个细节我们在mydata上添加了一个取当前毫秒时间戳的.st属性。这是因为storage事件只有在值真正改变时才会触发。举个例子window.localStorage.setItem(test, 123);window.localStorage.setItem(test, 123);由于第二次的值123与第一次的值相同所以以上的代码只会在第一次setItem时触发storage事件。因此我们通过设置st来保证每次调用时一定会触发storage事件。小憩一下上面我们看到了三种实现跨页面通信的方式不论是建立广播频道的 Broadcast Channel还是使用 Service Worker 的消息中转站抑或是些 tricky 的storage事件其都是“广播模式”一个页面将消息通知给一个“中央站”再由“中央站”通知给各个页面。在上面的例子中这个“中央站”可以是一个 BroadCast Channel 实例、一个 Service Worker 或是 LocalStorage。下面我们会看到另外两种跨页面通信方式我把它称为“共享存储轮询模式”。4. Shared WorkerShared Worker 是 Worker 家族的另一个成员。普通的 Worker 之间是独立运行、数据互不相通而多个 Tab 注册的 Shared Worker 则可以实现数据共享。Shared Worker 在实现跨页面通信时的问题在于它无法主动通知所有页面因此我们会使用轮询的方式来拉取最新的数据。思路如下让 Shared Worker 支持两种消息。一种是 postShared Worker 收到后会将该数据保存下来另一种是 getShared Worker 收到该消息后会将保存的数据通过postMessage传给注册它的页面。也就是让页面通过 get 来主动获取(同步)最新消息。具体实现如下首先我们会在页面中启动一个 Shared Worker启动方式非常简单// 构造函数的第二个参数是 Shared Worker 名称也可以留空const sharedWorker new SharedWorker(../util.shared.js, ctc);然后在该 Shared Worker 中支持 get 与 post 形式的消息/* ../util.shared.js: Shared Worker 代码 */let data null;self.addEventListener(connect, function (e) {const port e.ports[0];port.addEventListener(message, function (event) {// get 指令则返回存储的消息数据if (event.data.get) { data port.postMessage(data); }// 非 get 指令则存储该消息数据else { data event.data; } });port.start();});之后页面定时发送 get 指令的消息给 Shared Worker轮询最新的消息数据并在页面监听返回信息// 定时轮询发送 get 指令的消息setInterval(function () {sharedWorker.port.postMessage({get: true});}, 1000);// 监听 get 消息的返回数据sharedWorker.port.addEventListener(message, (e) {const data e.data;const text [receive] data.msg —— tab data.from;console.log([Shared Worker] receive message:, text);}, false);sharedWorker.port.start();最后当要跨页面通信时只需给 Shared Worker postMessage即可sharedWorker.port.postMessage(mydata);注意如果使用addEventListener来添加 Shared Worker 的消息监听需要显式调用MessagePort.start方法即上文中的sharedWorker.port.start()如果使用onmessage绑定监听则不需要。5. IndexedDB除了可以利用 Shared Worker 来共享存储数据还可以使用其他一些“全局性”(支持跨页面)的存储方案。例如 IndexedDB 或 cookie。鉴于大家对 cookie 已经很熟悉加之作为“互联网最早期的存储方案之一”cookie 已经在实际应用中承受了远多于其设计之初的责任我们下面会使用 IndexedDB 来实现。其思路很简单与 Shared Worker 方案类似消息发送方将消息存至 IndexedDB 中接收方(例如所有页面)则通过轮询去获取最新的信息。在这之前我们先简单封装几个 IndexedDB 的工具方法。打开数据库连接function openStore() {const storeName ctc_aleinzhou;return new Promise(function (resolve, reject) {if (!(indexedDB in window)) {return reject(don\t support indexedDB); }const request indexedDB.open(CTC_DB, 1);request.onerror reject;request.onsuccess e resolve(e.target.result);request.onupgradeneeded function (e) {const db e.srcElement.result;if (e.oldVersion 0 !db.objectStoreNames.contains(storeName)) {const store db.createObjectStore(storeName, {keyPath: tag});store.createIndex(storeName Index, tag, {unique: false}); } } });}存储数据function saveData(db, data) {return new Promise(function (resolve, reject) {const STORE_NAME ctc_aleinzhou;const tx db.transaction(STORE_NAME, readwrite);const store tx.objectStore(STORE_NAME);const request store.put({tag: ctc_data, data});request.onsuccess () resolve(db);request.onerror reject; });}查询/读取数据function query(db) {const STORE_NAME ctc_aleinzhou;return new Promise(function (resolve, reject) {try {const tx db.transaction(STORE_NAME, readonly);const store tx.objectStore(STORE_NAME);const dbRequest store.get(ctc_data);dbRequest.onsuccess e resolve(e.target.result);dbRequest.onerror reject; }catch (err) {reject(err); } });}剩下的工作就非常简单了。首先打开数据连接并初始化数据openStore().then(db saveData(db, null))对于消息读取可以在连接与初始化后轮询openStore().then(db saveData(db, null)).then(function (db) {setInterval(function () {query(db).then(function (res) {if (!res || !res.data) {return; }const data res.data;const text [receive] data.msg —— tab data.from;console.log([Storage I] receive message:, text); }); }, 1000);});最后要发送消息时只需向 IndexedDB 存储数据即可openStore().then(db saveData(db, null)).then(function (db) {// …… 省略上面的轮询代码// 触发 saveData 的方法可以放在用户操作的事件监听内saveData(db, mydata);});小憩一下在“广播模式”外我们又了解了“共享存储长轮询”这种模式。也许你会认为长轮询没有监听模式优雅但实际上有些时候使用“共享存储”的形式时不一定要搭配长轮询。例如在多 Tab 场景下我们可能会离开 Tab A 到另一个 Tab B 中操作过了一会我们从 Tab B 切换回 Tab A 时希望将之前在 Tab B 中的操作的信息同步回来。这时候其实只用在 Tab A 中监听visibilitychange这样的事件来做一次信息同步即可。下面我会再介绍一种通信方式我把它称为“口口相传”模式。6. window.open window.opener当我们使用window.open打开页面时方法会返回一个被打开页面window的引用。而在未显示指定noopener时被打开的页面可以通过window.opener获取到打开它的页面的引用 —— 通过这种方式我们就将这些页面建立起了联系(一种树形结构)。首先我们把window.open打开的页面的window对象收集起来let childWins [];document.getElementById(btn).addEventListener(click, function () {const win window.open(./some/sample);childWins.push(win);});然后当我们需要发送消息的时候作为消息的发起方一个页面需要同时通知它打开的页面与打开它的页面// 过滤掉已经关闭的窗口childWins childWins.filter(w !w.closed);if (childWins.length 0) {mydata.fromOpenner false;childWins.forEach(w w.postMessage(mydata));}if (window.opener !window.opener.closed) {mydata.fromOpenner true;window.opener.postMessage(mydata);}注意我这里先用.closed属性过滤掉已经被关闭的 Tab 窗口。这样作为消息发送方的任务就完成了。下面看看作为消息接收方它需要做什么。此时一个收到消息的页面就不能那么自私了除了展示收到的消息它还需要将消息再传递给它所“知道的人”(打开与被它打开的页面):需要注意的是我这里通过判断消息来源避免将消息回传给发送方防止消息在两者间死循环的传递。(该方案会有些其他小问题实际中可以进一步优化)window.addEventListener(message, function (e) {const data e.data;const text [receive] data.msg —— tab data.from;console.log([Cross-document Messaging] receive message:, text);// 避免消息回传if (window.opener !window.opener.closed data.fromOpenner) {window.opener.postMessage(data); }// 过滤掉已经关闭的窗口 childWins childWins.filter(w !w.closed);// 避免消息回传if (childWins !data.fromOpenner) {childWins.forEach(w w.postMessage(data)); }});这样每个节点(页面)都肩负起了传递消息的责任也就是我说的“口口相传”而消息就在这个树状结构中流转了起来。小憩一下显然“口口相传”的模式存在一个问题如果页面不是通过在另一个页面内的window.open打开的(例如直接在地址栏输入或从其他网站链接过来)这个联系就被打破了。除了上面这六个常见方法其实还有一种(第七种)做法是通过 WebSocket 这类的“服务器推”技术来进行同步。这好比将我们的“中央站”从前端移到了后端。关于 WebSocket 与其他“服务器推”技术不了解的同学可以阅读这篇《各类“服务器推”技术原理与实例(Polling/COMET/SSE/WebSocket)》此外我还针对以上各种方式写了一个 在线演示的 Demo 二、非同源页面之间的通信上面我们介绍了七种前端跨页面通信的方法但它们大都受到同源策略的限制。然而有时候我们有两个不同域名的产品线也希望它们下面的所有页面之间能无障碍地通信。那该怎么办呢要实现该功能可以使用一个用户不可见的 iframe 作为“桥”。由于 iframe 与父页面间可以通过指定origin来忽略同源限制因此可以在每个页面中嵌入一个 iframe (例如http://sample.com/bridge.html)而这些 iframe 由于使用的是一个 url因此属于同源页面其通信方式可以复用上面第一部分提到的各种方式。页面与 iframe 通信非常简单首先需要在页面中监听 iframe 发来的消息做相应的业务处理/* 业务页面代码 */window.addEventListener(message, function (e) {// …… do something});然后当页面要与其他的同源或非同源页面通信时会先给 iframe 发送消息/* 业务页面代码 */window.frames[0].window.postMessage(mydata, *);其中为了简便此处将postMessage的第二个参数设为了*你也可以设为 iframe 的 URL。iframe 收到消息后会使用某种跨页面消息通信技术在所有 iframe 间同步消息例如下面使用的 Broadcast Channel/* iframe 内代码 */const bc new BroadcastChannel(AlienZHOU);// 收到来自页面的消息后在 iframe 间进行广播window.addEventListener(message, function (e) {bc.postMessage(e.data);});其他 iframe 收到通知后则会将该消息同步给所属的页面/* iframe 内代码 */// 对于收到的(iframe)广播消息通知给所属的业务页面bc.onmessage function (e) {window.parent.postMessage(e.data, *);};下图就是使用 iframe 作为“桥”的非同源页面间通信模式图。其中“同源跨域通信方案”可以使用文章第一部分提到的某种技术。总结今天和大家分享了一下跨页面通信的各种方式。对于同源页面常见的方式包括广播模式Broadcast Channe / Service Worker / LocalStorage StorageEvent共享存储模式Shared Worker / IndexedDB / cookie口口相传模式window.open window.opener基于服务端Websocket / Comet / SSE 等而对于非同源页面则可以通过嵌入同源 iframe 作为“桥”将非同源页面通信转换为同源页面通信。本文在分享的同时也是为了抛转引玉。如果你有什么其他想法欢迎一起讨论提出你的见解和想法~