前端博主,热衷各种前端向的骚操作,经常想到哪就写到哪,如果有感兴趣的技术和前端效果可以留言~博主看到后会去代替大家踩坑的~
主页: oliver尹的主页
格言: 跌倒了爬起来就好~
来个关注吧,点个赞吧,谢谢
论多窗口相互关联下window.open打开已在的窗口时只激活不刷新的实现方案
- 一、前言
- 二、本文内容概述
- 三、待解决问题
- 四、问题解决说明
- 4.1 刷新问题解决
- 4.2 多窗口的相互关联激活
- 4.2.1 确认窗口name值
- 4.2.2 确认窗口是否已经打开过
- 4.2.3 给窗口命名
- 4.3 缓存的问题
- 五、代码下载
- 六、小结
一、前言
近期,我司有个小伙伴遇到这么个场景实现起来感觉有点困难与我讨论,大概是这样的:
项目框架使用的是Vue开发的单页应用,这个单页应用在打开某个大模块路由时 需要使用window.open()
去打开新的模块,也就是说如果打开的是大模块,那么会新建一个窗口,在这个窗口中打开页面,但是如果打开的换菜单又是小路由,那么使用的VueRouter切换路由,这就导致了一个问题,在重复点击大模块时出现了重复打开新窗口,并且后续又陆陆续续出现了许许多多的问题…因此有了本文作记录,以备后续不时之需~
耐心看完,也许你会有所收获~
二、本文内容概述
本文主要解决的场景是:
在类似Vue的单页应用项目中,在任意一个窗口内首次打开指定页面时通过新建窗口的方式打开,在任意一个窗口内打开已打开的界面时,仅激活对应窗口,比如:在A页面使用类似 window.open()
的效果打开了同域下的B页面和C页面(A,B,C页面具有相同的路由系统),切换窗口至B页面,点击B页面下的C页面地址,此时不会再次新建C页面窗口,而是将已打开的C页面窗口激活至可视状态,且C页面窗口不触发刷新保留原来的内容与操作状态;
同时,在任意界面中执行浏览器的f5刷新,不影响使用效果;
先简单说一下实现思路吧,在openUrl的内部判断当前名为title的窗口是否打开过,如果没有打开过,执行 window.open()
方法新建窗口,如果打开过,执行 window.open('javascript:;', title)
去激活名为title的窗口,让它显示出来;
如果有小伙伴想要文件,请直接跳转至 第五部分下载文件 即可~
三、待解决问题
为了完成上面的场景需求,实际开发中遇到的主要问题一共存在三个:
- 刷新的问题,当使用
window.open(url,name)
打开对应name的窗口时,如果该name的窗口已存在确实会仅仅是激活对应name的窗口,但是这个激活会执行刷新,它刷新这个网页,假如这个网页存在复杂表单,用户输入的一半的内容或者页面操作的状态都将会被刷新掉,据产品经理讲体验甚是不好… - 多页面相互激活的问题,这个问题就比较有意思了,由于是Vue开发的单页应用,整个路由系统其实是同一套,这就会出现一个问题,在A页面通过
window.open()
打开了B页面和C页面,而B页面和C页面自身其实也包含完整的路由系统的,这个路由系统中自然也有A,B,C的路由地址,那如果在B页面通过window.open()
打开C页面,此时正确的逻辑应该是仅仅去激活C页面的窗口,而不是重新再打开一个,示例图如下:
是不是挺有意思,这里面大概率存在通信问题,比如在C页面怎么知道在A页面中打开过哪些窗口并且获得窗口name等等~
- 缓存的问题,即浏览器刷新,在A页面执行浏览器的的f5刷新后,可以在点击对应模块时依然仅仅是去激活对应的模块页面,
四、问题解决说明
关于 window.open()
具体参数以及各个参数的说明可以看MDN的官方解释,具体链接如下:window.open
4.1 刷新问题解决
先说核心实现吧,刷新的问题核心解决应该是在激活的实现,而不是重新通过 window.open()
去打开url,经过实验,确认通过以下这个实现
window.open('javascript:;', name)
可能会有小伙伴奇怪,论激活窗口的功能不应该是通过 window.focus()
去实现么,使用 window.focus()
确实可以将指定name值的窗口激活,实现如下:
let win = null;
if(win){
win.focus()
}
else{
win = window.open(url, name);
}
但是由于我们的场景比较奇葩,需要实现跨窗口激活,也就是说在C页面打开B页面时需要在C页面执行B页面这个 win.focus()
,但由于win这个对象它其实是一个最顶层的 window对象,它无法被传递,被转化,即想使用postMessage发送,会提示发送失败,根本无法跨窗口,同理,win也无法被缓存,一旦刷新界面,这个win对象就会丢失无法留存,自然待解决问题中第三个问题也同样无法实现;
在尝试 window.focus()
这条路走不通后,发现了另外一种方法,即上面说到的 window.open('javascript:;', name)
这个方法,这个方法同样可以解决激活窗口的问题,并且这个name值的类型是一个字符串,是窗口的名字也是 winodw.open()
中的name,代码如下
window.open(url, name);
这种方法的实现和focus()的实现非常接近,无非就是将 win.focus()
改成了 window.open('javascript:;', name)
if(判断是否打开过){
window.open('javascript:;', name)
}
else{
window.open(url, name);
}
4.2 多窗口的相互关联激活
多窗口的相互关联激活细想一下,上面采用的是 window.open('javascript:;', name)
实现的激活,在这个代码中唯一的变量就是一个字符串格式的name,扩展一下思维,在任意一个窗口里知道了其它窗口的name,是不是都可以通过 window.open('javascript:;', name)
实现激活,想了想从理论上来说这个方法应该是可行的(当然事实证明确实可以),剩下的就是要解决以下这三个小问题:
- 知道已打开窗口的name值,目的是为了方便使用
window.open('javascript:;', name)
去激活; - 判断窗口是否已经打开过,目的是为了方便知道使用
window.open('javascript:;', name)
去激活窗口还是使用window.open(url, name)
去打开窗口; - 给每一个打开的窗口命名,这个name值是必须唯一的,并且需要和执行
window.open('javascript:;', name)
中的name对应起来,不对应起来的话执行open的时候会找不到name值的窗口,自然也就无法实现激活;
4.2.1 确认窗口name值
name值的小问题其实非常好解决,这是一个Vue单页应用,路由名称和路由地址是固定的,也就是说,所有个这些个新窗口的路由也完全相同,而我们的路由往往是由菜单名字和路由地址组成的,具有唯一性(你总不能两个菜单的名字完全一模一样吧),因此往往是如下这种结构
// template
<div v-for="item in menuList" :key="item.value" @click="changeRouter(item)">
<div>{{item.title}}</div>
</div>
// js
changeRouter(item){
window.open('javascript:;', item.title)
}
因此,到这里基本解决了一个小问题,多窗口由于共享的是一个路由系统,name的名字其实在各个页面是相互知道的,我们只需要知道这个name的窗口是否处于打开状态即可;
4.2.2 确认窗口是否已经打开过
如何确认窗口是否已经打开过?想了下,由于多个窗口实际上处于同域下,localStorage自然成了首选,当打开一个新页面时,往localStorage里插入一条消息,关闭页面的时候再到localStorage中将对应的窗口信息删掉;
大致的代码如下:
首先是存储信息
// 执行首次跳转
window.open(url, name)
// 将跳转的名字和地址保存下来
setTsLocalData(name, url)
function setLocalData(key, value) {
let local = localStorage.getItem("demo")
if (local) {
local = JSON.parse(local)
}
console.log(local)
const item = { ...local }
item[key] = value
localStorage.setItem("demo", JSON.stringify(item))
}
接着在执行跳转前加一个判断,判断打开过的页面不再执行 window.open(url, name)
,而是转而去执行window.open(‘javascript:;’, name)激活窗口
const urlname = getLocalName(url)
// 存在name的话执行激活,不执行跳转
if (urlname) {
window.open('javascript:;', urlname)
return
}
// 执行首次跳转
window.open(url, name)
// ...上方代码
4.2.3 给窗口命名
当通过window.open打开某个页面后,自动给窗口命名,名字的来源来自于localStorage,还是因为同域的关系,共享了localStorage,我们可以通过当前的网址去localStorage做匹配,匹配到网址后取到对应的key,这个key就是窗口的名字,将其赋值给窗口
function loadWidName(url) {
const newUrl = getLocalName(url)
window.name = newUrl
}
function getLocalName(url) {
let local = localStorage.getItem(localStorageKey)
if (!local) return false
local = JSON.parse(local)
// return Object.prototype.hasOwnProperty.call(local, key);
let name = ''
for (const key in local) {
if (!Object.prototype.hasOwnProperty.call(local, key)) continue
if (local[key] === url) {
name = key
break
}
}
return name
}
注意的是,loadWidName这个函数得在页面一加载就执行,因为必须得在执行open之前就给窗口命名成功,否则执行open的时候会认为当前页面没有被打开过,直接进行跳转了;
4.3 缓存的问题
在想想,仔细想想,缓存的问题是不是已经被解决掉了,缓存最大的困难是什么,是窗口的命名,一旦进行刷新窗口的名字就丢失了,因此在别的窗口执行 window.open('javascript:;', item.title)
的时候会找不到带有这个名字的窗口;
而如果在一加载页面的时候就执行了上面4.2.3这个方法,去主动给窗口命名,那么刷新的时候自然也会去执行这个命名的过程,自然再怎么刷新窗口的name值一直是保持有的;
缓存的问题除了主动给窗口命名还剩下的就是关闭窗口是去 移除对应缓存 了,如果不移除,那么把窗口关闭后在其他窗口中点击关闭窗口时,会认为该窗口依然处于打开状态,那就会去执行window.open('javascript:;', urlname)
,打开一个空白页面~
移除代码很简单,给window添加一个beforeunload事件
window.addEventListener('beforeunload', function () {
let local = localStorage.getItem("demo")
if (!local) return false
local = JSON.parse(local)
const newItem = {}
for (let item in local) {
if (item !== key) {
newItem[item] = local[item]
}
}
localStorage.setItem("demo", JSON.stringify(newItem))
return
})
到这里基本就实现了这个功能了
五、代码下载
代码已经上传到CSDN上了,由于是正式使用的我简单压缩了一下,下载地址如下:前端window.open实现激活而非打开的功能;
如果有需要源码的小伙伴留言或者私信留下邮箱,博主看到后会及时发送的~问题不大
至于使用也非常简单,在main.js中引入文件,这个文件扩展了一个类似于 window.open()
的方法,所有执行 window.open()
的地方改用我们的方法代替,在方法内部去判断是新建窗口还是激活窗口,比如
// main.js
import openUrl from "xxx"
// 使用时在内部实现逻辑判断是新建还是激活
window.openUrl(url,title) 代替 winow.open(url,title)
六、小结
本文主要解决了在类似Vue的单页应用项目中,在任意一个窗口内首次打开指定页面时通过新建窗口的方式打开,在任意一个窗口内打开已打开的界面时,仅激活对应窗口的场景功能;
这个场景可能比较冷门,但确实真实存在并被我司的小伙伴遇到了,当然博主这种方法是不是最优解不太清楚,如果有小伙伴知道更优的方式,一定,一定记得留言告诉博主,谢谢~
如果有帮助,点个赞,点个关注吧~谢谢