单页面的物理返回 原来可以这么玩 !(基于Vue2.0)

571 阅读1分钟

前言

有时候我们需要用vue写移动端的单页面应用,我们知道在单页面应用中,我们有些场景非常需要通过按钮 js 来控制 物理返回键,也就是 :有些表单组件中,有很多字段是需要二级页面支撑,比如一个新增客户 :销售区域选择、客户分类选择、仓库列表选择,并不是简单的用下拉就可以解决,因为 例如客户分类需要是可以搜索并分页,因此必须要二级页面的支撑,基于 vue2.0 的 popupLayer 组件使物理返回和js控制返回达到了统一,而不是返回上上个状态,不废话上码。

demo 体验入口

html部分

<template>
  <div 
  class="pop-container" 
  :class="{ show: visible, animation: isAnimation }" 
  :style="{ zIndex }" 
  :id="id">
    <slot></slot>
  </div>
</template>

script部分

<script>
export default {
  name: 'popupLayer',
  props: {
    // 显示状态,默认 false 不显示
    visible: Boolean,
    // 是否开启动画,默认开启 true
    isAnimation: {
      type: Boolean,
      default: true
    },
    // 容器DOM的层级
    zIndex: {
      type: Number,
      default: 1000
    },
    // 设置存储于locaStorage中的字段名称,默认 historyState
    storeName: {
      type: String,
      default: 'historyState'
    }
  },
  data() {
    return {
      index: 0, //返回层级
      isSynced: false, // 是否已经同步过
      id: 'popuplayer_' + this.guid() //弹层唯一 id(容器唯一id)
    }
  },
  watch: {
    visible(val) {
      this[val ? 'show' : 'hide']()
    }
  },
  mounted() {
    window.addEventListener('popstate', e => {
      // 浏览器历史状态  (控制台可以直接输入:history)
      let curState = e.state && e.state.id ? e.state.id : ''
      // 获取当前的 本地popuplayer历史记录
      let historyState = this.store().getCur()
      // 获取弹出层的层级数
      let _len = historyState.length
      // 如果 弹出层的层级数 有值 并且 当前的返回事件id值 不等于
      if (_len && curState != historyState[_len - 1]) {
        if (historyState[_len - 1] == this.id) {
          // 抛出关闭事件
          this.$emit('onClose')
          // 关闭弹出层
          this.$emit('update:visible', false)
          // 删除弹出层id
          this.store().pop()
          // 点击物理返回键,直接触发 popstate 事件,走到这里 说明 上一步 已经同步删除了本地 弹出层id
          this.isSynced = true
          // 输入框失去焦点
          document.activeElement.blur()
        }
      }
    })
  },
  created() {
    // 刷新页面 就重置
    this.store().reset()
    /**
     *  触发顺序说明:
     *  非物理返回键触发:先 只需 hide 再触发 物理返回事件 popstate
     *  物理返回,则 先触发 popstate 事件,再触发 hide,需要一个中间变量(isSynced)来调和 
     */
  },
  methods: {
    /**
     * @description: 基于 localStorage 封装的组件所需的相关方法
     */    
    store() {
      // 为保证无依赖,我们采用 localStorage
      const name = this.storeName
      const store = localStorage
      const parse = JSON.parse
      const stringify = JSON.stringify
      const old = parse(store.getItem(name))
      return {
        reset() {
          if (!store.getItem(name) || old.length) {
            store.setItem(name, stringify([]))
          }
        },
        update(newVal) {
          store.setItem(name, stringify([...old, newVal]))
        },
        getCur() {
          return old
        },
        pop() {
          old.pop()
          store.setItem(name, stringify(old))
        },
        push(val) {
          store.setItem(name, stringify([...old, val]))
        }
      }
    },
    /**
     * @description: 为保证每个弹出层的id不同,生成guid的方法(不能用时间戳哈)
     * @param  {*}
     * @return {*}
     */    
    guid() {
      function S4() {
        return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
      }
      return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4()
    },
    /**
     * @description: 打开弹层
     * 1. 打开弹出层
     * 2. 历史堆栈中 push 一个状态
     * 3. 往外抛出 打开弹出层 成功 后的 回调
     * 4. 本地自定义堆栈中同步 弹出层 id
     */
    show() {
      // 历史堆栈 push 一个 状态
      window.history.pushState({ id: this.id }, '')
      // 本地历史堆栈数字同步记录
      this.store().push(this.id)
      // 对外抛出弹出层打开事件
      this.$emit('onOpen', this.id)
    },
    /**
     * @description: 关闭弹出层
     * 判断用户是否直接点击物理返回键:
     * 1. 如果用户直接点击的是物理返回键,说明已经在   popstate 事件中已经同步删除 弹出层id了,此时这里无需操作
     * 2. 如果用户是通过点击按钮事件触发关闭(如:改变 <popupLayer :visible.sync="show" ><popupLayer> 的 show 的状态 false 或 true),
     *    则同步本地弹出层id数组(本地历史记录数组) 和 历史堆栈
     *
     */
    hide() {
      // 如果用户是通过点击物理返回键关闭,说明 历史栈 和 本地历史记录 已经同步过,此时只需还原 isDel 状态为 false 即可
      if (this.isSynced) return (this.isSynced = false)
      // 删除弹出层id
      this.store().pop()
      // 同步历史记录
      this.index ? history.back(-this.index) : history.back()
    },
    setLevel(level) {
      this.index = level
      setTimeout(() => {
        this.index = 0
      }, 300)
    }
  }
}
</script>

