浏览器返回问题汇总

前言

每次遇到浏览器的返回问题,就很麻烦。因为安卓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)

这三个并不会增减历史记录栈,而是在历史栈中进行跳转,将指针指向对应的节点

例如我打开一个浏览器窗口,这时候历史栈中会有一个节点:

1-1.png
然后我在控制台通过:“window.location.href=”www.baidu.com“”跳转到百度页面,历史栈的情况就会变成:

1-2.png

继续在控制台通过:“window.location.href=”www.zhihu.com“”跳转到知乎页面,历史栈的情况就会变成:

1-3.png

接下来,历史记录栈的指针变化和我对应的操作记录如下:

1-4.png

由此可以很明显地看到,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');

得到的结果如下:

1-5.png

可以看到当我们执行了这个代码后:

  • url发生了变更,但页面并没有跳转与刷新.

  • 历史记录栈增加了一条,并且指针指向了最新的这一条

  • history.state的值也发生了变更

    但是现在我还不知道,pushState的新增记录是在顶部新增还是在当前指针上方新增.

    于是可以做如下的实验:

    首先我浏览器执行两次返回后回到百度页面,这时候我查看history的内容,发现state是null,这说明设置的state只会存储在对应的历史记录上,而不是所有的历史记录共享.

    然后我执行:

    // 存储页面状态
    
    const state = { test: 'testStr' };
    window.history.pushState(state, '', '/test');
    

    后查看window.history,得到效果如下:

1-6.png

可以看到,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);

1-7.png

于是可以知道,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)
 });

然后点击浏览器的返回按键,得到的效果如下:

1-8.png

可以看到,点击执行两次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();
    }
});

1-9.png

就是利用历史记录栈中指针发生了移动(点击浏览器返回),从而触发popState事件,当发现是返回到第一层,就强制它回到最新页面.

这样就实现了浏览器的返回禁用.但是很明显的,当我们手动执行window.history.go(-2)就能绕过这一禁用,直接到达最开始的知乎页面.

温馨提示:
有些版本的浏览器,想要在chrome中或是基于 chrome 的 webview 中实现后退按钮拦截效果,至少需要用户主动进行一次交互操作。才能在历史记录中指针移动时触发popState事件

二,Location 对象知识点

Location 对象提供了 URL 相关的信息和操作方法,通过 document.locationwindow.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值.

1-10.png

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先执行.

1-11.png

2.6,Location方法

Location 对象提供以下方法:

2.6.1,window.location.assign(url)

接受一个 URL 字符串作为参数,使得浏览器立刻跳转到新的 URL。

它和location.href一样,会添加一个新的历史记录并清空后面的所有历史记录.(如下图,知乎页面的那条历史记录被清空了.)

1-12.png

2.6.2,window.location.replace(url)

window.location.assign(url) 实现一样的功能,区别在于 replace 方法执行后跳转的 URL 会 覆盖 浏览历史中的当前记录,因此原先的当前记录就在浏览历史中 删除 了。并且不会清空后续的历史记录.(如下图,最上层的知乎页面历史记录没有被清空,而百度页面则被替换掉了).

1-13.png

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页面返回时,直接回到页面.

对应的历史记录栈如下:

1-14.png

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,在浏览器输入生成的地址,即可在浏览器中看到效果.

1-15.png

值得注意的是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.

1-16.png

这种情况有三个坑需要注意.

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)

浏览器禁止页面回退 – 掘金 (juejin.cn)

浏览器的History、Location对象,及使用js控制网页的前进后退和加载,刷新当前页面总结! – 掘金 (juejin.cn)

手写hash和history路由 – 掘金 (juejin.cn)

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYiYaHSb' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片