前言
每次遇到浏览器的返回问题,就很麻烦。因为安卓ios的返回机制往往有区别,又因为h5常常是作为混合开发内嵌在app中,不同的app中表现形式又不一样,而且业务需求总是需要各种无厘头的跳转和返回。
遇到的多了,俺就想写篇文章总结下。也算是抛砖引玉,希望有大佬路过甩下几个更合适的解决方案拯救下我等菜鸡。
按照以往写文章的惯例,我会先简单把相关知识点理一遍,然后结合具体案例讲解。
一,history对象知识点
这部分的知识点具体可以看MDN的,我这里只是简单提及。
window对象通过 history 对象提供了对浏览器的会话历史的访问。允许在用户浏览历史中向前和向后跳转,从 HTML5 开始增加了对 history 栈中内容的操作。
1.1,属性值:
length:历史堆栈中页面的数量,是个只读的属性,最大只能100.
scrollRestoration: 滚动恢复属性允许web应用程序在历史导航上显式地设置默认滚动恢复行为。该属性有两个可选值,默认为auto,将恢复用户已滚动到的页面上的位置。另一个值为:manual,不还原页上的位置,用户必须手动滚动到该位置。(这个属性在移动端存在坑,后续文章我会提及)
state:返回一个表示历史堆栈顶部的状态的值,这是一种可以不必等待popstate事件而查看状态的方式。history.state 只在支持 HTML5 History API 的浏览器中可用,并且只有在页面使用了 pushState() 或 replaceState() 方法改变了历史记录状态时才会有值(并且只改变该历史记录的state)。如果页面还没有改变过历史记录状态,history.state 的值为 null。
这里让人疑惑的只有这个state,具体来说,这个state可以用来暂存值.主要实现如下三个功能,具体会在下文展开:
保存页面状态:在使用浏览器的前进和后退按钮进行页面导航时,可以将当前页面的状态数据存储在 window.history.state 中。这样,在后续导航到该页面时,可以从 window.history.state 中获取之前保存的状态数据,并在页面中进行相应的处理。
数据传递:通过修改 window.history.state 的值,可以在页面之间传递数据。例如,当从一个页面导航到另一个页面时,可以在 window.history.state 中设置某个属性,下一个页面可以读取该属性并根据其值进行相应的操作。
历史记录管理:window.history.state 可以与 pushState() 和 replaceState() 方法一起使用,来自定义修改浏览器历史记录的行为。通过修改 window.history.state 的值,可以在不刷新页面的情况下改变URL,并添加或替换历史记录条目。
1.2,方法:
1.2.1,history.go/forward/back方法
history.go(x) 去到对应的url历史记录。
history.back() 相当于浏览器的后退按钮。其实就是history.go(-1)
history.forward() 相当于浏览器的前进按钮。其实就是history.go(1)
这三个并不会增减历史记录栈,而是在历史栈中进行跳转,将指针指向对应的节点
例如我打开一个浏览器窗口,这时候历史栈中会有一个节点:
然后我在控制台通过:“window.location.href=”www.baidu.com“”跳转到百度页面,历史栈的情况就会变成:
继续在控制台通过:“window.location.href=”www.zhihu.com“”跳转到知乎页面,历史栈的情况就会变成:
接下来,历史记录栈的指针变化和我对应的操作记录如下:
由此可以很明显地看到,history.go/forward/back这三个方法并不会变更历史记录栈的条数,而只是在已有的历史记录栈中切换指针的指向,从而切换页面.
1.2.2,pushState方法
接下来看另一个方法:pushState
history.pushState(object, title, url)方法接受三个参数
第一个参数用于存储该url对应的状态对象,该对象可在onpopstate事件中获取,也可在history对象中获取。
第二个参数是标题,通常我们取空参数
第三个参数则是设定的url。一般设置为相对路径,如果设置为绝对路径时需要保证同源。pushState函数向浏览器的历史堆栈压入一个url为设定值的记录,并改变历史堆栈的当前指针至栈顶。
将上文的测试页面切换到知乎页面,然后在控制台执行如下代码:
// 存储页面状态
const state = { page: 'home', tab: 'overview' };
window.history.pushState(state, '', '/home');
得到的结果如下:
可以看到当我们执行了这个代码后:
-
url发生了变更,但页面并没有跳转与刷新.
-
历史记录栈增加了一条,并且指针指向了最新的这一条
-
history.state的值也发生了变更
但是现在我还不知道,pushState的新增记录是在顶部新增还是在当前指针上方新增.
于是可以做如下的实验:
首先我浏览器执行两次返回后回到百度页面,这时候我查看history的内容,发现state是null,这说明设置的state只会存储在对应的历史记录上,而不是所有的历史记录共享.
然后我执行:
// 存储页面状态 const state = { test: 'testStr' }; window.history.pushState(state, '', '/test');
后查看window.history,得到效果如下:
可以看到,pushState是在当前历史记录后新增一条记录,并且会把它后面的历史记录清空.并且指针指向这个新创建的历史记录.
于是,我们就可以得到pushState的特性如下:
url发生了变更,但页面并没有跳转与刷新.
在当前历史记录后方生成一条新的历史记录,并且清空后续所有的历史记录,并且指针指向了新创建的这一条
如果第一个参数有传值,则会且仅会在新创建历史记录的history.state中记录这个值.
第三个url参数,如果设置绝对路径,则必须同源,否则会报错
1.2.3,replaceStae方法
replaceState(object, title, url)
该接口与pushState参数相同,含义也相同。唯一的区别在于replaceState是替换浏览器历史堆栈的当前历史记录为设定的url。需要注意的是,replaceState不会改动浏览器历史堆栈的当前指针。
同样的,我重新打开一个浏览器窗口,依次是初始页面-百度页面-知乎页面,然后我回到百度页面,执行如下代码:
const newState = { page: 2 };
const newTitle = "Page 2";
const newURL = "/page2";
window.history.replaceState(newState, newTitle, newURL);
于是可以知道,replaceState具备如下特性:
- url发生了变更,但页面并没有跳转与刷新.
- 覆盖当前历史记录,并且不清空后续历史记录,指针不发生改变
- 如果第一个参数有传值,则会且仅会在新创建历史记录的history.state中记录这个值.
- 第三个url参数,如果设置绝对路径,则必须同源,否则会报错
1.3,事件
1.3.1,popState事件
popstate事件只会在指针在历史记录栈中已有的位置上移动时会触发,这就意味着调用history.pushState()和history.replaceState()方法不会触发(前者是新创建的历史记录,后者不仅新创建,指针还没移动).
而当我们点击浏览器的前进/后退按钮时会触发,调用history对象的back()、forward()、go()方法时,则触发。
popstate事件的回调函数的参数为event对象,该对象的state属性为随状态保存的那个对象。具体的属性如下:
event.state: 这是一个表示当前状态的对象。当使用pushState方法将状态添加到浏览器历史记录时,可以通过该属性获取存储的状态数据。
event.title: 这是一个字符串,表示当前状态的标题。当使用pushState方法添加状态时,可以通过该属性获取设置的标题。
event.url: 这是一个字符串,表示当前状态的URL。当使用pushState方法添加状态时,可以通过该属性获取设置的URL。
结合上文,这不就是pushState和replaceState的三个参数嘛!但是这三个参数是当前历史记录的state,还是即将进入的历史记录的state呢?俺还不知道,所以需要验证下:
和上文一样,打开一个新的浏览器窗口,创建三个访问记录:初始页面-百度页面-知乎页面,然后执行如下代码:
window.history.pushState({ target: 'MeanSure', random: Math.random() }, '', window.location.href);
window.history.pushState({ target: 'Final', random: Math.random() }, '', window.location.href);
window.addEventListener('popstate', (event) => {
console.log(event)
});
然后点击浏览器的返回按键,得到的效果如下:
可以看到,点击执行两次pushState之后,history的长度变成了5,紧接着点击浏览器返回,页面指针前移一位,并且触发popState事件的监听,打印event,注意到打印出来是Measure,这就说明popState事件的回调函数取到的参数是最新进入的历史记录的状态快照.
1.3.2,利用pushState和popState禁用浏览器返回
用目前知道的这几个知识点,我们就可以实现一个常见的业务需求:禁用浏览器返回.
还是利用上文初始页面-百度页面-知乎页面为例,我们希望在知乎页面禁止浏览器返回,这时候,我们只需要在知乎页面pushState两次,并且对应在state中保存该条历史记录的状态.等popState监听到浏览器返回时,若回调函数的参数是第一个pushState中的状态,则让浏览器历史记录前进一步,回到第二层pushstate的页面即可.
在控制台输入代码:
window.history.pushState({ target: 'MeanSure', random: Math.random() }, '', window.location.href);
window.history.pushState({ target: 'Final', random: Math.random() }, '', window.location.href);
window.addEventListener('popstate', (e) => {
if (e.state && e.state.target === 'MeanSure') {
// 如果是第一层pushState则历史记录前进一格
console.log("监听到浏览器返回")
window.history.forward();
}
});
就是利用历史记录栈中指针发生了移动(点击浏览器返回),从而触发popState事件,当发现是返回到第一层,就强制它回到最新页面.
这样就实现了浏览器的返回禁用.但是很明显的,当我们手动执行window.history.go(-2)
就能绕过这一禁用,直接到达最开始的知乎页面.
温馨提示:
有些版本的浏览器,想要在chrome中或是基于 chrome 的 webview 中实现后退按钮拦截效果,至少需要用户主动进行一次交互操作。才能在历史记录中指针移动时触发popState事件
二,Location 对象知识点
Location 对象提供了 URL 相关的信息和操作方法,通过 document.location
和 window.location
属性都能访问这个对象。
History API 和 Location 对象实际上是通过地址栏中的 URL 关联 的,因为 Location 对象的值始终与地址栏中的 URL 保持一致,所以当我们操作会话浏览历史的记录时,Location 对象也会随之更改;当然,我们修改 Location 对象,也会触发浏览器执行相应操作并且改变地址栏中的 URL。
2,1,location对象属性
window.location.href:完整的 URL;http://username:password@www.test.com:8080/test/index.html?id=1&name=test#test。
window.location.protocol:当前 URL 的协议,包括 :;http:。
window.location.host:主机名和端口号,如果端口号是 80(http)或者 443(https),那就会省略端口号,因此只会包含主机名;www.test.com:8080。
window.location.hostname:主机名;www.test.com。
window.location.port:端口号;8080。
window.location.pathname:URL 的路径部分,从 / 开始;/test/index.html。
window.location.search:查询参数,从 ? 开始;?id=1&name=test。
window.location.hash:片段标识符,从 # 开始;#test。
window.location.username:域名前的用户名;username。
window.location.password:域名前的密码;password。
window.location.origin:只读,包含 URL 的协议、主机名和端口号;http://username:password@www.test.com:8080。
除了 window.location.origin
之外,其他属性都是可读写的;因此,改变属性的值能让页面做出相应变化。例如对 window.location.href
写入新的 URL,浏览器就会立即跳转到相应页面;另外,改变 window.location
也能达到同样的效果。
2.2,改变href
Location.href
属性是浏览器唯一允许跨域写入的属性.当通过将新的 URL 赋值给 window.location.href
属性,可以实现页面的重定向。例如,window.location.href = "https://www.example.com"
将会将页面重定向到指定的 URL.历史记录栈中会新增这一新页面记录.并且清空后续的历史记录.
不仅如此,我们还可以通过它修改hash.
如果href指向的是一个完整的域名地址+hash,则会刷新页面,历史记录加一,且修改hash值.
而如果仅仅是携带hash值,则不会刷新页面,但同样会新增一条历史记录,并且修改hash值.
2.3,改变 hash
直接通过location.hash
改变 hash
并不会触发页面跳转和刷新,因为 hash
链接的是当前页面中的某个片段,所以如果 hash
有变化,那么页面将会滚动到 hash
所链接的位置;当然,页面中如果 不存在 hash
对应的片段,则没有 任何效果。
这看起来和 window.history.pushState(data, title, ?url)
方法非常类似,都能在不刷新页面的情况下更改 URL,并且增加一个历史记录.因此,我们也可以使用 hash
来实现前端路由,但是 hash
相比 pushState
来说有以下缺点:
hash
只能修改 URL 的片段标识符部分,并且必须从 #
开始;而 pushState
却能修改路径、查询参数和片段标识符;因此,在新增会话浏览历史的记录时,pushState
比起 hash
来说更符合以前后端路由的访问方式,也更加优雅。
hash 必须与原先的值不同,才能新增会话浏览历史的记录;而 pushState 却能新增相同 URL 的记录。
hash 想为新增的会话浏览历史记录关联数据,只能通过字符串的形式放入 URL 中;而 pushState 方法却能关联所有能被序列化的数据。
hash 不能修改页面标题,虽然 pushState 现在设置的标题会被浏览器忽略,但是并不代表以后不会支持。
2.4,hashchange
事件
hash即URL中“#”字符后面的部分。使用浏览器访问网页时,如果网页URL中带有hash,页面就会定位到id(或name)与hash值一样的元素的位置,故而又称之为锚点。hash还有另一个特点,它的改变不会导致页面重新加载,因此在单页应用流行的当下,它的用处就更多了。
我们可以通过 hashchange
事件监听 hash
的变化.
它的触发条件是:非pushState和popState触发的hash变化.
它的回调函数的参数有两个值,原始url和修改后的url.
window.addEventListener('hashchange', function (e) {
console.log(e.oldURL);
console.log(e.newURL)
}, false)
2.5,popstate 和 hashchange 的事件顺序
如上文所说,如果hash发生了改变,且在已有的历史记录栈中指针发生了变化,就会同时触发popstate 和 hashchange.
例如:初始页面-百度页面-百度#/test,这时候点击返回,就能都触发.
window.addEventListener('hashchange', function(e) {
console.log(e.oldURL);
console.log(e.newURL);
}, false);
window.addEventListener('popstate',function(e){
console.log('location: ' + document.location);
})
可以看到,popstate会比hashchange先执行.
2.6,Location方法
Location 对象提供以下方法:
2.6.1,window.location.assign(url)
接受一个 URL 字符串作为参数,使得浏览器立刻跳转到新的 URL。
它和location.href一样,会添加一个新的历史记录并清空后面的所有历史记录.(如下图,知乎页面的那条历史记录被清空了.)
2.6.2,window.location.replace(url)
与window.location.assign(url)
实现一样的功能,区别在于 replace
方法执行后跳转的 URL 会 覆盖 浏览历史中的当前记录,因此原先的当前记录就在浏览历史中 删除 了。并且不会清空后续的历史记录.(如下图,最上层的知乎页面历史记录没有被清空,而百度页面则被替换掉了).
2.6.3,windwo.location.reload(boolean)
window.location.reload(boolean)
方法使得浏览器重新加载当前 URL。boolean有两个值,
没有传值或者false:就相当于用户点击浏览器的刷新按钮,这将导致浏览器拉取缓存中的页面.历史记录保持不变.
true:强制重新请求页面(不走缓存),但是目前只有Firefox支持,其他浏览器仅仅是普通刷新.
2.6.4,window.location.tostring()
window.location.toString()
方法返回整个 URL 字符串.相当于读取Location.href
属性.
https://developer.mozilla.org/zh-CN/docs/Web/API/Location/reload?test=sdsda
使用后的返回:
window.location.toString()
'https://developer.mozilla.org/zh-CN/docs/Web/API/Location/reload?test=sdsda'
三,在前端工程中的应用场景
3.1,禁用浏览器返回
这个主要是使用pushState和popState相结合.上文已经给出了具体的使用方法和原理,详细见本文1.3.2小节.
3.2,A页面- B页面-C页面,返回需要回到A页面
这种其实很容易做到,当我们在vue等单页面开发时,只要在B页面跳转C页面时,使用router.replace(‘C页面’)即可.
但是当我们的C页面是外部项目呢(和AB不同域名地址)?就需要使用window.location.replace(url)来进行跳转.这样也能实现C页面返回时,直接回到页面.
对应的历史记录栈如下:
3.3,前端路由中的使用
现在基本上都流行单页面开发,好吧,俺接触编程的时候,已经是单页面开发盛行的时代了.
SPA 就是一个WEB项目只有一个 HTML 页面,一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转。 取而代之的是利用 JS 动态的变换 HTML 的内容,从而来模拟多个视图间跳转。
那么前端路由就需要解决改变 URL触发页面内容变更,并保持页面不刷新.
现在流行的主要是history路由和hash路由两种.
3.3.1,hash路由原理:
hash 可以改变 url ,但是不会触发页面重新加载(hash的改变是记录在 window.history 中),即不会刷新页面。
hash 通过 window.onhashchange 的方式,来监听 hash 的改变,借此实现无刷新跳转的功能。
这里我简单实现下hash路由,其实很简单,就是先收集注册相关的hash及其回调函数,然后监听hash变化,执行该hash对应的回调函数,展示对应的页面即可.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"
name="viewport"
/>
<title>实现简单的hash路由</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
height: 100%;
}
#main {
height: calc(100vh - 50px);
display: flex;
align-items: center;
justify-content: center;
font-size: 3em;
}
#nav {
height: 50px;
position: fixed;
left: 0;
bottom: 0;
width: 100%;
display: flex;
}
#nav a {
width: 25%;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid black;
}
#nav a:not(:last-of-type) {
border-right: none;
}
</style>
</head>
<body>
<main id="main"></main>
<nav id="nav">
<a href="#/">首页</a>
<a href="#/shop">商城</a>
<a href="#/shopping-cart">购物车</a>
<a href="#/mine">我的</a>
</nav>
</body>
<script>
class xsHashRouter {
constructor(routes = []) {
this.routes = routes; // 存储路由集合
this.currentHash = ""; // 存储当前hash值
this.matchRouter = this.matchRouter.bind(this); // 创建设置路由方法
// 页面加载完之后,需要匹配一次路由内容(相当于初始化)
window.addEventListener("load", this.matchRouter, false);
// 页面触发hashchange的时候,匹配路由
window.addEventListener("hashchange", this.matchRouter, false);
}
// 路由匹配方法,匹配路由,将路由的内容设置到页面上
matchRouter(event) {
let hash = "";
if (event.newURL) {
// hashchange才有这个newURL参数,针对hashchange事件获取hash值
hash = this.getUrlPath(event.newURL);
} else {
// 针对load事件获取hash值
hash = this.getUrlPath(location.hash);
}
this.setRouter(hash);
}
// 设置路由
setRouter(hash) {
this.currentHash = hash;
// 查找当前匹配到的路由
let curRoute = this.routes.find(
(route) => route.path === this.currentHash
);
// 如果找不到,默认返回首页路由
if (!curRoute) {
curRoute = this.routes.find((route) => route.path === "/index");
}
let { component } = curRoute; // 解构出路由的component字段
// 将结构出来的component挂载在页面
document.getElementById("main").innerHTML = component;
}
// 获取url上的hash值
getUrlPath(url) {
// 获取hash
return url.indexOf("#") >= 0 ? url.slice(url.indexOf("#") + 1) : "/";
}
}
const router = new xsHashRouter([
{
path: "/",
name: "home",
component: "<div>首页内容</div>"
},
{
path: "/shop",
name: "shop",
component: "<div>商城内容</div>"
},
{
path: "/shopping-cart",
name: "shopping-cart",
component: "<div>购物车内容</div>"
},
{
path: "/mine",
name: "mine",
component: "<div>我的内容</div>"
}
]);
</script>
</html>
因为hash的变化,同样会在历史记录栈中新增历史记录,因此,我们也可以使用浏览器的前进回退按钮实现页面的跳转.(实际上是同一个页面,只是id=”app”容器的内容发生变化,这和我们平时使用vue开发一致).
3.3.2,history路由原理:
有了上文的知识,其实history路由的原理很简单,就是利用 pushState 、 replaceState 来实现无刷新改变url的同时,进行对应页面的展示.(并且使用state记录该页面的状态)
然后如果是浏览器的后退前进啥的,因为是在已有的历史记录栈内变更指针,就可以利用popState来捕获url变更,再展示对应的页面.
下面也简单实现一下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"
name="viewport"
/>
<title>实现简单的history路由</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
height: 100%;
}
#main {
height: calc(100vh - 50px);
display: flex;
align-items: center;
justify-content: center;
font-size: 3em;
}
#nav {
height: 50px;
position: fixed;
left: 0;
bottom: 0;
width: 100%;
display: flex;
}
#nav a {
width: 25%;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid black;
}
#nav a:not(:last-of-type) {
border-right: none;
}
</style>
</head>
<body>
<main id="main"></main>
<nav id="nav">
<a onclick="clickhref('/')">首页</a>
<a onclick="clickhref('/shop')">商城</a>
<a onclick="clickhref('/shopping-cart')">购物车</a>
<a onclick="clickhref('/mine')">我的</a>
</nav>
</body>
<script>
class newHistoryRouter {
constructor(path = []) {
// 创建路由对象,用key,value存储路由
this.routes = path;
// 监听popstate方法(),触发时更新页面
this._bindPopState();
}
// 初始化挂载路由,初始化肯定要用replace
init(path) {
window.history.replaceState({ path: path }, null, path);
this.setRouter(path);
}
// 路由跳转
push(path) {
// history插入新路由
window.history.pushState({ path: path }, '', path);
this.setRouter(path);
}
// 监听popstate方法-监听到路由变化,则让页面切换成对应路由组件
_bindPopState() {
window.addEventListener("popstate", (e) => {
const path = e.state && e.state.path;
this.setRouter(path);
});
}
// 设置路由-找到当前url路由
setRouter(path) {
let curRoute = this.routes.find((route) => route.path === path);
if (!curRoute) {
curRoute=this.routes.find((route) => route.path === "/");
}
let { component } = curRoute; // 解构出路由的component字段
// 将结构出来的component挂载在页面
document.getElementById("main").innerHTML = component;
}
}
const router = new newHistoryRouter([
{
path: "/",
name: "home",
component: "<div>首页内容</div>"
},
{
path: "/shop",
name: "shop",
component: "<div>商城内容</div>"
},
{
path: "/shopping-cart",
name: "shopping-cart",
component: "<div>购物车内容</div>"
},
{
path: "/mine",
name: "mine",
component: "<div>我的内容</div>"
}
]);
router.init(location.pathname);
function clickhref(path) {
router.push(path);
}
</script>
</html>
值得注意的是,这个html需要起一个静态服务器来运行.我们安装个库:
npm install http-server -g
然后在项目根目录执行http-server,在浏览器输入生成的地址,即可在浏览器中看到效果.
值得注意的是history模式刷新会报错404,因为浏览器会把这个链接当成一个正式的请求发送到服务器,如果找不到这个静态资源就会报错。
对此,通常我们的处理方式是nginx进行配置:
// 告诉服务器,当我们访问的路径资源不存在的时候,默认指向静态资源index.html
location / { try_files $uri $uri/ /index.html; }
四,浏览器返回兼容性问题
4.1,混合开发中多层webview的返回问题
在混合开发中,我们的h5是内嵌在app中的.很多时候,我们会对接第三方的一些服务,这时候,就需要进行跳转.
有的时候,我们不是使用window.location.href进行跳转,而是使用的bridge.openPage()之类的客户端原生层api来打开新的第三方页面.
这时候,app就会有两层webview,上层是外部项目,底下才是我们的h5.
如果是新开webview的形式,这时候我们关闭页面,就会把上层的webview关闭,从而露出我们自己的webview.
这种情况有三个坑需要注意.
4.1.1,新开上层webview时,底下的webview会继续执行
如下代码,假设bridge.openPage()是新开webview:
bridge.openPage()
console.log(test)
底下的webview是会继续执行congsole.log(“test”)的.有的时候页面开发利用这一点,可以优化一些体验.
比如说,我们的h5在中间页需要打开第三方新的webview,那么说就可以在打开后,让其代码继续执行,跳转到首页.
这样一来,外部项目运行完毕后,关闭上层webview的时候,就能直接露出底下已经切换到首页并且加载渲染完毕的页面,用户体验会很流畅.
4.1.2,关闭上层webview时,底下的页面是不会重新渲染执行的
这个是所有webview的共性,就好像是我们在电脑的浏览器打开了两个窗口ab,当我们从a切换到b时,b并不会重新刷新.也不会执行js.
这就会带来一个问题,试想如下场景:
a页面有账户余额的展示,新开webview到第三方页面后,进行了消费,用户的账户余额发生了变化,这时候点击返回,关闭第三方webview,那么就会出现这种情况.底下webview没有发生变化,于是余额并没有更新.
这显然是不合理的,通常我们解决办法是使用visibilitychange这个api来监听页面的显示与隐藏.然后更新指定的数据.
document.addEventListener('visibilitychange', () => {
if (!document.hidden){
console.log('-----页面可见了,判断当前是在首页则更新余额接口------');
store.commit('wallet/setAccountLoading', true);
store.dispatch('wallet/getAccount').then(() => {
store.commit('wallet/setAccountLoading', false);
});
} else {
console.log('-----页面不可见了------');
}
});
4.1.3,新开webview其实和浏览器多窗口一样
主要的影响是出现在多个webView是同域名的情况.出现这种情形出要有以下两种情况:
- a项目和b项目部署在同一个域名下,a通过新开webview跳转b项目.
- a项目通过新开webview跳转第三方项目(举个例子是滴滴出行),付款时需要回a项目,如果滴滴那边使用window.location.href进行回跳,那么当前页面还是在新的那层webView上.
这种情况下webview的sessionStorage是不相通的,但是localStorage却是相通的.
对于localstorage来说:
如果b层webview修改了localstorage中a层需要使用的数据,就会出问题.
对于sessionStorage来说:
因为两层的webView的sessionStorage是不相通的,那么新开的那个webview就会是空的.
4.2,ios和安卓的返回现象不一致问题
对于浏览器的返回,这个就更离谱了,尤其是混合开发时,在各种app各种机型的手机各种版本的浏览器内核,很混乱,具体的还是要具体拿到机器和app进行测试联调得出结论.
这里我只写比较共性的,从外部项目(a项目跳转b项目然后返回)的情况.
4.2,1,安卓手机的返回特性
安卓手机是自带系统返回的,具体表现在网页浏览时,底部会有系统返回键.另外安卓手机左滑或者右滑是能够实现页面前进后退的.
对于安卓手机的返回,当我们在a项目使用window.locationhref来跳转b项目的页面,然后点击返回,这时候是重新加载渲染a项目.
这种情况下,容易遇到的问题就是类似于vuex存储的数据如果没有进行持久化处理,就会被重置为初始的状态.(和浏览器刷新一样的效果)
4.2.2,ios手机的返回特性
有一部分ios手机的返回是有缓存策略的,在IOS微信内置浏览器中返回上一页时,上一个页面不会被刷新。 而通常在浏览器缓存机制中,在返回上一页的操作中, html/css/js/接口 等动静态资源不会重新请求,但是js会重新加载。
但在IOS页面中js也会保存上一页面最后执行的状态,不会重新执行js。 使用这种模式的缓存机制可以加快渲染速度,但是部分数据需要经常展示和编辑的情况下会导致不同步。比如‘详情页’跳转到‘编辑页’,编辑完后再返回到‘详情页’,如果‘详情页’数据展示未进行同步修改那肯定是不能接受的。 在webview和5+的混合app模式中,也会遇到这种返回上一个页面不刷新的问题.
这是因为存在浏览器前进/后退缓存(Backward/Forward Cache, BF Cache),当然也有人叫 disk Cache。 BF Cache 是一种浏览器优化, HTML 标准并未指定其如何进行缓存,因此缓存行为是各浏览器各自实现,所以不尽相同。
这时候可以使用另一个api来解决:pageShow.
window.addEventListener('pageshow', () => {
if (e.persisted || (window.performance &&
window.performance.navigation.type == 2)) {
location.reload()
}
}, false)
其中的参数:
1,为了查看页面是直接从服务器上载入还是从缓存中读取,可以使用 PageTransitionEvent 对象的 persisted 属性来判断。如果页面从浏览器的缓存中读取该属性返回 ture,否则返回 false。
2,window.performance对象,performance.navigation.type是一个无符号短整型
TYPE_NAVIGATE (0):
当前页面是通过点击链接,书签和表单提交,或者脚本操作,或者在url中直接输入地址,type值为0
TYPE_RELOAD (1)
点击刷新页面按钮或者通过Location.reload()方法显示的页面,type值为1
TYPE_BACK_FORWARD (2)
页面通过历史记录和前进后退访问时。type值为2
TYPE_RESERVED (255)
任何其他方式,type值为255
五,总结
日常我们开发中会遇到的浏览器返回问题基本上就这些.
熟练理解浏览器的前进与后退,首先就需要区分好:当前展示页面(指针指向的历史记录) 和历史记录栈中历史记录条数的关系.
明确好哪些方式是移动指针:
浏览器的前进后退
history.go/back/forward
哪些方式会新增浏览器记录条目:
pushState:在当前记录后新增一个记录,并且指针移向新记录,且清空后续记录.
window.location.href:在当前记录后新增一个记录,并且指针移向新记录,且清空后续记录.强制触发刷新.
window.location.hash:与原值不同时才增加新的历史记录,不触发刷新.
window.location.assign:在当前记录后新增一个记录,并且指针移向新记录,且清空后续记录.强制触发刷新.
哪些方式会覆盖当前历史记录:
window.location.replace(url)
哪些方式会触发popState事件:
只会在指针在历史记录栈中已有的位置上移动时会触发
也就是移动指针的几个操作:浏览器的前进后退,history.go/back/forward
哪些方式会触发hashState事件:
非pushState和popState触发的hash变化.
哪些方式会触发visibilitychange事件:
当前页面不可见/可见之间切换,就会触发.
例如:两个浏览器间窗口切换、混合开发中最小化窗口和打开、混合开发中切换app、混合开发中多层webView的打开与切换.
哪些方式会触发pageShow:
同一个窗口中,跳转外部项目和返回
明白了这些概念,就基本能拿捏浏览器返回啦.
六,参考文章
[极致用户体验] 网页里的「返回」应该用 history.back 还是 push ? – 掘金 (juejin.cn)
History API与浏览器历史堆栈管理 – royalrover – 博客园 (cnblogs.com)
[高级]深入浅出浏览器的history对象 – 掘金 (juejin.cn)
点击浏览器后退按钮 chrome 不会触发popstate事件分析- 掘金 (juejin.cn)
浏览器的History、Location对象,及使用js控制网页的前进后退和加载,刷新当前页面总结! – 掘金 (juejin.cn)