前言

 最近实施的同事报障,说用户审批流程后直接关闭浏览器,操作十余次后系统就报用户会话数超过上限,咨询4A同事后得知登陆后需要显式调用登出API才能清理4A端,否则必然会超出会话上限。

 即使在页面上增添一个登出按钮也无法保证用户不会直接关掉浏览器,更何况用户已经习惯这样做,增加功能好弄,改变习惯却难啊。这时想起N年用过的window.onbeforeunloadwindow.onunload事件。

 本文记录重拾这两个家伙的经过,以便日后用时少坑。

为网页写个Dispose方法

 C#中我们会将释放非托管资源等收尾工作放到Dispose方法中, 然后通过using语句块自动调用该方法。对于网页何尝不是有大量收尾工作需要处理呢?那我们是否也有类似的机制,让程序变得更健壮呢?——那就靠beforeunloadunload事件了。但相对C#通过using语句块自动调用Dispose方法,beforeunloadunload的触发点则复杂不少。

 我们看看什么时候会触发这两个事件呢?

  1. 在浏览器地址栏输入地址,然后点击跳转;
  2. 点击页面的链接实现跳转;
  3. 关闭或刷新当前页面;
  4. 操作当前页面的Location对象,修改当前页面地址;
  5. 调用window.navigate实现跳转;
  6. 调用window.opendocument.open方法在当前页面加载其他页面或重新打开输入流。

     OMG!这么多操作会触发这两兄弟,怎么处理才好啊?没啥办法,针对功能需求做取舍咯。对于我的需求就是在页面的Dispose方法中调用登出API,经过和实施同事的沟通——只要刷新页面就触发登出。
;(function(exports, $, url){
exports.dispose = $.proxy($.get, $, url)
}(window, $, "http://pseudo.com/logout"))

那现在剩下的问题就在于到底是在beforeunload还是unload事件处理函数中调用dispose方法呢?这里涉及两点需要探讨:

  1. beforeunloadunload的功能定位是什么?
  2. beforeunloadunload的兼容性.

beforeunloadunload的功能定位是什么?

beforeunload顾名思义就是在unload前触发,可通过弹出二次确认对话框来试图终断执行unload.

unload就是正在进行页面内容卸载时触发的,一般在这里进行一些重要的清理善后工作,而这时页面处于以下一个特殊的临时状态:

  1. 页面所有资源(img, iframe等)均未被释放;
  2. 页面可视区域一片空白;
  3. UI人机交互失效(window.open,alert,confirm全部失效);
  4. 没有任何操作可以阻止unload过程的执行。(unload事件的Cancelable属性值为No)

 那么反过来看看beforeunload事件,这时页面状态大致与平常一致:

  1. 页面所有资源均未释放,且页面可视区域效果没有变化;
  2. UI人机交互失效(window.open,alert,confirm全部失效);
  3. 最后时机可以阻止unload过程的执行.(beforeunload事件的Cancelable属性值为Yes)

beforeunloadunload的兼容性

 对于移动端浏览器而言(Safari, Opera Mobile等)而言不支持beforeunload事件,也许是因为移动端不建议干扰用户操作流程吧。

防数据丢失机制——二次确认

 当用户正在编辑状态时,若因误操作离开页面而导致数据丢失常作为例外处理。处理方式大概有3种:

  1. 丢了就丢呗,然后就是谁用谁受罪了;
  2. 简单粗暴——侦测处于编辑状态时,监听beforeunload事件作二次确定,也就是将责任抛给用户;
  3. 自动保存,甚至做到Work in Progress(参考john papa的分享John Papa-Progressive Savingr-NG-Conf)

     这里我们选择方式2,弹出二次确定对话框。想到对话框自然会想到window.confirm,然后很自然地输入以下代码
window.addEventListener('beforeunload', function(e){
var msg = "Do u want to leave?\nChanges u made may be lost."
if (!window.confirm(msg)){
e.preventDefault()
}
})

然后刷新页面发现啥都没发生,接着直接蒙了。。。。。。

坑1: 无视window.alert/confirm/prompt/showModalDialog

beforeunloadunload是十分特殊的事件,要求事件处理函数内部不能阻塞当前线程,而window.alert/confirm/prompt/showModalDialog却恰恰就会阻塞当前线程,因此H5规范中以明确在beforeunloadunload中直接无视这几个方法的调用。

