如何监听浏览器中同一域名的 tab 全部关闭? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a Javascript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
Javascript 权威指南第 5 版
Closure: The Definitive Guide
Jexxie
V2EX    Javascript

如何监听浏览器中同一域名的 tab 全部关闭?

  •  1
     
  •   Jexxie 2022-08-18 10:26:22 +08:00 6180 次点击
    这是一个创建于 1150 天前的主题,其中的信息可能已经有所发展或是发生改变。

    奇葩需求:所有这个网站的标签页 tab 全部关闭则退出登陆(调用 logout 接口)。

    (以下代码是目前的实现,完全关闭浏览器后退出登陆)

     // 刷新和关闭网页都会触发 onunload, 因此用 sessionStorage 区分,浏览器完全关闭后会清空 sessionStorage sessionStorage.setItem('reloaded', 'yes'); window.Onunload= function(e) { if (sessionStorage.getItem('reloaded') == null) { fetch("/logout", { method: "POST", // keepalive 属性用于页面卸载时,告诉浏览器在后台保持连接,继续发送数据。开启了 keepalive 属性后,网页就算被关闭了,请求被会继续执行而不会中断。 keepalive: true }); } }; 

    请问,有没有方法实现检测到所有同域标签页关闭的方法?

    41 条回复    2022-08-24 17:30:01 +08:00
    encro
        1
    encro  
       2022-08-18 10:27:48 +08:00   1
    session 不设置过期时间,默认行为就是这样。
    encro
        2
    encro  
       2022-08-18 10:29:23 +08:00   3
    这是后端的坑需要前端填吗?

    打脸:

    如果客户端异常了,停电了,服务端就不会自动退出吗?
    penzi
        3
    penzi  
       2022-08-18 10:39:18 +08:00
    localStorage 周期性写时间。下次打开的时候判断时间是不是过期了,过期就 logout
    zjsxwc
        4
    zjsxwc  
       2022-08-18 10:47:20 +08:00
    “完全关闭浏览器后退出”
    不就是没设置到期时间的 session 默认行为吗
    runze
        5
    runze  
       2022-08-18 11:15:39 +08:00   5
    典型的 XY 问题,楼主还是先说一下原本的目的是什么比较好。

    https://noob.tw/xy-problem/
    Belmode
        6
    Belmode  
       2022-08-18 11:15:48 +08:00 via Android
    思路错了。浏览器做不到你这需求中的效果,即使有也必定很绕。让服务端来做。
    vanton
        7
    vanton  
       2022-08-18 11:37:30 +08:00
    让后端去做,前端没这个义务做。
    shakaraka
        8
    shakaraka  
    PRO
       2022-08-18 11:41:04 +08:00
    起 ws 啊,ws 断了服务器就把 session 删了不就好了
    lisongeee
        9
    lisongeee  
       2022-08-18 11:47:02 +08:00   2
    你这个代码根本不管用啊,sessionStorage 各个标签页是完全独立的
    测试如下代码
    ```js
    sessionStorage.setItem('reloaded', 'yes');
    window.Onunload= function(e) {
    if (sessionStorage.getItem('reloaded') == null) {
    fetch("http://127.0.0.1:8080/on", {
    method: "GET",
    keepalive: true
    });
    } else {
    fetch("http://127.0.0.1:8080/off", {
    method: "GET",
    keepalive: true
    });
    }
    };
    ```
    只打开一个标签页然后关闭,结果如下
    ![img]( https://github.com/lisonge/src/raw/main/img/Snipaste_2022-08-18_11-43-07.png)
    lisongeee
        10
    lisongeee  
       2022-08-18 11:53:19 +08:00   4
    一个可能可行的解法是 Service Worker ,它独立于 标签页 存在,你可以在每个标签页 onunload 的时候给 Service Worker postMessage ,然后 Service Worker 去调用 await clients.matchAll() ,如果得到的列表长度是 0 ,就 doYourWork
    有一个问题,文档里没有说明 Service Worker 何时被停止,而且我懒得测试,你可以自己试试
    yolee599
        11
    yolee599  
       2022-08-18 13:04:19 +08:00 via Android   1
    如果用户端直接杀浏览器进程或者浏览器崩了,就永远不退出吗?
    viakiba
        12
    viakiba  
       2022-08-18 14:21:48 +08:00 via Android
    量不大心跳就完事了
    nothingistrue
        13
    nothingistrue  
       2022-08-18 15:02:54 +08:00
    那些让后端处理的,你们是哪里学的开发,浏览器的事件有没有触发,服务器端要拿头去判断吗。现行 HTTP 规范下,只有标签页关闭和窗口关闭两个事件,没有特定标签页全部关闭事件,这事拿常规手段做不了。

    我想到一个思路,需要前后端配合。前端,每个页面定时上报自己还活着,可以用 websocket 加心跳,也可以就是单纯的 Javascript 定时器(如果标签页是频繁打开关闭的,那就千万别用 websocket ,DDOS 了)。后端需要监控当前 Session (如果能定位到客户端也可以把维度换成客户端)的“活着的页面数”,变成零的时候就触发登出操作。

    但是,跟产品沟通一下,把需求变成“5 分钟或者半小时内没操作就自动登出”,回是更好的选择。
    lambdaq
        14
    lambdaq  
       2022-08-18 15:04:29 +08:00
    @lisongeee 然后用户来了个 chrome 和 firefox 双开
    nothingistrue
        15
    nothingistrue  
       2022-08-18 15:06:06 +08:00
    不嫌 LOW ,不怕性能爆炸,并且还不关心是否长时间没操作的话,把会话超时时间定为 1 分钟,然后每个页面都弄个 30 秒的定时器触发垃圾请求,也能大力出奇迹。
    explore365
        16
    explore365  
       2022-08-18 15:08:53 +08:00
    cookie 定时更新每个 tab 时间戳,检查 tab 列表,时间戳超时则为退出。
    Puteulanus
        17
    Puteulanus  
       2022-08-18 15:10:56 +08:00   2
    换一个思路,用跨标签页通信呢?

    第一个标签页触发登陆之后,登陆凭据不是保存在 cookie 里,而是只存在内存里,从第二个标签页开始,打开时的登陆凭据都靠和已经打开的标签页沟通来拿到

    这样所有标签页关闭之后合法登陆凭据消失,下一个“第一个标签页”拿不到凭据,再次触发用户登陆操作
    nothingistrue
        18
    nothingistrue  
       2022-08-18 15:31:28 +08:00
    刚去看了下 localStorage 、sessionStorage ,发现 sessionStorage 是基于标签页的,这样是有办法监控当前域名打开的标签页的个数的。

    思路就是:
    用与标签页无关的 localStorage 存储计数,每打开一个标签页就加 1 ,每关闭一个标签页就减 1 ;
    用于标签页有关的 sessionStrrage 结合 load unload 事件来触发标签页打开和标签页关闭事件,主要是把刷新标签页给区分出去,怎么区分还要仔细想一想,不是太好处理。
    给 unload 事件加个监听,通过 localStorage 的计数,来判断是否需要触发登出处理,也需要区分刷新跟关闭。
    lonenol
        19
    lonenol  
       2022-08-18 15:35:13 +08:00
    需求不合理,直接砍需求。。
    dudubaba
        20
    dudubaba  
       2022-08-18 15:38:16 +08:00
    试试 BroadcastChannel 监听
    nothingistrue
        21
    nothingistrue  
       2022-08-18 15:40:08 +08:00
    还是遇到难题了,新建标签页跟刷新事件很好区分,但是关闭标签页跟刷新事件,不好区分。
    yunye
        22
    yunye  
       2022-08-18 15:50:03 +08:00   1
    谁提的需求谁自己来实现
    shyling
        23
    shyling  
       2022-08-18 16:00:52 +08:00
    一个标签连个 ws ,全断了就全关了
    nothingistrue
        24
    nothingistrue  
       2022-08-18 16:05:01 +08:00   3
    我搜了一圈,没有找到区分 unload 是刷新还是关闭的方法,也就是说没有标签页关闭事件( windows.onclose 还在试验中)。所以这个每必要再研究下去了,就是不可实现的需求,怼回去。就留一个长时间未操作服务器端自动退出就可以了,浏览器端就别搞骚套路了。
    Imindzzz
        25
    Imindzzz  
       2022-08-18 16:49:41 +08:00
    window.Onload= () => {
    const count = parseInt(localStorage.getItem('count') || '0', 10);
    localStorage.setItem('count', count + 1 + '');

    const lastExitTime = parseInt(sessionStorage.getItem('lastExitTime') || '0', 10);
    // 三秒内回来,表示是刷新了
    if (lastExitTime && Date.now() - lastExitTime < 30000) {
    // TODO 发送接口 还原登录状态
    }
    };

    window.Onbeforeunload= (event) => {
    const count = parseInt(localStorage.getItem('count') || '0', 10);
    localStorage.setItem('count', count - 1 + '');
    if (count <= 1) {
    // TODO 但是其实这里无法区分是刷新还是关闭。 需要后端
    sessionStorage.setItem('lastExitTime', Date.now().toString());

    // TODO 发送接口退出
    // 发送网络请求建议使用 sendBeacon https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/sendBeacon

    // 注意:页面必须有用户操作才能正常拦截
    event.preventDefault();
    event.returnValue = '所有都关闭了';
    return '所有都关闭了';
    }
    };
    autoxbc
        26
    autoxbc  
       2022-08-18 16:50:37 +08:00
    用 BroadcastChannel 向其他标签广播,没有回应说明是最后一个标签,然后用 sendBeacon 向服务器发送信息
    lin07hui
        27
    lin07hui  
       2022-08-18 17:08:16 +08:00
    当前的浏览器都没法区分开“关闭标签页跟刷新事件”
    dtdths1
        28
    dtdths1  
       2022-08-18 19:16:00 +08:00
    无法监听,over 。你要是想退出可用心跳,让服务端判断
    v2eb
        29
    v2eb  
       2022-08-18 19:27:16 +08:00 via Android
    每个 tab 页分配个唯一 id , 集合形式存到 local storage .tab 页关闭时判断集合长度
    chnwillliu
        30
    chnwillliu  
       2022-08-18 19:47:32 +08:00 via Android
    @v2eb 关闭和刷新分不出来的,只有一个 tab 一刷新结果登出了?
    chnwillliu
        31
    chnwillliu  
       2022-08-18 19:55:24 +08:00 via Android
    不妨想想为什么登出一定要调用 logout 接口,不调用会有什么后果?是后端有资源要释放么?用户浏览器直接崩也是可能没机会调用 js 的 unload 的,所以关键逻辑不能依赖 logout 接口。
    zhuweiyou
        32
    zhuweiyou  
       2022-08-18 21:29:01 +08:00
    session 行为就是这样的. 不需要前端干预. 这帮后端不行,还要前端背锅?
    realpg
        33
    realpg  
    PRO
       2022-08-18 21:52:38 +08:00
    这需求有啥奇葩的...
    这不是动态网页出来这二三十年的最基本的一个操作^
    chnwillliu
        34
    chnwillliu  
       2022-08-19 08:13:36 +08:00 via Android   1
    @realpg 这还不奇葩?这跟动态网页有什么关系?你哪怕是 windows 客户端也不能保证用户退出就一定能调用到服务端的 logout 接口。我强杀进程,直接强制关机等等,都可能导致客户端直接就消失不调用你的 logout 接口。

    logout 是要干什么?统计在线人数么?还是释放啥资源还是什么?非要强依赖客户端在线与否,那只能建立 socket 或者让客服端发心跳。期望客户端恭恭敬敬给你调用 logout ,那只能说无法保证。
    realpg
        35
    realpg  
    PRO
       2022-08-19 12:01:40 +08:00
    @chnwillliu #34
    现在的开发者连有个东西叫 session,有个东西叫 cookie 都不知道了
    realpg
        36
    realpg  
    PRO
       2022-08-19 12:11:00 +08:00
    @chnwillliu #34
    而且, 不依赖 cookie, 也可以用 localstorage 和 sessionstorage 实现他要求的掉登录状态机制
    DingJZ
        37
    DingJZ  
       2022-08-19 13:42:09 +08:00
    这玩意还真实现了一个,上面大家说 cookie 之类的,是可以,但是后端不改怎么办,整个用户体系都是基于 token 的,改不现实,需求还是提给前端的。
    用 BroadcastChannel ,可以 tab 之前通信,退出的时候发通知,所有页面订阅,我把代码放在 nginx 里注入到项目里,这样业务不需要关注
    dtdths1
        38
    dtdth1  
       2022-08-19 16:39:51 +08:00
    凡是让客户端主动去调的,不管用什么,全不靠谱。各种异常情况、兼容问题,不要想当然
    chnwillliu
        39
    chnwillliu  
       2022-08-20 14:44:01 +08:00 via Android
    @realpg 依赖 session 类型的 cookie 然后捏?怎么在合适的时候调用 logout 接口?所以说要改实现,因为没法保证调用后端的 logout 接口。

    还有哦,sessionStorage 和 session 类型的 cookie 行为不一致。sessionStorage 不跨 tab.
    chnwillliu
        40
    chnwillliu  
       2022-08-20 14:48:59 +08:00 via Android
    @realpg 对,其实用 session 类型的 cookie 存东西就行,用户关了所有 tab 自动被清掉。用 sessionStorage 存的话要考虑跨 tab 共享问题,解决完这个问题,行为就和 session cookie 一致了。

    利用好了,根本就不要什么 logout 接口。
    Jexxie
        41
    Jexxie  
    OP
       2022-08-24 17:30:01 +08:00
    感谢大家,最后改需求了,沿用目前方案,遵循浏览器默认行为,完全关闭浏览器后退出登录。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     961 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 22:57 PVG 06:57 LAX 15:57 JFK 18:57
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86