html5 history微信浏览器返回不刷新

History API与浏览器历史堆栈管理:

移动端开发在某些场景中有着特殊需求,如为了提高用户体验和加快响应速度,常常在部分工程采用SPA架构。传统的单页应用基于url的hash值进行路由,这种实现不存在兼容性问题,但是缺点也有–针对不支持onhashchange属性的IE6-7需要设置定时器不断检查hash值改变,性能上并不是很友好。

而如今,在移动端开发中HTML5规范给我们提供了一个History接口,使用该接口可以自由操纵历史记录。本文并不详细介绍History接口,而是探究History接口如何影响浏览器历史堆栈,并且利用这个规律应用到具体的实际业务中,提出两种历史记录保存策略,使路由逻辑更清晰,让SPA更容易。

History API回顾

HTML5 History API包括2个方法:history.pushState()history.replaceState(),和1个事件:window.onpopstate

pushState

history.pushState(stateObject, title, url),包括三个参数。

第一个参数用于存储该url对应的状态对象,该对象可在onpopstate事件中获取,也可在history对象中获取。

第二个参数是标题,目前浏览器并未实现。

第三个参数则是设定的url。一般设置为相对路径,如果设置为绝对路径时需要保证同源。

pushState函数向浏览器的历史堆栈压入一个url为设定值的记录,并改变历史堆栈的当前指针至栈顶。

在这里笔者使用历史堆栈和当前指针,用以说明浏览器对历史记录的管理策略。文档中并没有使用这样的词汇,笔者为了更形象的介绍接口对浏览器历史记录的影响,使用这样的描述,如有不当之处请及时指出(不过目前以这套模型为基础的逻辑实现中并未出现悖论)。

replaceState

该接口与pushState参数相同,含义也相同。唯一的区别在于replaceState是替换浏览器历史堆栈的当前历史记录为设定的url。需要注意的是,replaceState不会改动浏览器历史堆栈的当前指针。

onpopstate

该事件是window的属性。该事件会在调用浏览器的前进、后退以及执行history.forward、history.back、和history.go触发,因为这些操作有一个共性,即修改了历史堆栈的当前指针。在不改变document的前提下,一旦当前指针改变则会触发onpopstate事件。

History API与业务实践

最常见的单页应用场景:列表页、商品详情页以及其内部的其他链接入口如图片页、评论页及其推荐其他商品详情页。以上提到的已经涉及到了4个单独业务逻辑页面(推荐的商品可复用商品详情页逻辑),分别是:列表、详情、图片详情和评论。将这4个页面合并到一个页面中,这就是最简单的SPA。为了用户的良好体验,必须设计合理的交互逻辑,最直观的就是浏览器(或手机app、微信公众号)的后退和前进必须合乎业务逻辑特点。因此,这就涉及到了History API的使用,也牵扯到浏览器的历史记录管理。

这里写图片描述

上图为具体的逻辑示意图。在列表页,点击其中一个商品,这里是商品1,进入详情页。详情页包括了该商品的轮播图、商品的图片详情入口、评论入口和推荐的其他商品入口。接下来进行如下操作:进入图片详情页,后退至详情页再进入评论页;后退至商品1详情页再由推荐商品入口进入商品9详情页,同样在商品9详情页进入图片详情页和评论页,再后退至商品9详情页;由推荐商品入口进入商品34详情页,再进行类似操作。最后保证在商品34图片详情页或评论页可以顺利后退至最初的商品列表页。

上文中加粗的“后退”,意味着使用浏览器后退按钮,或者使用手机自带的返回,再或者使用页面上提供的后退按钮。

这样一个很细小的需求,但是一旦真正放手去做却不是那么容易。仅仅根据History API的2个函数和1个事件去盲目的尝试实现,这属于盲人摸象,鲁棒性不高。不清楚浏览器的历史记录管理策略,不了解当前页面的历史记录数量,此种情况若要实现上述场景就有些麻烦。所以在具体动手写业务代码之前,需要搞懂History的pushState和replaceState具体如何影响历史记录栈。

探究浏览器历史记录策略与History API的关系

由于浏览器并未针对每个页面的历史记录提供具体访问的接口,因此所有的测试都是黑盒。但是在移动端的中,大都是webkit内核,其webcore的具体实现也都相近,因此该节得出的结论完全可以在移动端使用。

尽管无法访问当前页的历史记录栈,但是浏览器却提供了history.length属性,它标明了当前历史记录栈的个数。该值会帮助我们更好地分析History API对历史记录栈的影响。
这里写图片描述

上图为测试实例。其中白色箭头意味着点击该链接并执行pushState操作(即操作1),黑色箭头则执行浏览器后退,红色的圆点为历史记录栈中的当前指针,而每个项则为历史记录栈,历史记录的个数则为其子项的数量。