Since 25 May 2011, the HTML5 specification states that calls to window.showModalDialog(), window.alert(), window.confirm() and window.prompt() methods may be ignored during this event.(onbeforeunload#Notes)[https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#Notes]

在chrome/chromium下会报"Blocked alert/prompt/confirm() during beforeunload/unload."的JS异常,而firefox下则连异常都懒得报。

 既然不给用window.confirm,那么如何弹出二次确定对话框呢?其实beforeunload事件已经为我们准备好了。只要改成

window.onbeforeunload = function(){
var msg = "Do u want to leave?\nChanges u made may be lost."
return msg
}

 通过DOM0 Event Model的方式监听beforeunload事件时,只需返回值不为undefined或null,即会弹出二次确定对话框。而IE和Chrome/Chromium则以返回值作为对话框的提示信息,Firefox4开始会忽略返回值仅显式内置的提示信息.

 太不上道了吧,还在用DOM0 Event Model:( 那我们来看看DOM2 Event Model是怎么一个玩法

// Microsoft DOM2-ish Event Model
window.attachEvent('onbeforeunload', function(){
var msg = "Do u want to leave?\nChanges u made may be lost."
var evt = window.event
evt.returnValue = msg
})

对于巨硬独有的DOM2 Event Model,我们通过设置window.event.returnValue为非null或undefined来实现弹出窗的功能(注意:函数返回值是无效果的)

那么标准的DOM2 Event Model呢?我记得window.event.returnValue是 for ie only的,但事件处理函数的返回值又木有效果,那只能想到event.preventDefault()了,但event.preventDefault()没有带入参的重载,那么是否意味通过标准DOM2 Event Model的方式就不支持自定义提示信息呢?

window.addEventListeners('beforeunload', function(e){
e.preventDefault()
})

在FireFox上成功弹出对话框,但Chrome/Chromium上却啥都没发生。。。。。。

坑2: HTMLElement.addEventListener事件绑定

event.preventDefault()这一玩法就FireFox支持,Chrome这次站到IE的队列上了。综合起来的玩法是这样的

;(function(exports){
exports.genDispose = genDispose /**
* @param {Function|String} [fnBody] - executed within the dispose method when it's data type is Function
* as return value of dispose method when it's data type is String
* @param {String} [returnMsg] - as return value of dispose method
* @returns {Function} - dispose method
*/
function genDispose(fnBody, returnMsg){
var args = getArgs(arguments) return function(e){
args.fnBody && args.fnBody()
if(e = e || window.event){
args.returnMsg && e.preventDefault && e.preventDefault()
e.returnValue = args.returnMsg
} return args.returnMsg
}
} function getArgs(args){
var ret = {fnBody: void 0, returnMsg: args[1]},
typeofArg0 = typeof args[0] if ("string" === typeofArg0){
ret.returnMsg = args[0]
}
else if ("function" === typeofArg0){
ret.fnBody = args[0]
} retrn ret
} }(window)) // uses
var dispose = genDispose("Do u want to leave?\nChanges u made may be lost.")
window.onbeforeunload = dispose
window.attachEvent('onbeforeunload', dispose)
window.addEventListener('beforeunload', dispose)

坑3: 尊重用户的选择

 有办法阻止用户关闭或刷新页面吗?没办法,二次确定已经是对用户操作的最大限度的干扰了。

问题未解决——Cross-domain Redirection

;(function(exports){
exports.Logout = Logout function Logout(url){
if (this instanceof Logout);else return new Logout(url)
this.url = url
}
Logout.prototype.exec = function(){
var xhr = new XMLHttpRequest()
xhr.open("GET", this.url, false)
xhr.send()
}
}(window)) var url = "http://pseudo.com/logout",
logout = new Logout(url)
var dispose = $.proxy(logout.exec, logout) var prefix = 'on'
(window.attachEvent || (prefix='', window.addEventListener))(prefix + 'unload', dispose)

 当我以为这样就能交功课时,却发现登出url响应状态编码为302,而响应头Location指向另一个域的资源,并且不存在Access-Control-Allow-Origin等CORS响应头信息,而XHR对象不支持Cross-domain Redirection,因此登出失效。

 以前只知道XHR无法执行Cross-domain资源的读操作(支持写操作),但只以为仅仅是不支持respose body的读操作而已,没想到连respose header的读操作也不支持。那怎么办呢?既然读操作不行那采用嵌套Cross-domain资源总行吧。然后有了以下的填坑过程:

  1. 第一想到的就是嵌套iframe来实现,当iframe的实例化成本太高了,导致iframe还没来得及发送请求就已经完成unload过程了;
  2. 于是想到了通过script发起请求, 因为respose body的内容不是有效脚本,因此会报脚本解析异常,若设置type="text/tpl"等内容时还不会发起网络请求;另外iframe、script等html元素均要加入DOM树后才能发起网络请求;
  3. 最后想到HTMLImageElement,只要设置src属性则马上发起网络请求,而且返回非法内容导致解析失败时还是默默忍受,特别适合这次的任务:)

 于是得到下面的版本

;(function(exports){
exports.Logout = Logout function Logout(url){
if (this instanceof Logout);else return new Logout(url)
this.url = url
}
Logout.prototype.exec = function(){
var img = Image ? new Image() : document.createElement("IMG")
img.src = this.url
}
}(window))

[before]unload导致性能下降?

 现在我们都明白如何利用[before]unload来做资源释放等善后工作了。

 但请记住一点:由于[before]unload事件会降低页面性能,因此仅由于需要做重要的善后或不可逆的清理工作时才监听这两个事件。

 以前,当我们从页面A跳转到页面B时,页面A的所有资源将被释放(销毁DOM对象,回收JS对象, 释放解码后的Image资源等);后来各大浏览器厂商分别采用bfcache/page cache/fast history navigation机制,将页面A的状态保存到缓存中,当通过浏览器的后退/前进按钮跳转时马上从缓存中恢复页面,而不是重新实例化。以下情况将不被缓存起来:

  1. 监听unloadbeforeunload事件;
  2. 响应头Cache-Control: no-store;
  3. 对于采用HTTPS协议的响应头,满足以下一个或以上:

    3.1. Cache-Control: no-cache

    3.2. Pragma: no-cache

    3.3. 存在Expires超期的
  4. 发生跳转时,页面存在未加载完的资源
  5. 旗下iframe存在上述情况的
  6. 页面在iframe中渲染,当用户修改iframe.src加载其他文档到该iframe时

 因此若执行不可逆的清理工作时,对于现代浏览器而言我们应该订阅pagehide事件,而不是unload事件,以便利用Page Cache机制。

事件发生顺序:load->pageshow->pagehide->unload

pageshowpagehide的事件对象存在一个persisted属性,为true时表示从cache中恢复,false表示重新实例化。

 经简单测试发现chrome默认没有启用该特性,而Firefox则默认启用。实验代码:

// index.html
window.addEventListener('load', function(){
console.log("index.load")
window.test = true
})
window.addEventListener('pageshow', function(e){
console.log("index.pageshow.persisted:" + e.persisted)
console.log("index.test:" + window.test)
}) <a href="./next.html">next.html</a>
// next.html
window.addEventListener('load', function(){
console.log("next.load")
})
window.addEventListener('pageshow', function(e){
console.log("next.pageshow.persisted:" + e.persisted)
})

运行环境:FireFox

操作步骤:1.首先访问index.html,2.然后点击链接跳转到next.html,3.然后点击浏览器的回退按钮跳转到index.html,4.最后点击浏览器的前进按钮跳转到next.html。

输出结果:

// 1
index.load
index.pageshow.persisted:false
index.test:true
// 2
next.load
next.pageshow.persisted:false
// 3
index.pageshow.persisted:true
index.test:true
//4
next.pageshow.persisted:true

 看到页面是从bfcache恢复而来的,所以JS对象均未回收,因此window.test值依然有效。另外load仅在页面初始化后才会触发,因此从bfcache中恢复页面时并不会触发。

 假如在index.html上订阅了unloadbeforeunload事件,那么该页面将不会保存到bfcache。

 另外通过jQuery.ready来监听页面初始化事件时,不用考虑bfcache的影响,因为它帮我们处理好了:)