css部分

<style scoped>
.pop-container {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  width: 100%;
  backface-visibility: hidden;
  z-index: 10;
  transform: translateX(100%);
  background-color: #fff;
}
.pop-container.animation {
  transition: transform 0.3s;
}
.pop-container.show {
  transform: translateX(0);
}
</style>

如何弹弹弹!!!

以 demo 为例,示例代码如下,popupLayer 组件可以继续嵌套,继续弹起走!

<template>
  <div class="wrapper">

    <button @click="show = true">open</button>

    <popupLayer :visible.sync="show">
      <div class="pop1">
        <h1>pop1弹出层</h1>
        <button @click="close1">关闭pop1弹出层</button>
        <button @click="show1 = true">开启pop2弹出层</button>

        <popupLayer :visible.sync="show1" ref="layer">
          <h1>pop2弹出层</h1>
          <button @click="close2">关闭pop2弹出层</button>
        </popupLayer>
      </div>
    </popupLayer>
    <button @click="isShowSelect = true">select</button>
    <selectCom :isShow.sync="isShowSelect"></selectCom>
  </div>
</template>

<script>
import popupLayer from "@/components/popupLayer";
import selectCom from "@/components/selectCom";

export default {
  components: { popupLayer, selectCom },
  props: {},
  data() {
    return {
      show: false,
      show1: false,
      isShowSelect: false
    };
  },
  methods: {
    close1() {
      console.log(`来源:按钮close1 --- 事件:关闭`);
      this.show = false;
    },
    close2() {
      console.log(`来源:按钮close2 --- 事件:关闭`);
      // this.$refs.layer.setLevel(-2);
      // this.show = false;
      this.show1 = false;
    }
  } 
};
</script>
<style>
.wrapper {
  background-color: aquamarine;
}
.pop1 {
  height: 100%;
  background: brown;
}
</style>

selectCom 组件源码(只需单个二级页面,弹一次即可)

<template>
  <popupLayer :visible.sync="show" @onClose="$emit('update:isShow', false)">
    二级支撑页面内容区域
    <button class="btn" @click="$emit('update:isShow', false)">取消</button>
  </popupLayer>
</template>

<script>
import popupLayer from "@/components/popupLayer";
export default {
  name: "selectCom",
  components: { popupLayer },
  props: {
    // 是否显示
    isShow: Boolean
  },
  data() {
    return {
      show: false
    };
  },
  watch: {
    isShow(val) {
      this.show = val;
    }
  }
};
</script>
<style scoped>
</style>

结语

同时,如果你有更好的点子,欢迎留言

文中若有不准确或错误的地方,欢迎指出,谢谢!