初始在第一个搜索列表页,执行操作1后历史堆栈数量增加,当前指针上移一位至26788.html;
同理在执行3次操作1,历史堆栈递增3个,当前指针仍在栈顶,即78099.html;
此后进行浏览器后退,历史堆栈数量不变,当前指针下移一位至8819.html;
在此处再执行操作1,栈顶元素改变,当前指针移至栈顶,历史堆栈数量不变;
继续执行操作1,栈顶元素改变,指针移至栈顶,历史堆栈数量加一;
执行浏览器后退,栈顶元素不变,指针下移一位至8128.html,历史堆栈数量不变;
执行浏览器后退,栈顶元素不变,指针下移一位至8819.html,历史堆栈数量不变;
执行浏览器后退,栈顶元素不变,指针下移一位至8128.html,历史堆栈数量不变;
执行浏览器后退,栈顶元素不变,指针下移一位至26788.html,历史堆栈数量不变;
执行操作1,栈顶元素变为9721.html,指针上移至栈顶,历史堆栈数量变为3;
执行操作1,栈顶元素变为8387.html,指针上移至栈顶,历史堆栈数量变为4;
执行浏览器后退,栈顶元素不变,指针下移一位至9721.html,历史堆栈数量不变;
执行浏览器后退,栈顶元素不变,指针下移一位至26788.html,历史堆栈数量不变;
执行浏览器后退,栈顶元素不变,指针下移一位至search.html,历史堆栈数量不变;
执行操作1,栈顶元素变为xxx.html,指针上移至栈顶,历史堆栈数量变为2;

至此,实验结束。虽然这里仅仅列出了这一个测试用例,但是其实笔者做了更多更复杂的测试,并且平台涉及了pc和移动端的浏览器、微信和原生webview,结果都一样。这一系列测试说明了很多问题,总结之一句话则是:

浏览器针对每个页面维护一个History栈。执行pushState函数可压入设定的url至栈顶,同时修改当前指针;

当执行back操作时,history栈大小并不会改变(history.length不变),仅仅移动当前指针的位置;

若当前指针在history栈的中间位置(非栈顶),此时执行pushState会改变history栈的大小。

总结pushState的规律,可发现当前指针在history栈顶部时执行pushState,会增加history栈大小;

若current指针不在栈顶则会在当前指针所在位置添加项。执行back操作并不修改history栈大小,因此可以通过back和forward在当前大小的history栈中自由移动。

掌握这个规律,就知道如何维护历史记录,就知道在什么状态下需要pushState。回到最初的需求,产品经理规定从商品34的评论页,按后退按钮可以到达最初的列表页,但是他并没有详细规定如何后退。在这里就会有2中实现方式:

每一次后退,会回到上次的访问地方。如,在商品34的评论页,会后退至商品34的详情页,再后退则会回到商品9的详情页,直至回到列表页。

总共维护三层历史记录,第一层(栈底)为列表页,第二层为详情页,第三层(栈顶)为评论页或图片详情页。在该种实现下,由商品34的评论页第一次后退至商品34的详情页,第二次后退至列表页。

针对第一种,其实实现最为简单,因为这完全是由浏览器默认控制历史记录堆栈,而我们只需在合适的时机调用pushState将url插入到堆栈,然后在onpopstate处理函数中监听对应的时间即可:

window.addEventListener('popstate', function (e) {

    console.log('popstate')
    // 后退(前进)至商品详情页,异步加载数据并渲染
    if(e.state && e.state.indexOf('/shop/sku/') !== -1){
      ajaxDetail(e.state,true);   
    }else
    // 后退(前进)至评论页,异步加载数据渲染
    if(e.state && e.state.indexOf('/shop/comment/commentList.html') !== -1){
      ajaxComment(e.state,true);
    }else 
    // 后退(前进)至图片详情页,异步加载数据渲染
    if(e.state && e.state.indexOf('/shop/item/pictext/') !== -1){
      ajaxPic(e.state,true);
    }else
    // 后退(前进)至列表页,隐藏浮层
    if(e.state && e.state.indexOf('/search/') !== -1){
      // 隐藏spa的浮层
      $('.spa-container').css('zIndex','-1');
    }

  });

针对第二种实现,则是本文的重点。毕竟,由浏览器默认维护的历史堆栈在某些业务场景中并不匹配,因此需要开发者自己维护一个历史记录栈。在本次实现中,由于总共涉及4张页面的显示,因此我们设定了3层历史堆栈,这很好理解。

为了构建这样的历史记录栈,在主页面(即列表页)中需要额外添加两条历史记录。这是由于默认打开列表页时,当前页面的url已加入历史记录栈中,

function push(state){
    history.pushState(state, null, location.pathname + location.search);
  }
  // 'abc'用于标示初始列表页
  history.replaceState('abc',null,location.pathname + location.search)

  // 压入两条历史记录
  push();
  push();

这样,打开列表页后就会创建3个历史记录,并且这3个历史记录的url都为列表页的url,这与后面的操作并无影响。

在列表页中打开详情页,需要做额外的处理。由于按照我们设计的历史记录栈,第二层应该为详情页,而此时在初始化后,历史记录栈的当前指针已指向栈顶元素,因此需要将当前指针下移一位。这里就需要history.back来完成。

$('.item-list').on('click','a',handler);

// 异步加载详情数据
var handler = function(e,isScrollXClick){
    var a = this;
    ajaxDetail($(a).attr('href'),isScrollXClick);
    return false;
};

