【BUG】h5全屏弹窗点击后退键时停留当前页

299 阅读4分钟

h5全屏弹窗点击后退键时停留当前页

最近本菜鸡在处理h5项目时,小伙伴们已经把组织架构的组件造好了,于是直接就拿着组件开始整活了,一路高歌到测试环节。在测试环节中,由于在首页就用到这个组件,每次打开弹窗后,想关闭时,测试大大们喜欢用虚拟回退键,再加上进企微版应用后,顶栏上自带回退键,用得更是得心应手,屡屡退出应用,抱怨开发垃圾使用不便。由于设计稿UI界面上存在<取消>、<确认>按钮,导致小伙伴先入为主,认为这是一个全屏弹窗组件,但目前又需要全屏页面支持点击虚拟回退键or回退键仅把弹窗关闭而实际路由并不后退。

这不,恰好吹牛的声音略大了些,于是乎就被叫去帮忙解决这个问题。

思考过程

原本是想重新改写成个页面的...但是时间并不充裕,而且取值传参方式也得大改一套,遂弃。

后来想着既然要实现路由操作的话,我模拟加一层路由栈,然后点击后退的时候把加的那一层路由记录给去掉。转头一想,似乎很有操作性的样子,赶紧捋一捋实现一下~~

首先需要模拟添加一层路由,于是联想到浏览器中添加和修改历史记录中条目的俩兄弟,没错就是pushStatereplaceState

HTML5 引入了 history.pushState() 和 history.replaceState() 方法,它们分别可以添加和修改历史记录条目。这些方法通常与window.onpopstate 配合使用。使用 history.pushState() 可以改变referrer,它在用户发送 XMLHttpRequest 请求时在HTTP头部使用,改变state后创建的 XMLHttpRequest 对象的referrer都会被改变。因为referrer是标识创建  XMLHttpRequest 对象时 this 所代表的window对象中document的URL。

// 新增一条记录
// 与 window.location.href 相比,
// pushState 的优点是任意URL都可以新增一条记录
// 而 window.location.href 对于相同的 URL 则无变化,包括仅变化 hash 部分。

window.history.pushState('状态对象', 'title', 'URL')

// 修改当前的记录项,而不是新增一条记录
window.history.replaceState('状态对象', 'title', 'URL')
  • 状态对象:可以是能被序列化的任何东西,但有640k的大小限制。
  • title:一个预留参数,可以是存放当前页的标题。
  • URL:可以是同源的任意URL

接着需要在点击回退按钮时监听到回退事件,于是联想到onpopstate监听事件

每当活动的历史记录项发生变化时, popstate 事件都会被传递给window对象。如果当前活动的历史记录项是被 pushState 创建的,或者是由 replaceState 改变的,那么 popstate 事件的状态属性 state 会包含一个当前历史记录状态对象的拷贝。

window.onpopstate = function(event) {
  // console.log(event)
  // do something...
}

最后如果弹窗没有点击按钮,而想把路由回归更改之前的状态,则需手动处理,将新增路由去除

DOM window 对象通过 history 对象提供了对浏览器的会话历史的访问(不要与 WebExtensions history搞混了)。它暴露了很多有用的方法和属性,允许你在用户浏览历史中向前和向后跳转,同时——从HTML5开始——提供了对history栈中内容的操作。

// 向前跳转
window.history.forward();

// 向后跳转
window.history.back();

// 跳转到任意一层
// n = 1 时 <==> forward 向前跳转
// n = -1 时 <==> back 向后跳转
window.history.go(n);

解决方案

由于项目是基于Vue框架,所以为了解决所有全屏弹窗的后退问题,就用了mixin

// 用于防止全屏弹窗退出应用
export const popStateWatcher = {
  methods: {
    // 修改Title
    handleEditPageTitle(title) {
      window.document.title = title;
    },
    // 创建监听事件
    handleCreatePopWatch() {
      window.addEventListener("popstate", this.popStateChange);
    },
    // 去除监听事件
    handleRemovePopWatch() {
      window.removeEventListener("popstate", this.popStateChange);
    },
    // 事件
    popStateChange(e) {
      const popState = this.handlePopStorage();
      this.handleEditPageTitle(popState?.title || "");
      this.afterPopChange && this.afterPopChange(e); // 需要单独定义 afterPopChange 处理业务
      this.handleRemovePopWatch();
      this.handlePopStorage('remove');
      // window.history.pushState(popState, popState.title, popState.url);
    },
    // 按钮调用 -- 浏览器加一层空白路由栈
    handleEmptyPopPush(type = "push") {
      const popState = {
        type,
        title: window.document.title,
        url: window.location.href,
        // url: `${window.location.href.split('#')[0]}#/emptyPage`,
      };
      this.handlePopStorage('set', popState);
      const func = `${type}State`;
      window.history[func](popState, popState.title, popState.url);
      this.handleCreatePopWatch(); // 点击再添加后退监听
    },
    // 手动返回上一页
    handleGoBackPopURL(order = -1) {
      window.history.go(order);
      // this.handlePopStorage('remove');
      // window.onbeforeunload = function (params) {
      //   return false;
      // };
    },
    // 判断是不是空栈
    handlePopStorage(type = "get", popState = {}, key = "__popState__") {
      if (type === "set") return sessionStorage.setItem(key, JSON.stringify(popState));
      if (type === "remove") return sessionStorage.removeItem(key);
      const JSONP = JSON.parse(sessionStorage.getItem(key));
      return JSONP;
    },
  },
};

参考资料