# 跨标签页通信
# 引言
在浏览器中,我们可以同时打开多个 Tab 页,每个 Tab 页可以粗略理解为一个 “独立” 的运行环境,即使是全局对象也不会在多个 Tab 间共享。然而有些时候,我们希望能在这些 “独立” 的 Tab 页面之间同步页面的数据、信息或状态。
比如:我在列表页点击 “收藏” 后,对应的详情页按钮会自动更新为 “已收藏” 状态;类似的,在详情页点击 “收藏” 后,列表页中按钮也会更新。这就是我们所说的前端跨页面通信。
# 一、同源页面间的跨页面通信
# 1.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 事件。
# 2.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));
}
});
这样,每个节点(页面)都肩负起了传递消息的责任,也就是我说的 “口口相传”,而消息就在这个树状结构中流转了起来。
# 二、非同源页面之间的通信
上面我们介绍了两种前端跨页面通信的方法,但它们大都受到同源策略的限制。然而有时候,我们有两个不同域名的产品线,也希望它们下面的所有页面之间能无障碍地通信。那该怎么办呢?
要实现该功能,可以使用一个用户不可见的 iframe 作为 桥 。由于 iframe 与父页面间可以通过指定 origin 来忽略同源限制,因此可以在每个页面中嵌入一个 iframe (例如:http://sample.com/bridge.html),而这些 iframe 由于使用的是一个 url,因此属于同源页面,其通信方式可以复用上面第一部分提到的各种方式。
页面与 iframe 通信非常简单,首先需要在页面中监听 iframe 发来的消息,做相应的业务处理:
/* 业务页面代码 */
window.addEventListener("message", function (e) {
// …… do something
});
然后,当页面要与其他的同源或非同源页面通信时,会先给 iframe 发送消息:
postMessage 可以安全的实现跨源通信。
/* 业务页面代码 */
window.frames[0].window.postMessage(mydata, "*");
// mydata 将要发送到其他 window的数据
// 第二个参数:指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。
其中为了简便此处将 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 作为 “桥” 的非同源页面间通信模式图。
# 总结
对于同源页面,常见的方式包括:
广播模式: LocalStorage + StorageEvent
口口相传模式:window.open + window.opener
而对于非同源页面,则可以通过嵌入同源 iframe 作为 “桥”,将非同源页面通信转换为同源页面通信。
本文节选自 https://juejin.cn/post/6844903811232825357 作者:AlienZHOU
# 路由的两种模式:hash 和 history
# 引言
为什么要使用路由
现在的网络应用程序越来越多的使用 AJAX 异步请求完成页面的无缝刷新,导致浏览器的 URL 不会发生任何变化而完成了请求,从而破换了用户浏览体验。同时本次浏览的页面内容在用户下次使用 URL 访问时将无法重新呈现,使用路由可以很好地解决这个问题。
单页面应用利用了 JavaScript 动态变换网页内容,避免了页面重载;路由则提供了浏览器地址变化,网页内容也跟随变化,两者结合起来则为我们提供了体验良好的单页面 web 应用。
# 前端路由实现方式
路由需要实现三个功能:
- 1. 当浏览器地址变化时,切换页面;
- 2. 点击浏览器【后退】、【前进】按钮,网页内容跟随变化;
- 3. 刷新浏览器,网页加载当前路由对应内容;
在单页面 web 网页中,单纯的浏览器地址改变,网页不会重载,如单纯的 hash 网址改变网页不会变化,因此我们的路由主要是通过监听事件,并利用 js 实现动态改变网页内容,有两种实现方式:
- 1.hash 模式:监听浏览器地址 hash 值变化,执行相应的 js 切换网页;
- 2.history 模式:利用 history API 实现 url 地址改变,网页内容改变;
它们的区别最明显的就是 hash 会在浏览器地址后面增加 #号,而 history 可以自定义地址。
# hash 模式
使用 window.location.hash 属性及窗口的 onhashchange 事件,可以实现监听浏览器地址 hash 值变化,执行相应的 js 切换网页。下面具体介绍几个使用过程中必须理解的要点:
- 1.hash 指的是地址中 #号以及后面的字符,也称为散列值。hash 也称作锚点,本身是用来做页面跳转定位的。如 http://localhost/index.html#abc,这里的 #abc 就是 hash;
- 2. 散列值是不会随请求发送到服务器端的,所以改变 hash,不会重新加载页面;
- 3. 监听 window 的 hashchange 事件,当散列值改变时,可以通过 location.hash 来获取和设置 hash 值;
- 4.location.hash 值的变化会直接反应到浏览器地址栏;
# 触发 hashchange 事件的几种情况:
-
浏览器地址栏散列值的变化(包括浏览器的前进、后退)会触发 window.location.hash 值的变化,从而触发 onhashchange 事件;
-
当浏览器地址栏中 URL 包含哈希如 (http://www.baidu.com/#home),这时按下输入,浏览器发送 (http://www.baidu.com/) 请求至服务器,请求完毕之后设置散列值为 #home,进而触发 onhashchange 事件;
-
当只改变浏览器地址栏 URL 的哈希部分,这时按下回车,浏览器不会发送任何请求至服务器,这时发生的只是设置散列值新修改的哈希值,并触发 onhashchange 事件;
-
html 中 a 标签的属性 href 可以设置为页面的元素 ID 如 #top,当点击该链接时页面跳转至该 id 元素所在区域,同时浏览器自动设置 window.location.hash 属性,地址栏中的哈希值也会发生改变,并触发 onhashchange 事件;
//设置 url 的 hash,会在当前url后加上'#abc' window.location.hash = "abc"; let hash = window.location.hash; //'#abc' window.addEventListener("hashchange", function () { //监听hash变化,点击浏览器的前进后退会触发 });
# history 模式
# 概述
window.history属性指向History对象,它表示当前窗口的浏览历史。当发生改变时,只会改变页面的路径,不会刷新页面。- History 对象保存了当前窗口访问过的所有页面网址。通过 history.length 可以得出当前窗口一共访问过几个网址。
- 由于安全原因,
浏览器不允许脚本读取这些地址,但是允许在地址之间导航。 - 浏览器工具栏的 “前进” 和 “后退” 按钮,其实就是对 History 对象进行操作。
# 属性
History 对象主要有两个属性。
-
History.length:当前窗口访问过的网址数量(包括当前网页)
-
History.state:History 堆栈最上层的状态值
// 当前窗口访问过多少个网页 history.length; // 1 // History 对象的当前状态 // 通常是 undefined,即未设置 history.state; // undefined
# 方法
History.back()、History.forward()、History.go()
这三个方法用于在历史之中移动。
-
History.back ():移动到上一个网址,等同于点击浏览器的后退键。对于第一个访问的网址,该方法无效果。
-
History.forward ():移动到下一个网址,等同于点击浏览器的前进键。对于最后一个访问的网址,该方法无效果。
-
History.go ():接受一个整数作为参数,以当前网址为基准,移动到参数指定的网址。
如果参数超过实际存在的网址范围,该方法无效果;如果不指定参数,默认参数为 0,相当于刷新当前页面。history.back(); history.forward(); history.go(1); //相当于history.forward() history.go(-1); //相当于history.back() history.go(0); // 刷新当前页面 0 刷新当前页面 正整数 前进n条记录 负整数 后退n条记录注意:移动到以前访问过的页面时,页面通常是从浏览器缓存之中加载,而不是重新要求服务器发送新的网页。
原文链接:https://blog.csdn.net/Charissa2017/article/details/104779412 CSDN 博主「蒲公英芽」
# DOM 树
网络进程接收到响应头之后,会根据响应头中的 content-type 字段来判断文件的类型,比如 content-type 的值是 “text/html” ,那么浏览器就会判断这是一个 HTML 类型的文件,然后为该请求选择或者创建一个渲染进程。渲染进程准备好之后,网络进程和渲染进程之间会建立一个共享数据的管道,网络进程接收到数据后就往这个管道里面放,而渲染进程则从管道的另外一端不断地读取数据,并同时将读取的数据 “喂” 给 HTML 解析器。你可以把这个管道想象成一个 “水管”,网络进程接收到的字节流像水一样倒进这个 “水管”,而 “水管” 的另外一端是渲染进程的 HTML 解析器,它会动态接收字节流,并将其解析为 DOM。 字节流->分词器(Tokens)->生成节点(Node)->DOM 。
HTML 解析器开始工作时,会默认创建了一个根为 document 的空 DOM 结构,同时会将一个 StartTag document 的 Token 压入栈底。然后经过分词器解析出来的第一个 StartTag html Token 会被压入到栈中,并创建一个 html 的 DOM 节点,添加到 document 上
但是解析到 script 标签时,渲染引擎判断这是一段脚本,此时 HTML 解析器就会 暂停 DOM 的解析, 因为接下来的 JavaScript 可能要修改当前已经生成的 DOM 结构 。
把内嵌 JavaScript 脚本修改成了通过 JavaScript 文件加载。其整个执行流程还是一样的,执行到 JavaScript 标签 时, 暂停整个 DOM 的解析 , 执行 JavaScript 代码 ,不过这里执行 JavaScript 时, 需要先下载这段 JavaScript 代码 。这里需要重点关注下载环境,因为 JavaScript 文件的下载过程会阻塞 DOM 解析 ,而通常下载又是非常耗时的,会受到网络环境、JavaScript 文件大小等因素的影响。
不过 Chrome 浏览器做了很多优化,其中一个主要的优化是 预解析操作 。当渲染引擎收到字节流之后,会开启一个 预解析线程 ,用来分析 HTML 文件中包含的 JavaScript、CSS 等相关文件 ,解析到相关文件之后, 预解析线程会提前下载这些文件 。
知道 引入 JavaScript 线程会阻塞 DOM ,不过也有一些相关的策略来规避,比如使用 CDN 来加速 JavaScript 文件的加载,压缩 JavaScript 文件的体积。另外,如果 JavaScript 文件中没有操作 DOM 相关代码,就可以将该 JavaScript 脚本设置为异步加载,通过 async 或 defer 来标记代码,async 和 defer 虽然都是异步的,不过还有一些差异,使用 async 标志的脚本文件 一旦加载完成,会立即执行 ;而使用了 defer 标记的脚本文件,需要在 DOMContentLoaded 事件之前执行 。
在执行 JavaScript 之前,需要先解析 JavaScript 语句之上所有的 CSS 样式。所以如果代码里引用了外部的 CSS 文件,那么 在执行 JavaScript 之前,还需要等待外部的 CSS 文件下载完成,并解析生成 CSSOM 对象之后,才能执行 JavaScript 脚本 。而 JavaScript 引擎在解析 JavaScript 之前,是不知道 JavaScript 是否操纵了 CSSOM 的,所以渲染引擎在遇到 JavaScript 脚本时, 不管该脚本是否操纵了 CSSOM,都会执行 CSS 文件下载,解析操作 ,再执行 JavaScript 脚本。所以说 JavaScript 脚本是依赖样式表的 ,这又多了一个阻塞过程。如何优化:
- 将样式表放在文档的头部:将样式表放在
<head>标签中,这样可以确保在加载 JavaScript 之前就开始加载样式表,减少阻塞时间。 - 使用异步加载:将 JavaScript 脚本标记为 async 或使用动态创建
<script>标签的方式进行异步加载,这样脚本的加载和执行过程将与样式表的加载过程并行进行,不会相互阻塞。 - 延迟加载:使用 defer 属性标记 JavaScript 脚本,使其在
文档解析完毕后再执行,这样可以确保样式表的加载不会被脚本阻塞。 - 内联样式:将一些关键的样式直接内联到 HTML 标签中,避免样式表的加载和解析过程。
- 使用样式表预加载:使用 link 标签的
rel="preload"属性,将样式表进行预加载,这样可以在 JavaScript 执行之前提前加载样式表。