var isScrollXClick;
  /**
   * @params: url 请求路径 isScrollXClick: 是否点击推荐商品
   *
   */ 
  var ajaxDetail = function(url,isScrollXClick){

     $.ajax({
      url: '/api' + url,
      success: function(data){
        ...
        ...
        if(!isScrollXClick){
          console.log('I am back!')

          // 在代码中进行back or forward并不会立即出发popstate事件,以v8引擎为例,在执行back之后
          // 的大概18us之后会触发事件,而此时如果立即通过replaceState修改url则会造成失败,修改的是
          // history stack栈顶的url.

          // 这里通过异步执行replaceState兼容
          history.back();       

        }

        // 异步触发
        setTimeout(function(){
          history.replaceState(url, null, url);
        })

        // 针对推荐栏的商品,循环绑定事件,此处用事件代理优化
        $('#J_PDSlider').on('click','a',function(e){
          isScrollXClick = 1;
          handler.call(this,e,isScrollXClick);
          return false;
        });
      },
      error: function(xhr, type){
        alert('Ajax error!')
      }
     })
  };

在此处实现,通过isScrollXClick变量判断是否点击的是推荐商品,如果不是则需要执行back操作,下移指针。此时指针是指在第二层,但是浏览器和第二层历史记录的url仍为初始化设定的url,因此需要修改,在这里异步修改当前url。

之所以异步执行replaceState,是由于webkit触发popState事件决定的。在代码中执行history.back 或者history.forward,并不会立即返回,也不会立即触发popState事件。由于没有阅读webkit的源码,因此无从推测执行back或者forward后具体需要额外做什么操作,它们之间有着10us级别的间隔,因此此处必须使用setTimeout实现异步改变url。

在具体开发过程中,这个问题困扰着笔者好几天,终于在一次调试过程中发现浏览器url的变动,才联想到可能是由事件触发的时间差导致。

对于图片详情和评论的逻辑处理,则和上文类似,无需多言。

最后一次后退需要回到列表页,而在初始化阶段我们给列表页设置了state为“abc”,特殊的标示该路由,因此在popState事件处理中,我们就可以根据该项回到初始页:

window.addEventListener('popstate', function (e) {

    if(e.state && e.state.indexOf('/shop/sku/') !== -1){
      ajaxDetail(e.state,true);   
    }else if(e.state && e.state.indexOf('abc') !== -1){
      // 隐藏spa的浮层
      $('.spa-container').css('zIndex','-1');


      push();
      push();
    }


  });

如果回到初始页,隐藏浮层,同时在执行2次push操作。根据上节发现的规律,在初始页执行2次push操作,会在当前指针位置重新添加2个历史记录,当前指针指向栈顶元素,历史记录栈的数量不变,仍为3。这样就完成了简单的由开发者自定义维护历史堆栈的spa系统。


history对象

概述

浏览器窗口有一个history对象,用来保存浏览历史。

如果当前窗口先后访问了三个网址,那么history对象就包括三项,history.length属性等于3。

history.length // 3

history对象提供了一系列方法,允许在浏览历史之间移动。

back():移动到上一个访问页面,等同于浏览器的后退键。
forward():移动到下一个访问页面,等同于浏览器的前进键。
go():接受一个整数作为参数,移动到该整数指定的页面,比如go(1)相当于forward(),go(-1)相当于back()。
history.back();
history.forward();
history.go(-2);

如果移动的位置超出了访问历史的边界,以上三个方法并不报错,而是默默的失败。

history.go(0)相当于刷新当前页面。

history.go(0);

常见的“返回上一页”链接,代码如下

document.getElementById('backLink').onclick = function () {
  window.history.back();
}

注意,返回上一页时,页面通常是从浏览器缓存之中加载,而不是重新要求服务器发送新的网页。

history.pushState()

HTML5为history对象添加了两个新方法,history.pushState()history.replaceState(),用来在浏览历史中添加和修改记录。

if (!!(window.history && history.pushState)){
  // 支持History API
} else {
  // 不支持
}

上面代码可以用来检查,当前浏览器是否支持History API。如果不支持的话,可以考虑使用Polyfill库History.js。
history.pushState方法接受三个参数,依次为:

state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。
title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。
url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。

假定当前网址是example.com/1.html,我们使用pushState方法在浏览记录(history对象)中添加一个新记录。

var stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', '2.html');

添加上面这个新记录后,浏览器地址栏立刻显示example.com/2.html,但并不会跳转到2.html,甚至也不会检查2.html是否存在,它只是成为浏览历史中的最新记录。假定这时你访问了google.com,然后点击了倒退按钮,页面的url将显示2.html,但是内容还是原来的1.html。你再点击一次倒退按钮,url将显示1.html,内容不变。

总之,pushState方法不会触发页面刷新,只是导致history对象发生变化,地址栏会有反应。

如果pushState的url参数,设置了一个新的锚点值(即hash),并不会触发hashchange事件。如果设置了一个跨域网址,则会报错。

// 报错
history.pushState(null, null, 'https://twitter.com/hello');

