两个html页面之间通讯,面试官:前端跨页面通信,你知道哪些方法?

引言

在浏览器中,我们可以同时打开多个Tab页,每个Tab页可以粗略理解为一个“独立”的运行环境,即使是全局对象也不会在多个Tab间共享。然而有些时候,我们希望能在这些“独立”的Tab页面之间同步页面的数据、信息或状态。

正如下面这个例子:我在列表页点击“收藏”后,对应的详情页按钮会自动更新为“已收藏”状态;类似的,在详情页点击“收藏”后,列表页中按钮也会更新。

bda0b7ead248

跨页面通信实例

这就是我们所说的前端跨页面通信。

你知道哪些跨页面通信的方式呢?如果不清楚,下面我就带大家来看看七种跨页面通信的方式。

一、同源页面间的跨页面通信

浏览器的同源策略在下述的一些跨页面通信方法中依然存在限制。因此,我们先来看看,在满足同源策略的情况下,都有哪些技术可以用来实现跨页面通信。

1. BroadCast Channel

BroadCast 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);

2. Service Worker

Service 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. LocalStorage

LocalStorage 作为前端最常用的本地存储,大家应该已经非常熟悉了;但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 Worker

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

Shared Worker 在实现跨页面通信时的问题在于,它无法主动通知所有页面,因此,我们会使用轮询的方式,来拉取最新的数据。思路如下:

让 Shared Worker 支持两种消息。一种是 post,Shared Worker 收到后会将该数据保存下来;另一种是 get,Shared 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 这类的“服务器推”技术来进行同步。这好比将我们的“中央站”从前端移到了后端。

此外,我还针对以上各种方式写了一个 在线演示的 Demo >>

bda0b7ead248

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 作为“桥”的非同源页面间通信模式图。

bda0b7ead248

image

其中“同源跨域通信方案”可以使用文章第一部分提到的某种技术。

总结

今天和大家分享了一下跨页面通信的各种方式。

对于同源页面,常见的方式包括:

广播模式:Broadcast Channe / Service Worker / LocalStorage + StorageEvent

共享存储模式:Shared Worker / IndexedDB / cookie

口口相传模式:window.open + window.opener

基于服务端:Websocket / Comet / SSE 等

而对于非同源页面,则可以通过嵌入同源 iframe 作为“桥”,将非同源页面通信转换为同源页面通信。

本文在分享的同时,也是为了抛转引玉。如果你有什么其他想法,欢迎一起讨论,提出你的见解和想法~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/564375.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL查看或显示数据库

MySQL 中,可使用 SHOW DATABASES 语句来查看或显示当前用户权限范围以内的数据库。查看数据库的语法格式为: SHOW DATABASES [LIKE 数据库名];语法说明如下: LIKE 从句是可选项,用于匹配指定的数据库名称。LIKE 从句可以部分匹配…

默认选中_双击dwg图纸,怎么设置默认天正打开?

文尾左下角阅读原文看视频教程好课推荐:零基础CAD:点我CAD室内:点我 周站长CAD:点我CAD机械:点我 Bim教程:点我CAD建筑:点我CAD三维:点我全屋定制:点我 ps教程&#xff1…

MySQL修改数据库

MySQL 中&#xff0c;使用 ALTER DATABASE 来修改已经被创建或者存在的数据库的相关参数。修改数据库的语法格式为&#xff1a; ALTER DATABASE [数据库名] { [ DEFAULT ] CHARACTER SET <字符集名> | [ DEFAULT ] COLLATE <校对规则名>}语法说明如下&#xff1a…

临颖一高2021高考成绩查询,临颍一高举办2021年决战高考百日冲刺誓师大会

原标题&#xff1a;临颍一高举办2021年决战高考百日冲刺誓师大会3月4日&#xff0c;县一高举办2021年决战高考百日冲刺誓师大会&#xff0c;擂响了百日冲刺的战鼓&#xff0c;全校5800余名师生参加誓师大会。誓师大会在激昂雄壮的国歌声中拉开了序幕。县一高校长巩海生满怀深情…

ddos源码 ntp_详解 NTP反射型DDos攻击

