专业编程基础技术教程

网站首页 > 基础教程 正文

基于elementUI的popupjs开发移动端组件(一)

ccvgpt 2024-08-08 13:11:09 基础教程 9 ℃

本文来自我的小伙伴晨荒,全网首发


基于elementUI的popupjs开发移动端组件(一)

场景:平常移动端项目中,总是会遇到一些对话框,作用安卓端用户,我希望通过返回键能直接关闭对话框,但是不要退出当前页面,更贴近原生APP的操作方式

对此,笔者发现,通过h5的historyState可以解决此类问题。

在进行操作的时候,我们在history里面推入一条记录,进行返回的时候,我们可以通过window.onpoostate事件来监控,关闭对话框。

借用elementUI的popupjs管理自己的弹窗

element源码 src/utils/popup 文件夹下存放了element的弹窗管理器

? 用过elementUI的同学不难发现,框架下的每一个对话框,都有一个z-index,且实时变化,每一个对话框都是后来居上

下面我们梳理一下这个管理器

//popup-manager.js(有删减)
//作用就是,把遮罩层抽离出各个组件,做一个统一管理

let hasModal = false;
let hasInitZIndex = false;
let zIndex;

const instances = {};
const PopupManager = {
  modalFade: true,  //遮罩层动画

  //获取当前实例
  getInstance: function(id) {
    return instances[id];
  },

  //注册实例,每个弹窗组件beforeCreate事件中,会生成一个popId ,并调用此方法
  register: function(id, instance) {
    if (id && instance) {
      instances[id] = instance;
    }
  },

  //注销实例
  deregister: function(id) {
    if (id) {
      instances[id] = null;
      delete instances[id];
    }
  },
	
  //获取最新的Z-index值,每次创建新组件或者弹窗展现时会获取并使用
  nextZIndex: function() {
    return PopupManager.zIndex++;
  },

  //每个实例 如果有配置灰色遮罩层,会在此储存一条记录信息
  modalStack: [],

  //遮罩层点击事件
  doOnModalClick: function() {
    const topItem = PopupManager.modalStack[PopupManager.modalStack.length - 1];
    if (!topItem) return;

    const instance = PopupManager.getInstance(topItem.id);
    if (instance && instance.closeOnClickModal) {
      instance.close();
    }
  },

  // 打开或创建遮罩层
  openModal: function(id, zIndex, dom, modalClass, modalFade) {
    if (!id || zIndex === undefined) return;
    this.modalFade = modalFade;

    const modalStack = this.modalStack;
	// 查询对应popip的遮罩层是否已经显示 
    for (let i = 0, j = modalStack.length; i < j; i++) {
      const item = modalStack[i];
      if (item.id === id) {
        return;
      }
    }
	// 获取当前遮罩层,没有就创建 方法如下
    const modalDom = getModal();
	
    //下面是给遮罩层添加类名   
    addClass(modalDom, 'v-modal');
    //添加动画
    if (this.modalFade && !hasModal) {
      addClass(modalDom, 'v-modal-enter');
    }
    if (modalClass) {
      let classArr = modalClass.trim().split(/\s+/);
      classArr.forEach(item => addClass(modalDom, item));
    }
    //动画结束
    setTimeout(() => {
      removeClass(modalDom, 'v-modal-enter');
    }, 200);

    //如果方法有传dom,则添加到dom盒子中,不然则添加到body(默认)
    if (dom && dom.parentNode && dom.parentNode.nodeType !== 11) {
      dom.parentNode.appendChild(modalDom);
    } else {
      document.body.appendChild(modalDom);
    }

    if (zIndex) {
      modalDom.style.zIndex = zIndex;
    }
    modalDom.tabIndex = 0;
    modalDom.style.display = '';

    this.modalStack.push({ id: id, zIndex: zIndex, modalClass: modalClass });
  }, 
    
  //关闭遮罩层
  closeModal: function(id) {
    const modalStack = this.modalStack;
    const modalDom = getModal();

    if (modalStack.length > 0) {
      const topItem = modalStack[modalStack.length - 1];
      if (topItem.id === id) {
        if (topItem.modalClass) {
          let classArr = topItem.modalClass.trim().split(/\s+/);
          classArr.forEach(item => removeClass(modalDom, item));
        }

        modalStack.pop();
        //如果还存在其他实例遮罩层,这修改zIndex为上一层的层级
        if (modalStack.length > 0) {
          modalDom.style.zIndex = modalStack[modalStack.length - 1].zIndex;
        }
      } else {
        for (let i = modalStack.length - 1; i >= 0; i--) {
          if (modalStack[i].id === id) {
            modalStack.splice(i, 1);
            break;
          }
        }
      }
    }
	
    // 所以遮罩层在modalStack(记录)弹出后,设置遮罩层淡出动画,并讲display设为nonde
    if (modalStack.length === 0) {
      if (this.modalFade) {
        addClass(modalDom, 'v-modal-leave');
      }
      setTimeout(() => {
        if (modalStack.length === 0) {
          if (modalDom.parentNode) modalDom.parentNode.removeChild(modalDom);
          modalDom.style.display = 'none';
          PopupManager.modalDom = undefined;
        }
        removeClass(modalDom, 'v-modal-leave');
      }, 200);
    }
  },

    
  /* --------------------控制返回键添加的代码--------------------------------*/
  //思路是 执行动作时添加一条记录  使用pushstate
  //当记录大于一,添加记录时,改为replacestate来管理 ,目的是为了保持路由干净
  historyStack:[],

  pushOrReplaceState:function(id){
    let instance = this.getInstance(id);
    if(!instance)return ;
    if (this.historyStack.length == 0) {
      this.historyStack.push(this.getInstance(id))
      history.pushState({
        id:id
      }, '', '');
    } else {
        this.historyStack.push(this.getInstance(id))
        history.replaceState({
          id: id
        }, '', '');
    }
  },

  popState:function(id){
    const historyStack = this.historyStack
    for (let i = 0, j = historyStack.length; i < j; i++) {
      const item = historyStack[i];
      if (item._popupId === id) {
        history.back();
        this.historyStack.splice(i,1);
      }
    }
  }
  /* ----------------------------------------------------*/
};



