共计 8513 个字符,预计需要花费 22 分钟才能阅读完成。
浏览器多标签页通信有助于降低服务器负载,提高运营人员的工作效率,提高用户体验。是前端开发优化的一个重要环节。
需求来源
在多数 CMS(内容管理系统)后台上,常见的是一个文章列表页面,点击列表项会打开一个新的文章详情页面。编辑人员经常在这个详情页面上对文章操作,比如修改标题、配图、摘要等内容。操作完毕之后,由于文章页和列表页是两个页面,文章内容数据不能及时同步到列表,这样就照成运营人员多次误操作,这大大降低了运营人员的工作效率。
对于前端工程师来讲,实现浏览器多个页卡之间的通信,及时更新相关数据更改,是一件重要的事情。
例如有一个需求:当文章详情页面更新的时候,会同步到文章列表页。
实现方式
方式一:cookie+setInterval
cookie 最初是在客户端用于存储用户的会话信息的。由于 HTTP 是一种无状态的协议,服务器单从网络连接上无法知道客户身份。通过 cookie 就给客户端们颁发一个通行证,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。cookie 实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用 response 向客户端浏览器颁发一个 cookie。客户端浏览器会把 cookie 保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该 cookie 一同提交给服务器。服务器检查该 cookie,以此来辨认用户状态。服务器还可以根据需要修改 cookie 的内容。
在 JavaScript 中,cookie 的操作接口即 document.cookie,通过这个接口可以读取、写入、删除 cookie。这个操作其实不太友好,所以很多工具库提供了 cookie 的操作方法。我这里提供一个简单的封装方法。
var QQ = {};
QQ.Cookie={
set:function(name,value,expires,path,domain){
if(typeof expires==”undefined”){
expires=new Date(new Date().getTime()+3600*1000);
}
document.cookie=name+”=”+escape(value)+((expires)?”; expires=”+expires.toGMTString():””)+((path)?”; path=”+path:”; path=/”)+((domain)?”;domain=”+domain:””);
},
get:function(name){
var arr=document.cookie.match(new RegExp(“(^|)”+name+”=([^;]*)(;|$)”));
if(arr!=null){
return unescape(arr[2]);
}
return null;
},
clear:function(name,path,domain){
if(this.get(name)){
document.cookie=name+”=”+((path)?”; path=”+path:”; path=/”)+((domain)?”; domain=”+domain:””)+”;expires=Fri, 02-Jan-1970 00:00:00 GMT”;
}
}
};
cookie 有个特性,一个页面产生的 cookie 能被与这个页面的同一目录或者其他子目录下的页面访问,这样页面之间就产生了一个共享的存储空间。通常把 cookie 的 path 设置为一个更高级别的目录,比如默认“/”,从而使更多的页面共享 cookie,实现多页面之间相互通信。cookie 所在的域,默认为请求的地址,也可以通过设置 document.domain 为父域等方式扩大 cookie 可被访问的域。
实现原理:
列表页通过 setInterval 定时器循环监听 cookie 的数据变动列表页代码:
window.onload=function(){
var tid = ”;
setInterval(function(){
if(tid != QQ.Cookie.get(“tid”)){
alert(‘ 数据更新!’);
tid = QQ.Cookie.get(“tid”)
}
}, 1000);
}
当详情页有数据修改时后,写入 cookie 详情页代码:
<input id=”content” type=”text”>
<button id=”btn”>Click</button>
<script>
window.onload=function(){
var oBtn=document.getElementById(“btn”);
var oInput=document.getElementById(“content”);
oBtn.onclick=function(){
var val=oInput.value;
QQ.Cookie.set(“tid”,val);
}
}
</script>
cookie+setInterval 的不足:
1、cookie 空间有限,浏览器在每一个域名下最多能设置 30-50 个 cookie,容量最多 4K 左右。2、每次 HTTP 请求会把当前域的 cookie 发送到服务器上,而有些 cookie 只是浏览器才用的到,浪费网络带宽。3、setInterval 的频率设置,过大会影响浏览器性能,过小会影响时效性。
cookie+setInterval 的优点:
兼容性好,几乎所有的浏览器都支持。
方式二:localStorage
在 HTML5 中,新加入了一个 localStorage 特性,这个特性主要是用来作为本地存储来使用的,解决了 cookie 存储空间不足的问题,localStorage 中一般浏览器支持的是 5M 大小,这个在不同的浏览器中 localStorage 会有所不同。localStorage 的 API 也很简单,提供了 JS 的读写操作。
if(!window.localStorage){
alert(“ 浏览器不支持 localstorage”);
return false;
}else{
var storage = window.localStorage;
// 通过属性写入 a 字段
storage.a = 1;
// 通过方法写入 b 字段
storage.setItem(“b”,2);
storage.getItem(“a”);
storage.b;
storage.clear();
}
它还比 cookie 多了一个优点,提供了 onstorage 以及 storage 事件,可以绑定一个回调函数,使用如下:
window.onstorage = function(e){console.log(e)}
// 或者
window.addEventListener(‘storage’, function(){console.log(e)})
localStorage 是 Storage 对象的实例。对 Storage 对象进行任何修改,都会在触发 storage 事件。当通过属性或者 setItem() 方法保存数据,或者使用 delete 操作符或 removeItem() 删除数据,或者调用 clear() 方法时,都会触发该事件。通过这个事件,我们可以实现页卡之间的变动监听。
实现原理:
列表页通过 storage 监听 localStorage 的数据变动列表页代码:
<script>
window.addEventListener(“storage”,function(event){
console.log(“newValue is”+localStorage.getItem(“tid”));
console.log(“oldValue is”+event.oldValue);
window.alert(‘ 数据更新!’);
},false);
</script>
当详情页有数据修改时后,写入 localStorage 详情页代码:
<input id=”content” type=”text”/>
<button id=”btn”>Click</button>
<script>
window.onload=function(){
var oBtn=document.getElementById(“btn”);
var oInput=document.getElementById(“content”);
oBtn.onclick=function(){
var val=oInput.value;
localStorage.setItem(“tid”,val);
}
}
</script>
不过,onstorage 以及 storage 事件,针对都是非当前页面对 localStorage 进行修改时才会触发,当前页面修改 localStorage 不会触发监听函数。还有就是在对原有的数据的值进行修改时才会触发,比如原本已经有一个 key 为 a,值为 1 的 localStorage,再执行:localStorage.setItem(‘a’, 1) 代码,同样是不会触发监听函数的。
localStorage 的不足:
1、浏览器的容量大小不统一(比 cookie 大很多了),并且在高版本的浏览器才支持 localStorage 这个属性 2、目前所有的浏览器中都会把 localStorage 的值类型限定为 string 类型,需要 JSON 转换。3、localStorage 本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡。4、localStorage 只能监听非己页面的数据变化,这一点严重影响使用。
localStorage 的优点:
1、解决了 cookie 容量小和时效性不足的问题。
方式三:WebSocket
WebSocket API 是下一代客户端 – 服务器的异步通信方法,已被 W3C 进行了标准化。WebSocket API 最伟大之处在于服务器和客户端可以双向实时通信。WebSocket 并不限于以 Ajax(或 XHR) 方式通信,因为 Ajax 技术需要客户端发起请求,而 WebSocket 服务器和客户端可以彼此相互推送信息;XHR 受到域的限制,而 WebSocket 允许跨域通信。
它的使用很简单,如下:
// 创建一个 Socket 实例
var socket = new WebSocket(‘ws://localhost:8080’);
// 打开 Socket
socket.onopen = function(event) {
// 发送一个初始化消息
socket.send(‘I am the client and I\’m listening!’);
// 监听消息
socket.onmessage = function(event) {
console.log(‘Client received a message’,event);
};
// 监听 Socket 的关闭
socket.onclose = function(event) {
console.log(‘Client notified socket has closed’,event);
};
// 关闭 Socket….
socket.close()
};
WebSocket 提供了 send 方法和 onmessage 事件,用来发送和接收数据。onmessage 事件提供了一个 data 属性,它可以包含消息的 Body 部分。消息的 Body 部分必须是一个字符串,可以进行序列化 / 反序列化操作,以便传递更多的数据。
实现原理:
列表页通过 onmessage 监听 socket 服务器发送过来的消息列表页代码:
<script>
var socket = new WebSocket(‘ws://localhost:8080’);
socket.onopen = function(event) {
socket.onmessage = function(event) {
console.log(‘Client received a message’, event);
};
};
</script>
当详情页有数据修改时后,通过 socket 连接,通知列表页更新数据。详情页代码:
<input id=”content” type=”text”/>
<button id=”btn”>Click</button>
<script>
var socket = new WebSocket(‘ws://localhost:8080’);
window.onload=function(){
var oBtn=document.getElementById(“btn”);
var oInput=document.getElementById(“content”);
oBtn.onclick=function(){
var val=oInput.value;
socket.onopen = function(event) {
// 发送数据类型必须是 string、ArrayBuffer、Blob 之一
socket.send(‘ 数据更新!’);
}
}
</script>
WebSocket 的语法非常简单,不过需要 IE10+ 浏览器才支持 WebSocket 通信。如果你的业务需要兼容 IE8,9。业界通常使用第三方库来解决这个问题,比如 Socket.IO,它使用检测功能来判断是否建立 WebSocket 连接,或者是 AJAX long-polling 连接,或 Flash 等,可快速创建实时的应用程序。Socket.IO 还提供了一个 NodeJS API,它看起来非常像浏览器的 API。
WebSocket 的不足:
1、它需要服务端的支持才能完成任务。如果 socket 数据量比较大的话,会严重消耗服务器的资源。
WebSocket 的优点:
1、使用简单,功能灵活、强大,如果部署了 WebSocket 服务器,可以实现很多实时的功能。
方式四:BroadcastChannel
BroadcastChannel 即广播频道,是 window 下面的一个 API,该 API 是用于同源不同页面之间完成通信的功能。我们可以理解它是一个广播台,所有的广播实例,都会接入这个广播台(中介者模式中的控制中心),所以,只要在初始化实例时,传入相同的频道值,就会被接入到一个相同的广播频道中。它的实现最简单,很多第三方 JS 库都实现了一套自己的 BroadcastChannel。
实现原理:
列表页通过 onmessage 监听其他页面发送过来的消息列表页代码:
// 接收广播
let articleCast = new BroadcastChannel(‘mychannel’);
articleCast.onmessage = function (e) {
console.log(e.data);
}
当详情页有数据修改时后,通过 postMessage,传递数据。详情页代码:
// 创建广播并发送
let listCast = new BroadcastChannel(‘mychannel’);
myObj = {tid: “123”, title: “ 更改后的标题 ”};
listCast.postMessage(myObj);
BroadcastChannel 的不足:
1、兼容性极差,只支持最新版的 Chrome 和 Firefox,完全不支持 IE 和 Safari。
BroadcastChannel 的优点:
1、使用简单,功能单一,跨页面通信的理想选择。
方式五:SharedWorker
SharedWorker 也是 HTML5 提供的新的浏览器 API,叫共享工作线程。它允许多个页面共享使用线程,每个页面都链接到该共享工作线程的某个端口号上。页面通过该端口与共享工作线程进行通信。目前的 Web 所有程序的操作都基于页面的,而 SharedWorker 的引入开辟了一个“Web 程序”在后台线程的概念。而且它还可以和页面交互,相当于把所有页面都聚拢起来了。上例讲为每个页面都维护一份 WebSocket 代码不仅耗费大量的连接数,而且还拖慢性能。这些通用的连接最好当然做成可跨域页面共用的,在 SharedWorker 引入之前并没有一个完美的跨页面通信解决方案。
实现原理
列表页通过 onmessage 监听 SharedWoker 发送过来的消息列表页代码:
<script>
var s = new SharedWorker(‘x.js’);
s.port.onmessage = function(e){
console.log(e.data);
window.alert(“ 数据变化!”)
};
s.port.start();
</script>
当详情页有数据修改时后,通过 SharedWorker,通知列表页更新数据。
<input id=”content” /><input type=”button” id=”btn” value=” 发送 ” />
<script>
var s = new SharedWorker(‘x.js’);
btn.onclick=function(){
s.port.postMessage(document.getElementById(‘content’).value);
};
s.port.start();
</script>
其中共享线程 x.js 的代码也很简单,它的工作是双向的,每一个页面都可以用来接收和发送数据。
//x.js
var pool = [];
onconnect = function(e) {
pool.push(e.ports[0]);
e.ports[0].onmessage = function(e){
for(var i=0; i<pool.length; i++)
pool[i].postMessage(e.data);
};
};
SharedWorker 就像运行在浏览器后端的守卫者,可以被多个 window 同时使用,但必须保证这些标签页都是同源的 (相同的协议,主机和端口号)。
SharedWorker 的不足:
1、兼容性较差,IE 完全不支持,chrome 和 Firefox 支持很完善,Safari 部分支持,如果你的业务是内部系统,不考虑 IE,可以使用。2、API 比较简单,配置繁琐,使用起来还是比较麻烦。
SharedWorker 的优点:
1、功能强大,不限于浏览器通信,还有共享数据,方法等功能。由于是另启的一个新线程,不影响主线程代码业务,性能优秀,无需借助服务器,是一个完美的跨页面通信解决方案。
我们做了什么?(划重点)
SharedWorker 提供的 API 很少,使用比较简单,如果需要完成复杂的页面通信,还是有一定难度。基于此,我实现了一款基于 SharedWorker 的封装库,叫作 superSharedWorker
它是一款页面之间通信的 JavaScript 框架,它通过 shared worker 实现纯浏览器页卡之间的通信。你无需了解 shared worker,可以快速使用页面之间的数据传递,快捷,强大。它的优点就是通过原生 JS 实现,无需依赖任何 JS 库实现了对 sharedWorker 的封装。开箱即用,配置简单。
两种使用方式:1、ES6 import 的方式
import superSharedWorker from ‘./src/index.js’;
let superSharedWorker = new superSharedWorker(‘page1’, callback); // 注册
superSharedWorker.send(‘hello world!’); // 发送消息
2、script 标签外链的形式
<script src=”./build/super-sharedworker.js”></script>
<script type=”text/javascript”>
//<!–
var superSharedWorker = new SuperShared(‘page1’, onRecvMsg);
function onRecvMsg(message) {
console.log(message)
}
superSharedWorker.send(‘hello, world’);
//–>
</script>
更多用法举例:
let superSharedWorker = new superSharedWorker(‘page1′, callback); // 注册
superSharedWorker.add({name:’chunpengliu’, sex:1});
superSharedWorker.del(‘sex’);// 删除缓冲区数据
superSharedWorker.send({‘time’:2019}, ‘page2’); // 一次性发送缓冲区数据,只发送给 name=”page2″ 的页面
superSharedWorker.close(); // 关闭线程,节省资源
它提供了很多强大的功能,可一对一,一对多发送消息。像使用 git 一样传递数据。
小结
通过讨论,实现了四种实现浏览器标签页之间的通信,分别是使用 cookie、使用 websocket 协议、通过 localstorage、以及使用 html5 浏览器的新特性 SharedWorker,每种方法各有利弊。如果不考虑兼容旧的浏览器,superSharedWorker 或许是最好的解决方案,优化使用效率,提升用户体验,赶快使用浏览器多标签页通信功能吧!