总结

若有纰漏望请指正,谢谢!

尊重原创,转载请注明来自:http://www.cnblogs.com/fsjohnhuang/p/5647649.html 肥子John_

感谢

window-onbeforeunload-not-working

beforeunload

unload

prompt-to-unload-a-document

webkit page cache i - the basics

webkit page cache ii - the unload event

pagehide

pageshow

Redirects Do’s and Don’ts

Using_Firefox_1.5_c aching#New_browser_events

cross-browser-onload-event-and-the-back-button

JS魔法堂:定义页面的Dispose方法——[before]unload事件启示录的更多相关文章

  1. 定义页面的Dispose方法:[before]unload事件启示录

    前言 最近实施的同事报障,说用户审批流程后直接关闭浏览器,操作十余次后系统就报用户会话数超过上限,咨询4A同事后得知登陆后需要显式调用登出API才能清理4A端,否则必然会超出会话上限. 即使在页面上增 ...

  2. JS魔法堂:那些困扰你的DOM集合类型

    一.前言 大家先看看下面的js,猜猜结果会怎样吧! 可选答案: ①. 获取id属性值为id的节点元素 ②. 抛namedItem is undefined的异常 var nodes = documen ...

  3. JS魔法堂:追忆那些原始的选择器

    一.前言                                                                                                 ...

  4. JS魔法堂:不完全国际化&本地化手册 之 理論篇

    前言  最近加入到新项目组负责前端技术预研和选型,其中涉及到一个熟悉又陌生的需求--国际化&本地化.熟悉的是之前的项目也玩过,陌生的是之前的实现仅仅停留在"有"的阶段而已. ...

  5. JS魔法堂:判断节点位置关系

    一.前言 在polyfill querySelectorAll 和写弹出窗时都需要判断两个节点间的位置关系,通过jQuery我们可以轻松搞定,但原生JS呢?下面我将整理各种判断方法,以供日后查阅. 二 ...

  6. JS魔法堂:jsDeferred源码剖析

    一.前言 最近在研究Promises/A+规范及实现,而Promise/A+规范的制定则很大程度地参考了由日本geek cho45发起的jsDeferred项目(<JavaScript框架设计& ...

  7. JS魔法堂:属性、特性,傻傻分不清楚

    一.前言 或许你和我一样都曾经被下面的代码所困扰 var el = document.getElementById('dummy'); el.hello = "test"; con ...

  8. JS魔法堂:LINK元素深入详解

    一.前言 我们一般使用方式为 <link type="text/css" rel="stylesheet" href="text.css&quo ...

  9. JS魔法堂:浏览器模式和文档模式怎么玩?

    一.前言 从IE8开始引入了文档兼容模式的概念,作为开发人员的我们可以在开发人员工具中通过“浏览器模式”和“文档模式”(IE11开始改为“浏览器模式”改成更贴切的“用户代理字符串”)品味一番,它的出现 ...

随机推荐

  1. dom中一些节点获取和增改

    1获取标签里的文本对象: 对象.innerText 获取标签里的文本内容     早期的火狐浏览器中是不支持的 赋值会输出转义后的内容 对象.innerHTML 获取标签里的所有内容 赋值会输出原样 ...

  2. Visual Studio跨平台开发Xamarin

    台湾微软的一系列Visual Studio跨平台开发Xamarin的资料,上面还有视频.具体参看 http://www.microsoft.com/taiwan/newsletter/library/ ...

  3. .NET中的DES对称加密

    DES是一种对称加密(Data Encryption Standard)算法,于1977年得到美国政府的正式许可,是一种用56位密钥来加密64位数据的方法.一般密码长度为8个字节,其中56位加密密钥, ...

  4. 剑指Offer面试题:12.在O(1)时间删除链表结点

    一.题目:在O(1)时间删除链表结点 题目:给定单向链表的头指针和一个结点指针,定义一个函数在O(1)时间删除该结点. 原文采用的是C/C++,这里采用C#,节点定义如下: public class ...

  5. c#语言-高阶函数

    介绍 如果说函数是程序中的基本模块,代码段,那高阶函数就是函数的高阶(级)版本,其基本定义如下: 函数自身接受一个或多个函数作为输入. 函数自身能输出一个函数,即函数生产函数. 满足其中一个条件就可以 ...

  6. 人人都是 DBA(XII)查询信息收集脚本汇编

    什么?有个 SQL 执行了 8 秒! 哪里出了问题?臣妾不知道啊,得找 DBA 啊. DBA 人呢?离职了!!擦!!! 程序员在无处寻求帮助时,就得想办法自救,努力让自己变成 "伪 DBA& ...

  7. [ASP.NET MVC 小牛之路]15 - Model Binding

    Model Binding(模型绑定)是 MVC 框架根据 HTTP 请求数据创建 .NET 对象的一个过程.我们之前所有示例中传递给 Action 方法参数的对象都是在 Model Binding ...

  8. Data Profiling Task

    Data Profiling Task 是用于收集数据的Metadata的Task,在使用ETL处理数据之前,应该首先检查数据质量,对数据进行分析,这将对Table Schema的设计结构和生成ETL ...

  9. 解密jQuery内核 DOM操作方法(二)html,text,val

    回顾下几组DOM插入有关的方法 innerHTML 设置或获取位于对象起始和结束标签内的 HTML outerHTML 设置或获取对象及其内容的 HTML 形式 看图对照区别 innerText 设置 ...

  10. Android图片选择器

    1.概述 应公司项目需求,要做一个图片选择器,网上搜索了一些源码,我在别人的基础上进行了修改,另外页面也进行了重整,我的是先加载图片文件夹列表,然后再进入选择图片.            参考博客地址 ...