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 · 5562 次点击
    这是一个创建于 858 天前的主题,其中的信息可能已经有所发展或是发生改变。

    奇葩需求:所有这个网站的标签页 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
    这是后端的坑需要前端填吗?

    打脸:

    如果客户端异常了,停电了,服务端就不会自动退出吗?
    maggch97
        3
    maggch97  
       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
    让后端去做,前端没这个义务做。
    wunonglin
        8
    wunonglin  
       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  
       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  
       2022-08-19 12:01:40 +08:00
    @chnwillliu #34
    现在的开发者连有个东西叫 session,有个东西叫 cookie 都不知道了
    realpg
        36
    realpg  
       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
    dtdths1  
       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   ·   实用小工具   ·   1028 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 19:58 · PVG 03:58 · LAX 11:58 · JFK 14:58
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.