上面代码中,pushState想要插入一个跨域的网址,导致报错。这样设计的目的是,防止恶意代码让用户以为他们是在另一个网站上。

history.replaceState()

history.replaceState方法的参数与pushState方法一模一样,区别是它修改浏览历史中当前纪录。

假定当前网页是example.com/example.html。

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()
// url显示为http://example.com/example.html?page=1

history.back()
// url显示为http://example.com/example.html

history.go(2)
// url显示为http://example.com/example.html?page=3

history.state属性

history.state属性返回当前页面的state对象。

history.pushState({page: 1}, 'title 1', '?page=1');

history.state
// { page: 1 }

popstate事件

每当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件。

需要注意的是,仅仅调用pushState方法或replaceState方法 ,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用JavaScript调用back、forward、go方法时才会触发。另外,该事件只针对同一个文档,如果浏览历史的切换,导致加载不同的文档,该事件也不会触发。

使用的时候,可以为popstate事件指定回调函数。这个回调函数的参数是一个event事件对象,它的state属性指向pushState和replaceState方法为当前URL所提供的状态对象(即这两个方法的第一个参数)。

window.onpopstate = function (event) {
  console.log('location: ' + document.location);
  console.log('state: ' + JSON.stringify(event.state));
};

// 或者

window.addEventListener('popstate', function(event) {
  console.log('location: ' + document.location);
  console.log('state: ' + JSON.stringify(event.state));
});

上面代码中的event.state,就是通过pushState和replaceState方法,为当前URL绑定的state对象。

这个state对象也可以直接通过history对象读取。

var currentState = history.state;

注意,页面第一次加载的时候,在load事件发生后,Chrome和Safari浏览器(Webkit核心)会触发popstate事件,而Firefox和IE浏览器不会。

URLSearchParams API

URLSearchParams API用于处理URL之中的查询字符串,即问号之后的部分。没有部署这个API的浏览器,可以用url-search-params这个垫片库。

var paramsString = 'q=URLUtils.searchParams&topic=api';
var searchParams = new URLSearchParams(paramsString);

URLSearchParams有以下方法,用来操作某个参数。


    has():返回一个布尔值,表示是否具有某个参数
    get():返回指定参数的第一个值
    getAll():返回一个数组,成员是指定参数的所有值
    set():设置指定参数
    delete():删除指定参数
    append():在查询字符串之中,追加一个键值对
    toString():返回整个查询字符串
var paramsString = 'q=URLUtils.searchParams&topic=api';
var searchParams = new URLSearchParams(paramsString);

searchParams.has('topic') // true
searchParams.get('topic') // "api"
searchParams.getAll('topic') // ["api"]

searchParams.get('foo') // null,注意Firefox返回空字符串
searchParams.set('foo', 2);
searchParams.get('foo') // 2

searchParams.append('topic', 'webdev');
searchParams.toString() // "q=URLUtils.searchParams&topic=api&foo=2&topic=webdev"

searchParams.append('foo', 3);
searchParams.getAll('foo') // [2, 3]

searchParams.delete('topic');
searchParams.toString() // "q=URLUtils.searchParams&foo=2&foo=3"

URLSearchParams还有三个方法,用来遍历所有参数。

keys():遍历所有参数名
values():遍历所有参数值
entries():遍历所有参数的键值对

上面三个方法返回的都是Iterator对象。

var searchParams = new URLSearchParams('key1=value1&key2=value2');

for (var key of searchParams.keys()) {
  console.log(key);
}
// key1
// key2

for (var value of searchParams.values()) {
  console.log(value);
}
// value1
// value2

for (var pair of searchParams.entries()) {
  console.log(pair[0]+ ', '+ pair[1]);
}
// key1, value1
// key2, value2

在Chrome浏览器之中,URLSearchParams实例本身就是Iterator对象,与entries方法返回值相同。所以,可以写成下面的样子。

for (var p of searchParams) {
  console.log(p);
}

下面是一个替换当前URL的例子。

// URL: https://example.com?version=1.0
var params = new URLSearchParams(location.search.slice(1));
params.set('version', 2.0);

window.history.replaceState({}, '', `${location.pathname}?${params}`);
// URL: https://example.com?version=2.0

URLSearchParams实例可以当作POST数据发送,所有数据都会URL编码。

let params = new URLSearchParams();
params.append('api_key', '1234567890');

fetch('https://example.com/api', {
  method: 'POST',
  body: params
}).then(...)

DOM的a元素节点的searchParams属性,就是一个URLSearchParams实例。

var a = document.createElement('a');
a.href = 'https://example.com?filter=api';
a.searchParams.get('filter') // "api"

URLSearchParams还可以与URL接口结合使用。

var url = new URL(location);
var foo = url.searchParams.get('foo') || 'somedefault';

微信浏览器各个ios版本后退不刷新

各个版本的ios微信浏览器返回或者history.go(-1),都是读取的浏览器的缓存,通过hack技术,每次修改施工进度工作台列表页的url,用

history.replaceState({}, "todo", location.pathname+'?time='+(+new Date()));