//获取一个遮罩层
const getModal = function() {
   // 如果管理器存在一个遮罩层 便直接返回
  let modalDom = PopupManager.modalDom;
  if (modalDom) {
    hasModal = true;
  } else {
    hasModal = false;
    //创建一个遮罩层 , 并为遮罩层添加事件
    modalDom = document.createElement('div');
    PopupManager.modalDom = modalDom;

    modalDom.addEventListener('touchmove', function(event) {
      event.preventDefault();
      event.stopPropagation();
    });
	//遮罩层点击事件
    modalDom.addEventListener('click', function() {
      PopupManager.doOnModalClick && PopupManager.doOnModalClick();
    });
  }

  return modalDom;
};


popup-manager相当于一个中枢,每一个弹出层需要一个遮罩层,告诉我你的ID,然后openModel就可以了

至于你想知道最顶层的zIndex应该是多少,调用popupManaer.nextZIndex()便可获取

借用它,我们不用在每个对话框都添加一个遮罩层。 简单的,我们只要写设置好定位,然后通过nextZindex获取最顶层的值并设置就可以了

下面怎么开发我们的组件呢?

有的同学应该发现了,popup文件夹下还有一个index.js 。它是基于popup-manager.js,在组件各个生命周期进行操作的mixins对象

下面看部分代码(这里不贴源码了,建议读者下载源码查看)

// 这个是用于生成ID的参数,每个minxin混入的组件,都可以通过闭包拿到它便使用它
let idSeed = 1;

{
 ...,
 beforeMount() {
    //每次组件创前,都生产一个唯一的IP告诉popup-manager
    this._popupId = 'popup-' + idSeed++;
    PopupManager.register(this._popupId, this);
  },
  beforeDestroy() {
    //组件注销
    PopupManager.deregister(this._popupId);
    PopupManager.closeModal(this._popupId);
    
  },
  
  watch:{
     visible(){...
           //visible  作为props值,由业务层提供
     	   //为true时 进行open操作
     	   //为false时 进行close操作
              } ,
  },
      
  methods:{
      open(options){...
            //被调用的地方是 watch:visible
      		//整合参数传入的配置,相当于打开遮罩前函数(options提供钩子能力)
      		// 业务上主要做了一个延迟打开的逻辑
            },
      doOpen(){...
              //主要用于打开遮罩(组件modal为true的时候)
      	      //给对话框加上最新的zIndex
      		  //调用onOpen钩子函数
              },
      doAfterOpen(){...
              //修改_opening为false
      		  // !!笔者在这添加了history状态代码
      		  //如果组件需要通过路由改变状态
              if(this.historyControl){
                  PopupManager.pushOrReplaceState(this._popupId)
              }
                   },
      close(){...
              //如果组件需要通过路由改变状态
      			  if(this.historyControl){
                      PopupManager.popState(this._popupId)
                  }
              //此方法主要是关闭组件的设置方法
              // 同上面open方法一样 添加了延迟逻辑
             },
      doClose(){...
               //调用了onClose钩子函数 供业务处理
               },
      doAfterClose(){...
      			// 主要是关闭遮罩层
                    },
    
  }
}

这个js文件很简单,它为组件提供了适用popup-manager的方法,巧妙的设置钩子,让我们不用去理会时候打开或关闭遮罩层,调用设置的钩子我们就可以完成组件业务。

下期,我们将使用上面的工具方法实现一个仿VUX的对话框。。未完待续。。。谢谢您的关注

Tags:

最近发表
标签列表