简介NTP Reply Flood Attack (NTP射型Ddos 攻击)以下简称NTP_Flood是一种利用网络中 NTP服务器的脆弱性(无认证&#xff0c;不等价数据交换&#xff0c;UDP协议)&#xff0c;来进行DDos行为的攻击&#xff0c;本文将就此种攻击的产生原因&#xff0c;利用方法等进行阐述&#x…

MySQL删除数据库

MySQL 中&#xff0c;当需要删除已创建的数据库时&#xff0c;可以使用 DROP DATABASE 语句。其语法格式为&#xff1a; DROP DATABASE [ IF EXISTS ] <数据库名>语法说明如下&#xff1a; <数据库名>&#xff1a;指定要删除的数据库名。IF EXISTS&#xff1a;用于…

2021年慈溪中学高考成绩查询,2021年慈溪市高考状元名单资料,今年慈溪市高考状元多少分...

高考状元一直都备受大家的关注&#xff0c;不管对于学校和还是当地教育系统&#xff0c;都是一件荣誉的事情。高考状元历来都诞生于艳羡的目光中&#xff0c;大家为他们的高分叫好&#xff0c;羡慕他们可以一步踏入国内知名学府。本文高考升学网为大家介绍历年慈溪市高考状元的…

网际风全推数据接口_智能风控系统设计与实践

导读在主流互联网产品中&#xff0c;比如搜索和推荐的系统&#xff0c;为了挖掘用户潜在购买需求&#xff0c;缩短用户到商品或信息的距离&#xff0c;提高用户的使用体验&#xff0c;都需要使用大量的特征来刻画用户的行为。在信息安全领域&#xff0c;建立在人工智能技术之上…

MySQL选择数据库

MySQL 中就有很多系统自带的数据库&#xff0c;那么在操作数据库之前就必须要确定是哪一个数据库。 在 MySQL 中&#xff0c;USE 语句用来完成一个数据库到另一个数据库的跳转。 当用 CREATE DATABASE 语句创建数据库之后&#xff0c;该数据库不会自动成为当前数据库&#xf…

esp8266 html文件,ESP8266 基ESP8266_RTOS_SDK (ESP-IDF )中嵌入网页文件(示例代码)

场景&#xff1a;在写ESP8266 web服务的时候&#xff0c;免不了要将自己设计的网页html和css等文件放入到固件中。在arduino中有fs可以进行上传文件&#xff0c;然后通过文件系统读出。那在ESP-IDF中该怎么办呢。有几个思路1. 通过flash_download_tools 直接向固定地址写入文件…

python捕获信号退出_Python捕获信号退出Python中的捕获Ctrl+C/SIGINT,优雅地退出多个进程,python,在,CtrlCSIGINT,并...

用multiprocessing.Pool处理CtrlC/SIGINT的正确方法是&#xff1a;在创建进程Pool之前&#xff0c;使进程忽略SIGINT。这样创建的子进程继承SIGINT处理程序。创建Pool之后&#xff0c;还原父进程中的原始SIGINT处理程序。使用map_async和apply_async而不是阻塞map和apply。等待…

高中计算机教师考试专业知识,高中教师资格证计算机专业考试内容

【导读】高中教师资格证计算机专业考综合素质、教育知识与能力和学科知识与教学能力&#xff0c;学科知识与教学能力是考计算机科学与技术的专业知识。高中教师资格证计算机专业考试内容高中教师资格证计算机专业考综合素质、教育知识与能力和学科知识与教学能力&#xff0c;学…

mysql并发 node_nodejs写入mysql单次数据量过大的解决方法_沃航科技

今天使用nodejs开发mysql程序时&#xff0c;发现了如下bug提示&#xff1a;{ Error: write EPIPEat WriteWrap.afterWrite [as oncomplete] (net.js:782:14)--------------------at Protocol._enqueue (/root/doctracing/node_modules/mysql/lib/protocol/Protocol.js:144:48)a…

MySQL整数类型

整数类型又称数值型数据&#xff0c;数值型数据类型主要用来存储数字。 MySQL 提供了多种数值型数据类型&#xff0c;不同的数据类型提供不同的取值范围&#xff0c;可以存储的值范围越大&#xff0c;所需的存储空间也会越大。 MySQL 主要提供的整数类型有 TINYINT、SMALLINT…

LISP 冻结excel窗格_粗暴讲解,2分钟 | 即懂excel 冻结首行、首列和单元格怎么弄?...

多少人上手excel&#xff0c;都是基本功不扎实啊&#xff1f;&#xff01;想起当初自己学做数据&#xff0c;迫于不可抗力因素&#xff0c;糊里糊涂上岗。没囤过基础知识&#xff0c;全靠业务实践练操作。领导甩来什么需求&#xff0c;当天就要查各种方法给他做出来&#xff0c…

MySQL小数类型

MySQL 中使用浮点数和定点数来表示小数。 浮点类型有两种&#xff0c;分别是单精度浮点数&#xff08;FLOAT&#xff09;和双精度浮点数&#xff08;DOUBLE&#xff09;&#xff1b;定点类型只有一种&#xff0c;就是 DECIMAL。 浮点类型和定点类型都可以用(M, D)来表示&…

html怎么引入圆角插件,jQuery圆角插件demo页面 张鑫旭-鑫空间-鑫生活

Auto-Ready!一、可用的模式默认的圆角样式表现roundRound$(this).corner();Bevel$(this).corner("bevel");Notch$(this).corner("notch");Bite$(this).corner("bite");Cool$(this).corner("cool");Sharp$(this).corner("sharp&qu…

html怎么设置锯齿边框样式,CSS3实现边框锯齿效果

通过CSS3的linear-gradient实现的.bg{width:300px;height:50px;background:#caca8c;background-image:-webkit-gradient(linear,50% 0,0 100%,from(transparent),color-stop(.5,transparent),color-stop(.5,#d86707),to(#d86707)),-webkit-gradient(linear,50% 0,100% 100%,fro…

linux windows文件 编码_Mac, Windows和Linux电脑之间如何快速传输文件

Mac, Windows和Linux电脑之间如何快速传输文件本文介绍的方法主要适用于在家庭环境中的多台电脑之间快速传输文件&#xff0c;比如家中有三台电脑分别是Windows, Mac和Linux系统&#xff0c;如果传输少量的小文件&#xff0c;那么可以直接使用微信等通讯软件就可以了。但是如果…

MySQL字符串类型

MySQL 中的字符串类型有 CHAR、VARCHAR、TINYTEXT、TEXT、MEDIUMTEXT、LONGTEXT、ENUM、SET 等。 下表中列出了 MySQL 中的字符串数据类型&#xff0c;括号中的M表示可以为其指定长度。 VARCHAR 和 TEXT 类型是变长类型&#xff0c;其存储需求取决于列值的实际长度&#xff…