实现当前url的修改,这样在排期后页面go(-1)时,读取的是不同的url,从而实现工作台列表从后台重新请求数据!!

<think>我们正在处理一个Vue项目中拦截微信浏览器左上角返回按钮的需求。根据引用资料,我们可以知道一些关键点:1.微信浏览器左上角返回按钮的默认行为是返回上一页,但有时我们需要先执行一些逻辑(比如关闭蒙层)而是直接返回。2.我们可以通过监听`popstate`事件或`pageshow`事件来实现拦截逻辑。3.在Vue中,我们可以在组件的mounted生命周期钩子中添加事件监听,并在beforeDestroy钩子中移除,以避免内存泄漏。根据引用[3]和[4]中的示例,我们可以在第二个页面(需要拦截返回的页面)中这样做:-在mounted中,我们添加一个`popstate`事件监听器,当事件触发时,我们执行自定义逻辑(例如隐藏蒙层),并且通过`history.pushState`方法向当前历史记录中添加一个状态,这样用户需要再次点击返回才会真正返回上一页。-注意:在iOS微信浏览器中,history的前进后退会重新加载页面,所以我们需要监听页面展示事件来执行逻辑。实现步骤:1.在进入需要拦截返回的页面时,先向history中添加一个空状态(使用`history.pushState`),这样当前页面就有两个状态:初始状态和我们添加的状态。当用户点击返回按钮时,首先会回到我们添加的状态(此时页面刷新),这时我们触发自定义逻辑(如关闭蒙层),然后移除我们添加的状态,这样再次点击返回就会返回上一页。2.我们监听`popstate`事件,在事件处理函数中判断是否应该执行拦截逻辑。具体代码实现思路(以Vue组件为例):```javascript//在需要拦截返回的页面组件中exportdefault{data(){return{isShowMask:true//假设有一个蒙层需要显示}},mounted(){//添加一个历史记录,这样当前页面就有两个状态:初始状态和pushState添加的状态history.pushState(null,null,document.URL);//监听popstate事件window.addEventListener('popstate',this.handlePopstate);},beforeDestroy(){//组件销毁前移除事件监听window.removeEventListener('popstate',this.handlePopstate);},methods:{handlePopstate(event){//在这里进行拦截处理if(this.isShowMask){//如果蒙层是显示的,则隐藏蒙层,并阻止默认的返回行为(通过再次pushState)this.isShowMask=false;//再次pushState,这样用户再次点击返回时才会返回上一页history.pushState(null,null,document.URL);//注意:这里我们并没有调用event.preventDefault(),因为popstate事件无法通过preventDefault阻止返回//我们的策略是:第一次返回时隐藏蒙层并添加一个新的历史记录,这样用户需要再点一次返回才会真正返回}//如果蒙层已经隐藏,那么做额外处理,让浏览器执行默认的返回行为(返回上一页)}}}```但是,上述代码有一个问题:在安卓微信中,可能一次返回按钮点击就会触发两次`popstate`事件。因此,我们需要更健壮的方案。改进方案(参考引用[4]):我们可以通过一个标记来控制。另外,引用[3]和[4]的示例中,在隐藏蒙层后再次pushState,这样当用户再次点击返回时,由于没有蒙层,就会返回上一页。注意:在Vue项目中,我们也可以使用VueRouter的导航守卫,但是微信浏览器左上角的返回按钮并触发VueRouter的导航守卫,所以我们只能使用原生的popstate事件。另外,引用[2]中提到,iOS中可以使用`pageshow`事件,但这里我们主要使用`popstate`事件。另一种实现方式(更常见):1.在进入页面时,先pushState一个状态。2.当用户点击返回时,触发popstate事件,此时我们隐藏蒙层,并且再次pushState一个状态(这样当前页面仍然停留在当前页),同时设置一个标记表示蒙层已经关闭。3.如果用户再次点击返回,由于蒙层已经关闭,我们就让页面返回(此时我们可以使用history.back()或者什么都做,因为popstate事件触发后,如果我们没有再次pushState,就会返回上一页)。但是,注意我们第二次pushState后,历史记录中又多了一条,所以我们需要在返回时移除我们添加的状态?或者我们可以使用history.go(-1)直接返回?实际上,我们可以在第一次popstate事件中,在隐藏蒙层并pushState之后,再添加一个popstate事件监听(或使用同一个),当再次触发popstate时,我们执行history.go(-1)返回上一页。但这样可能造成循环。因此,更安全的做法是:在第一次触发popstate时,我们隐藏蒙层,然后pushState,这样历史记录中有三个状态:初始状态、我们第一次pushState的状态、第二次pushState的状态。然后,当用户第二次点击返回时,会触发popstate事件,此时我们判断蒙层已经关闭,那么我们就移除事件监听,并执行history.back()(或者history.go(-1))返回上一页。但这样需要精确控制历史记录。实际上,我们可以简化:在第一次返回时,我们隐藏蒙层,并pushState,这样用户再点一次返回,就会回到我们第一次pushState的状态,然后再次触发popstate事件,这时我们做拦截,直接返回上一页?但是这样我们会在历史记录中留下两个无用的状态。所以,我们采用以下策略:-进入页面时:history.pushState(state1)-第一次返回(点击返回按钮):-触发popstate事件,我们隐藏蒙层,然后history.pushState(state2)->这样历史记录中有三个状态:初始、state1、state2-此时用户再次点击返回,会触发popstate事件(对应state2被弹出,当前状态变为state1),我们在这个事件中再次pushState(state3)?这样会陷入循环。为了避免循环,我们只在蒙层显示的时候拦截,蒙层隐藏后就再拦截。所以,在第一次popstate事件中,我们隐藏蒙层并pushState,然后当用户再次点击返回时,由于蒙层已经隐藏,我们拦截,让浏览器返回上一页(即state1被弹出,当前状态为初始状态),然后再点一次返回,就会返回上一页?这样用户需要点三次返回?显然行。因此,我们调整思路:我们只拦截一次,在第一次返回时隐藏蒙层,然后我们手动后退到上一页?行,这样会直接离开页面。所以,我们采用以下方法:在第一次popstate事件中,我们执行:1.隐藏蒙层(或其他操作)2.阻止浏览器默认的返回行为(实际上popstate事件中无法阻止,所以我们需要改变历史记录)3.我们使用setTimeout,在下一个事件循环中,使用history.go(-1)来抵消我们之前添加的历史记录?这样历史记录就恢复了,然后用户再次点击返回就会直接返回上一页。但是,history.go(-1)会触发popstate事件,所以我们需要在调用history.go(-1)之前先移除事件监听,执行完后再重新添加?这样会有些复杂。参考常见做法:```javascriptmethods:{handlePopstate(event){if(this.isShowMask){this.isShowMask=false;//再次pushState,这样当前历史记录中多了一条,用户需要再点一次返回才会触发返回history.pushState(null,null,document.URL);}else{//如果蒙层已经关闭,我们移除事件监听,并且什么都做,让浏览器返回上一页//但是注意,我们之前pushState了两次,所以需要回退两次?对,我们第一次进入页面时pushState一次,然后第一次返回时又pushState一次,所以当前历史记录有三条:初始、第一次pushState、第二次pushState。//当用户第二次点击返回时,我们期望返回上一页,但实际上只是回退到第一次pushState的状态(仍然是当前页),然后第三次点击返回才会返回上一页。//所以,我们需要在蒙层关闭后,将历史记录恢复到进入页面时的状态?我们可以使用history.go(-2)直接回退两页?这样就直接返回上一页了,但用户会看到页面直接返回了,没有过渡。//所以,我们在蒙层关闭后,移除事件监听,然后手动回退一次(因为我们添加了两个状态,需要回退两次?)//但这样会直接返回上一页,用户可能没有感知。//更好的做法是:在第一次拦截时,我们pushState,然后当蒙层关闭后,我们等待用户再次点击返回,这时我们再拦截,但是因为历史记录中还有两个状态(初始和第一次pushState),所以用户需要再点两次返回?这样体验好。//因此,我们可以在蒙层关闭后,立即用history.back()或history.go(-1)来移除我们第一次pushState的状态,这样历史记录就只剩下初始状态,用户再点一次返回就会返回上一页。//但是,在popstate事件中执行history.back()会再次触发popstate事件,所以我们需要在执行前移除事件监听。//移除事件监听window.removeEventListener('popstate',this.handlePopstate);//回退一次,移除我们第一次拦截时pushState的状态history.go(-1);}}}```但是,这个处理在第一次触发popstate时(蒙层显示)我们pushState,然后当蒙层关闭后,用户再次点击返回,进入else分支,我们移除监听并执行history.go(-1)(这又会触发popstate事件,但此时我们已经移除了监听,所以会再次进入handlePopstate),然后页面会回退一次(移除我们第一次pushState的状态),然后用户再点一次返回,就会返回上一页。这样用户需要点击三次返回按钮(第一次隐藏蒙层,第二次无反应(实际上执行了history.go(-1)),第三次返回上一页)。但用户第二次点击返回时,由于我们执行了history.go(-1),页面状态会回到初始状态(即我们刚进入页面时的状态),然后用户第三次点击返回,才离开页面。这样体验好,用户会感到困惑。因此,我们采用另一种思路:在第一次触发popstate事件时,我们隐藏蒙层,然后立即使用history.back()来模拟返回,但是history.back()会触发popstate事件,所以我们需要在隐藏蒙层后,在事件处理函数中移除事件监听,然后执行history.back(),这样就会返回上一页?对,这样会直接返回上一页,没有达到拦截的目的。所以,最终我们采用最常用的方法:在蒙层显示时,拦截返回并隐藏蒙层,改变历史记录的次数,而是通过再次pushState让用户需要再点一次返回才能返回。这样用户需要点击两次返回按钮:第一次隐藏蒙层,第二次返回上一页。虽然多了一次点击,但逻辑清晰。代码实现:```javascriptexportdefault{data(){return{isShowMask:true}},mounted(){//记录当前历史记录的状态history.pushState(null,null,document.URL);window.addEventListener('popstate',this.handlePopstate);},beforeDestroy(){window.removeEventListener('popstate',this.handlePopstate);},methods:{handlePopstate(event){if(this.isShowMask){//隐藏蒙层this.isShowMask=false;//再次pushState,这样用户再点一次返回才会返回上一页history.pushState(null,null,document.URL);}else{//如果蒙层已经隐藏,那么我们就让页面返回//但这里我们什么都做,因为浏览器会执行默认的返回行为(返回上一页)//但是,由于我们之前pushState了两次,所以需要移除两次?实际上,我们第一次进入页面时pushState一次,第一次返回时又pushState一次,所以当前历史记录有三条。//当用户第二次点击返回时,会触发popstate,此时我们什么都做,那么浏览器返回上一个历史记录(即我们第一次pushState的状态),然后用户需要再点一次返回才会返回上一页(总共三次)。//所以我们需要在蒙层隐藏后,手动移除一个历史记录?//注意:我们无法在popstate事件中再次pushState,否则会无限循环。//因此,我们在else中做任何操作,而是让浏览器按历史记录返回。这样用户需要点击三次返回按钮(第一次隐藏蒙层,第二次返回进入当前页面的初始状态,第三次返回上一页)。但这样体验好。//解决方案:在蒙层隐藏后,我们移除事件监听,然后手动执行history.go(-2)直接返回上一页?但是这样会直接跳转,没有过渡。//权衡之下,我们接受用户需要点击两次返回按钮才能离开页面(第一次隐藏蒙层,第二次离开页面)。但是,按照上面的代码,用户需要点击三次,所以我们需要调整。//实际上,在第一次pushState后,我们有一个历史记录,然后我们进入页面时的历史记录是一个(比如A),然后pushState(变成A和B),当用户点击返回,触发popstate(从B回到A),然后我们再次pushState(变成A和C)。然后当用户再次点击返回,从C回到A,再次触发popstate事件,这时我们进入else,然后我们移除事件监听,并执行history.back(),这样就会从A回到上一页(A之前的状态)。这样用户点击两次返回:第一次从B->A(触发拦截,隐藏蒙层并pushState到C),第二次从C->A(触发popstate,进入else,执行history.back(),回到上一页)。但是,在第二次触发popstate时,我们执行history.back()又会触发popstate事件?所以我们需要在移除监听后执行。//所以,我们修改:在else分支中,我们移除事件监听,然后执行history.back(),这样就会返回上一页。但是注意,执行history.back()会触发popstate事件,但我们已经移除了监听,所以会处理。window.removeEventListener('popstate',this.handlePopstate);history.back();}}}}```这样,当用户第一次点击返回时,蒙层隐藏,并添加一个新的历史记录(当前页面URL)。然后用户第二次点击返回时,触发popstate事件,进入else分支,我们移除事件监听并执行history.back(),这样页面就会返回上一页。但是,在安卓微信中,可能一次返回按钮点击会触发两次popstate事件,所以我们需要在handlePopstate中加入防抖或标记。因此,我们可以添加一个标记,表示是否正在处理返回事件。改进后的代码:```javascriptexportdefault{data(){return{isShowMask:true,isHandling:false//防止多次触发}},mounted(){history.pushState(null,null,document.URL);window.addEventListener('popstate',this.handlePopstate);},beforeDestroy(){window.removeEventListener('popstate',this.handlePopstate);},methods:{handlePopstate(event){if(this.isHandling){return;}this.isHandling=true;if(this.isShowMask){this.isShowMask=false;history.pushState(null,null,document.URL);//处理完成后,重置标记this.isHandling=false;}else{window.removeEventListener('popstate',this.handlePopstate);history.back();}}}}```但是,在else分支中,我们移除了监听并执行history.back(),此时会再次触发handlePopstate(因为已经移除了),所以需要标记。而且,在history.back()之后,组件可能被销毁,所以会有问题。但是,在安卓中,一次返回按钮点击可能连续触发两次popstate事件,所以我们需要在第一次触发时设置标记,防止第二次触发时再次执行。所以,我们在handlePopstate开头检查isHandling,如果正在处理就跳过。另外,引用[4]中的代码只添加了一次pushState,并在popstate事件中隐藏蒙层,并没有处理后续的返回。所以用户需要再点一次返回才能离开,这就是两次点击。综上所述,我们采用以下方案(两次点击方案):1.进入页面时,pushState一个状态。2.在popstate事件中,如果蒙层显示,则隐藏蒙层,并再次pushState一个状态,然后设置标记防止重复处理(针对安卓的双次触发问题)。3.如果蒙层已经隐藏,则移除事件监听,并执行history.back()返回上一页。注意:在隐藏蒙层后再次pushState,是为了让用户再次点击返回按钮时能够触发popstate事件,从而进入else分支进行返回操作。但是,执行history.back()会立即返回上一页,用户可能没有机会再次点击返回按钮?对,我们的else分支是在用户第二次点击返回按钮时触发的,所以此时执行history.back()就会返回上一页。然而,在else分支中执行history.back(),会触发浏览器返回操作,页面会立即跳转,所以用户会感知到中间状态。因此,我们最终代码如下:```javascriptexportdefault{data(){return{isShowMask:true,isHandling:false}},mounted(){//添加历史记录history.pushState(null,null,document.URL);window.addEventListener('popstate',this.handlePopstate);},beforeDestroy(){window.removeEventListener('popstate',this.handlePopstate);},methods:{handlePopstate(event){//防止安卓微信中一次返回触发两次事件if(this.isHandling)return;this.isHandling=true;if(this.isShowMask){//隐藏蒙层this.isShowMask=false;//再次pushState,保证用户再次点击返回时能触发事件history.pushState(null,null,document.URL);//重置标记,允许下一次处理this.isHandling=false;}else{//蒙层已隐藏,移除监听并返回上一页window.removeEventListener('popstate',this.handlePopstate);history.back();}}}}```但是,在iOS中,可能需要防抖,但为了兼容安卓,我们加上。另外,注意:在VueRouter中使用history模式时,可能会和VueRouter的历史记录冲突,但微信浏览器中我们通常使用hash模式,所以影响大。最后,如果蒙层隐藏后,用户是点击返回按钮,而是点击页面内的返回按钮,我们也要能够返回上一页。所以,我们可以在页面内的返回按钮事件中,执行:```javascriptthis.$router.go(-1);```这样,我们既拦截了微信左上角的返回按钮,又提供了页面内的返回按钮。总结:以上代码实现了在Vue项目中拦截微信浏览器左上角返回按钮的功能,第一次点击返回时隐藏蒙层,第二次点击返回返回上一页。</think>在 Vue 项目中拦截微信浏览器左上角返回按钮的实现方法如下,我将分步骤说明关键实现逻辑: ### 核心实现步骤 1. **添加历史记录状态** 在页面加载时,使用 `history.pushState()` 添加一个空历史记录: ```javascript mounted() { // 添加历史记录,使当前页面有可返回的状态 history.pushState(null, null, document.URL); } ``` 2. **监听 popstate 事件** 在 Vue 组件的 `mounted` 钩子中添加事件监听: ```javascript mounted() { history.pushState(null, null, document.URL); window.addEventListener('popstate', this.handleBackButton); } ``` 3. **实现拦截逻辑** 在事件处理函数中控制返回行为: ```javascript methods: { handleBackButton(event) { if (this.isShowMask) { // 1. 如果蒙层显示,则隐藏蒙层 this.isShowMask = false; // 2. 重新添加历史记录,保持当前页面 history.pushState(null, null, document.URL); } else { // 3. 无蒙层时正常返回 window.removeEventListener('popstate', this.handleBackButton); history.back(); } } } ``` 4. **销毁事件监听** 在组件销毁前移除监听器: ```javascript beforeDestroy() { window.removeEventListener('popstate', this.handleBackButton); } ``` ### 完整 Vue 组件示例 ```vue <template> <div> <!-- 蒙层元素 --> <div v-if="isShowMask" class="mask"> <button @click="closeMask">关闭蒙层</button> </div> <!-- 页面内容 --> <div v-else> <h1>当前是二级页面</h1> <button @click="goBack">手动返回</button> </div> </div> </template> <script> export default { data() { return { isShowMask: true // 初始显示蒙层 } }, mounted() { // 关键步骤1:添加历史记录 history.pushState(null, null, document.URL); // 关键步骤2:监听返回事件 window.addEventListener('popstate', this.handleBackButton); }, beforeDestroy() { // 关键步骤4:清理监听 window.removeEventListener('popstate', this.handleBackButton); }, methods: { // 关键步骤3:拦截逻辑 handleBackButton(event) { if (this.isShowMask) { this.closeMask(); history.pushState(null, null, document.URL); // 维持当前页 event.preventDefault(); // 阻止默认返回 } }, closeMask() { this.isShowMask = false; }, goBack() { this.$router.go(-1); // 手动返回 } } } </script> <style scoped> .mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); } </style> ``` ### 关键注意事项 1. **iOS 特殊处理** iOS 微信浏览器会重新加载页面,建议在 `pageshow` 事件中同步状态: ```javascript mounted() { window.addEventListener('pageshow', this.syncState); }, methods: { syncState() { // 同步页面状态 } } ``` 2. **安卓双触发问题** 部分安卓机型会连续触发两次 `popstate`,需添加防抖: ```javascript let isHandling = false; handleBackButton() { if (isHandling) return; isHandling = true; // ...拦截逻辑 setTimeout(() => isHandling = false, 500); } ``` 3. **路由兼容性** 使用 Vue Router 时,需确保与原生历史记录操作兼容: ```javascript this.$router.push('/current-page'); // 替代 history.pushState ``` ### 实现效果 1. 首次点击返回按钮 → 关闭蒙层(仍停留当前页) 2. 再次点击返回按钮 → 正常返回上一页 3. 页面内手动返回 → 直接触发路由跳转 > **引用说明**:该方法基于微信浏览器特性实现,通过操作历史记录栈改变返回行为[^3][^4]。iOS 需注意页面状态同步问题[^2],安卓需处理事件多次触发[^1]。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值