H5开发返回上一页是一个很辣手的问题。开发中,我们一般用history.go(-1)返回上一页,原生返回按钮的动作也是history.go(-1)。由于业务需求,某些页面返回需要go(-2)或go(-3)、go(-n),有些页面返回时还有加一个弹窗,让用户再次确认是否离开,这些都有监听物理机的返回事件。开发时,思维打结了,没想到“监听物理机的返回事件”这关键句子,踩了挺多坑的,本文就是记录一下小编的踩坑过程。
前几天就遇到了一个需求,离开正在编辑的页面时,弹窗提示用户,尚未保存编辑信息,是否确定离开。确定则离开,取消则留下继续编辑。当时觉得很简单,调用window.onbeforeunload便可,很快实现了这个需求。
beforeunload
// beforeunload实现,优点:简单,缺点:弹窗不能自定义,而且兼容性不好
window.addEventListener('beforeunload', (event) => {
// Cancel the event as stated by the standard.
event.preventDefault();
// Chrome requires returnValue to be set.
event.returnValue = '';
});
beforeunload的缺点太多,局限性太大,个人觉得不是一个好的方案,于是有了下面的踩坑。
history
由于所有的返回上一页都是调用history.go(-1)。于是去百度go(-1)的监听事件popstate,然后在监听回调里让自定义弹窗展示。思路是没错,可弹窗没弹出来。于是在进入页面前调localStorage加了个缓存,在监听的回调里把缓存清除,测试回调是否执行。是的,回调没有执行,心里一千个草泥马在奔腾,一度怀疑这api是假的。
于是把MDN文档的例子复制过来,原来是思维打结了。
window.addEventListener('popstate', (event) => {
console.log("location: " + document.location + ", state: " + JSON.stringify(event.state));
});
history.pushState({page: 1}, "title 1", "?page=1");
history.pushState({page: 2}, "title 2", "?page=2");
history.replaceState({page: 3}, "title 3", "?page=3");
history.back(); // Logs "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // Logs "location: http://example.com/example.html, state: null
history.go(2); // Logs "location: http://example.com/example.html?page=3, state: {"page":3}
由于小编开发是多页应用,go(-1)就直接返回上一页了,没触发监听。要触发监听,进入页面后必须使用了history.pushState()(该api可以改变浏览器历史记录,可以改变URL还不会触发页面重新请求。)添加一条自定义历史记录。这时再调go(-1),浏览器检测不到url变化,就不会重载页面,这时监听才能触发。
function doCcallback() {
// 自定义弹窗
var r = confirm("是否退出");
if (r == true) {
history.go(-1);
} else {
addState();
}
}
// 监听浏览器返回
window.addEventListener("popstate", function (e) {
if (history.state === null || history.state._is !== "first") {
doCcallback();
}
});
// 使用异步,目的是等待浏览器历史记录发生变化后在添加自定义记录,这样自定义记录会准确添加到栈顶
setTimeout(function () {
if (history.state === null || history.state._is !== "first") {
addState();
}
}, 300);
function addState() {
// 添加浏览器历史记录,url就使用当前的页面的url
history.pushState({ _is: "first" }, "", location.href);
}
查了一下history.pushState的兼容性,兼容性也还可以。有时候就这么神奇,突然脑袋又多了另一个想法,也可以添加历史记录不触发浏览器请求。
hash
function doCcallback() {
// 自定义弹窗
var r = confirm("是否退出");
if (r == true) {
history.go(-1);
} else {
addState();
}
}
// 监听路由hash值是否改变
window.addEventListener("hashchange", function (e) {
// 判断存在hash值才进入
if ("#goback" !== location.hash) {
doCcallback();
}
});
// 使用异步,目的是等待浏览器历史记录发生变化后在添加自定义记录,这样自定义记录会准确添加到栈顶
setTimeout(function () {
if ("#goback" !== location.hash) {
addState();
}
}, 300);
function addState() {
// 添加浏览器历史记录
location.hash = "goback";
}
该方法优点:兼容性好,缺点:在SPA应用中可能会冲突。有时候就这么神奇,突然脑袋又多了另一个想法。记得vue-router使用了两种路由实现方式,供用户选择。
history and hash
function MonitorBack(config) {
if (Object.prototype.toString.call(config) !== "[object Object]") {
config = {};
}
if (Object.getPrototypeOf(this).constructor !== MonitorBack) {
return new monitorBack();
}
var mode = config.mode || "hash"; // 模式
var self = this;
var history = window.history;
var location = window.location;
var methods = {
// 存储事件回调
goback: [],
};
if (mode === "history") {
historyMode();
} else {
hashMode();
}
this.getMode = function setMode() {
return mode;
};
// 返回上一页
this.back = function back() {
var backCount = -1;
if (mode === "history") {
backCount = history.state && history.state._is === "first" ? -2 : -1;
} else {
backCount = location.hash === "#goback" ? -2 : -1;
}
history.go(backCount);
};
// 停留
this.stay = function stay() {
history.forward();
};
// 监听
this.on = function on(key, cb) {
if (typeof key !== "string") {
return;
}
if (typeof cb !== "function") {
return;
}
var method = methods[key];
method.push(cb);
};
// 取消监听
this.off = function off(key, cb) {
if (typeof key !== "string") {
return;
}
if (typeof cb !== "function") {
return;
}
var method = methods[key] || [];
for (var i = 0, l = method.length; i < l; i++) {
if (method[i] === cb) {
method.splice(i, 1);
break;
}
}
};
function historyMode() {
// history 模式
setTimeout(function () {
if (history.state === null || history.state._is !== "first") {
history.pushState({ _is: "first" }, "", location.href);
}
}, 300);
window.addEventListener("popstate", function (e) {
if (history.state === null || history.state._is !== "first") {
var method = methods.goback || [];
for (var i = 0, l = method.length; i < l; i++) {
method[i].call(self);
}
}
});
}
function hashMode() {
// hash模式
setTimeout(function () {
if ("#goback" !== location.hash) {
location.hash = "goback";
}
}, 300);
window.addEventListener("hashchange", function (e) {
// 判断存在hash值才进入
if ("#goback" !== location.hash) {
var method = methods.goback || [];
for (var i = 0, l = method.length; i < l; i++) {
method[i].call(self);
}
}
});
}
}
var monBack = new MonitorBack({ mode: "history" });
monBack.on("goback", function () {
var r = confirm("是否退出");
if (r == true) {
this.back();
} else {
this.stay();
}
});
❤️爱心三连击1.看到这里了就点个在看支持下吧,你的「点赞,在看」是我创作的动力。
2.关注公众号程序员成长指北,回复「1」加入Node进阶交流群!「在这里有好多 Node 开发者,会讨论 Node 知识,互相学习」!
3.也可添加微信【ikoala520】,一起成长。
“在看转发”是最大的支持