likes
comments
collection
share

vue2设计实现一个全局弹窗组件

作者站长头像
站长
· 阅读数 34

上周工作中在用elementUI的mesagebox组件的自定义功能jsx写法时,发现只能做数据渲染无法做事件绑定数据响应。参阅elementUI源码后才发现这块是用插槽实现的,creatEelement生成的vnode被赋值到this.$slots.default 插槽的数据是没有响应的也就解释得通了。

vue2设计实现一个全局弹窗组件

vue2设计实现一个全局弹窗组件

实际业务开发过程中其实一般不需要去自己设计实现这种弹窗 除非有很必要的定制化要求 以及这个第三方组件的功能无法满足开发需要 自己去实现可以做一些功能扩展及改造

接下来参考elementUI的messagebox源码,使用vue2提供的extend api动态创建组件去实现的一个简易的全局弹窗(vue3就是createapp)

目录结构如图:

vue2设计实现一个全局弹窗组件

设计一个弹窗

按顺序首先是考虑样式布局

  1. 首先分header content bottom三部分 header放title content放展示的主体内容 bottom放按钮
  2. 然后是否需要有全屏遮罩层 有的话考虑层叠顺序及实现方式
  3. 是否居中 居中如何实现有哪些方案
  4. ...等等等等

然后是组件实例的配置

  1. props
  2. events
  3. slots

最后组件实例的创建 设计 挂载 暴露install方法等

先给dialog.vue填空

样式布局
<template>
  <div v-show="visible" :class="['message-box', type]">
    <div class="inner">
      <header class="header">
      </header>
      <div class="content">
      </div>
      <div class="bottom">
      </div>
    </div>
  </div>
</template>
<style lang="scss">
body {
  margin: 0;
}

h1 {
  margin: 0;
  font-weight: normal;
}

.message-box {

//粘性定位实现遮罩层
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 2001;//element里面默认是2000 我大他一点 如果层级不够跟其他组件出现冲突 可以配置成prop参数传入手动配置
  background-color: rgba(0, 0, 0, .5);

  .inner {
    position: absolute;
    top: 100px;
    left: 50%;
    top:50%;
    transform: translate(-50%,-50%);
    width: 500px;
    background-color: #fff;
    box-shadow: 1px 3px 5px #ededed;
    overflow: hidden;
    border-radius: 5px;

    .header {
      height: 44px;
      line-height: 44px;
      padding: 0px 10px;
      box-sizing: border-box;

      .title {
        float: left;
        font-size: 16px;
      }

      .close-btc {
        float: right;
        cursor: pointer;
      }
    }

    .content {
      padding: 20px;
      box-sizing: border-box;
    }

    .bottom {
      height: 45px;
      line-height: 45px;
      display: flex;
      margin-top: 30px;

      button {
        flex: 1;
        display: block;
        border: 0;
        cursor: pointer;
        margin: 0 10px;

      }
    }
  }
//弹框提供四种提示类型 对应四种样式
  &.primary {
    header {
      background-color: rgb(148, 202, 231);
      color: #fff;
    }
  }

  &.success {
    header {
      background-color: rgb(148, 231, 173);
      color: #fff;
    }
  }

  &.warn {
    header {
      background-color: rgb(245, 175, 122);
      color: #fff;
    }
  }

  &.danger {
    header {
      background-color: rgb(240, 145, 145);
      color: #fff;
    }
  }
}
</style>

比较关键的居中方案特别多貌似有六七种比如flex text-aligin:center、 tabel-cell 、margin:0 auto、 absolute定位手动计算div宽度然后控制移动 等等。 我采用的tranform: translate的方案

优点是方便而且从浏览器渲染的性能方面考虑更优秀。 transform 浏览器绘制方面简单讲就是会创建一个合成层 拥有独立的GraphicsLayer transform的绘制只会发生在独立的graphicslayer中,不会改变render tree的结构(相反margin会改变元素的位置重绘)并不会引起整个页面的回流重绘。而且合成层的位图会交由 GPU 合成,相比 CPU 处理要快。webgl的底层opengl不就是利用硬件加速渲染视图所以性能和处理能力更好吗

以上是样式方面 很多css参数其实可以做成配置化传入更方便配合实现更丰富的功能 关于居中方案提到的transform 涉及浏览器底层的原理想了解更多的可看网易云团队写的这篇 浏览器渲染魔法之合成层

组件构成

一个再复杂的组件,都是由三部分组成的:propeventslot

它们构成了 Vue.js 组件的 API。如果你开发 的是一个通用组件,那一定要事先设计好这三部分,因为组件一旦发布,后面再修改 API 就很困难了,使用者都是希望不断新增功能,修复 bug,而不是经常变更接口。如果你阅读别人写的组件,也可以从这三个部 分展开,它们可以帮助你快速了解一个组件的所有功能。

我实际业务中并没有需要去重新实现弹窗 出于学习需要 就简单设计下 功能可以自己根据需求具体扩展 比如按钮绑定一些请求 prop控制是否居中 z-index 点击遮罩层是否关闭 是否需要动画 插槽地方若想实现自定义传入jsx可实现数据响应及自定义事件等可以用动态组件component的is属性结合extend等等 添加完上面讲的三要素后 代码是这样的

<template>
  <div v-show="visible" :class="['message-box', type]">
    <div class="inner">
      <header class="header">
        <h1 class="title">{{ title }}</h1>
        <span @click="handleAction('cancel')" class="close-btc ">x</span>
      </header>
      <div class="content">
        <slot>{{ message }}</slot>
      </div>
      <div class="bottom">
        <el-button @click="handleAction('cancel')">取消</el-button>
        <el-button @click="handleAction('confirm')">确认</el-button>
      </div>
    </div>
  </div>
</template>
  
<script>
export default {
  name: 'MessageBox',
  props: {
    title: {
      type: String,
      default: '标题',
    },
    message: {
      type: String | Object,
      default: '内容',
    },
    type: {
      type: String,
      default: 'primary',
      validator(value) {
        return [
          'primary',
          'success',
          'warn',
          'danger'
        ].includes(value);
      }
    }
  },
  data() {
    return {
      action: '',
      visible: false,
      callback: null,
    };
  },
  methods: {
    getSafeClose() {
      const currentId = this.uid;
      return () => {
        this.$nextTick(() => {
          if (currentId === this.uid) this.doClose();
        });
      };
    },
    handleAction(action) {
      this.action = action;
      if (typeof this.beforeClose === 'function') {
        this.close = this.getSafeClose();
        this.beforeClose(action, this, this.close);
      } else {
        this.doClose();
      }
    },
    doClose() {
      if (!this.visible) return;
      this.visible = false;
      setTimeout(() => {
      //至于这里为什么模仿element要用settimeout包一下回调放入下一次事件循环执行 
      //我认为应该是跟beforeClose有关 如果用户配置了beforeClose 也传入了callback //自己手动去调用第三个参数也就是this.close去关闭弹窗 
      //然后关闭后面又写了其他执行代码即使有nexttick代码也应该晚于这些代码执行 
      //不是settimeout的话 callback会先于其他代码执行 
      //这时候 跟事件循环 回调函数应该的执行时机明显不符 这个不是太重要但迷惑了半天
        if (this.action) this.callback(this.action, this);
      });
    }
  },
};
</script>

接下来dialog.js文件的内容

//可以做一些默认配置 扩展功能 
const defaults = {
    title:'',
    message:'',
    type:'',
    beforeClose:'',
    center:true
    //....
}
// 创建组件实例
import Vue from 'vue'
import Dialog from './dialog.vue'
let currentMsg, instance
let msgQueue = [];
//通过extend 创建一个构造函数 
const InstanceCtor = Vue.extend(Dialog)
const defaultCallback = action => {
  if (currentMsg) {
    let callback = currentMsg.callback
    // 不传就是undefined
    if (typeof callback === 'function') {
      callback(action)
    }
    if (currentMsg.resolve) {
      if (action === 'confirm') {
        if (instance.showInput) {
          currentMsg.resolve({ value: instance.inputValue, action })
        } else {
          currentMsg.resolve(action)
        }
      } else if (currentMsg.reject && (action === 'cancel' || action === 'close')) {
        currentMsg.reject(action)
      }
    }
  }
}
// 创建实例
function initInstance (propsData) {
  instance = new InstanceCtor({
    el: document.createElement('div') // 如果不传el new vue的时候不会需要执行$mount挂载
  })
  instance.callback = defaultCallback
}
// 打开对话框
function showDialog () {
  if (!instance) { // 可以看出 instance只有一个实例 单例模式
    initInstance()
  }
  instance.action = ''
  if (!instance.visible) { 
  //visible判断配合下面执行的nexttick visible=true涉及到任务队列  
  //如果同步多次调用同一eventloop触发多次showdialog的时候判断visible都会
  //是false msgQueue长度都会大于0 进入下面的判断给实例赋值 
  //currentMsg只会是eventloop最后一个实例的配置这时候弹窗展示的都会是最后这个的信息内容
  //如果异步多次调用 涉及下面callback重新被改造
    if (msgQueue.length > 0) {
      currentMsg = msgQueue.shift()// 移出队列第一项,拿到参数信息
      let options = currentMsg.options
      for (const prop in options) {
        if (Object.hasOwnProperty.call(options, prop)) {
          instance[prop] = options[prop]
        }
      }
      if (options.callback === undefined) {
        instance.callback = defaultCallback
      }
      let oldCb = instance.callback // 从新对 callback 进行制定
      instance.callback = (action, instance) => { 
      // callback是按钮点击触发doclose执行的 visible = false  
      //然后settimeout 执行callback这时候会再次调用showdialog 第一个执行showdialog后 nexttick visible = true 早于settimeout执行
      //所以后面的msg执行showdialog时无法进入!visible == true的判断 也就无法替换instance的信息展示无法弹出msgQueue  
      //所以msgQueue也就缓存着多个后面多个未执行的Msg 
      //当点击确认 取消关闭弹窗的操作触发doclose时候也就短暂使得visible为false //diclose执行callback的时候也就可以进入!visible == true 
      //且msgQueue.length > 0的判断 弹出msgQueue 让currentMsg指针指向msgQueue队列刚弹出的这个msg 替换instance展示的内容 
      //重新赋值instance的callback 回调触发时重复上面的操作 直到msgqueue清空
        oldCb(action, instance)
        showDialog()// 进行了递归调用,消耗队列
      }
      // 给插槽default位置放vnode
      if (isVNode(instance.message)) {
        instance.$slots.default = [instance.message]// 触发更新
        instance.message = null
      } else {
        delete instance.$slots.default
      }
      document.body.appendChild(instance.$el)
      Vue.nextTick(() => {
        instance.visible = true
      })
    }
  }
}
function hasOwn (obj, key) {
  const hasOwnProperty = Object.prototype.hasOwnProperty
  return hasOwnProperty.call(obj, key)
};
function isVNode (node) {
  return node !== null && typeof node === 'object' && hasOwn(node, 'componentOptions')
};

const MessageBox = function (options, callback) {
  console.log('....', new Date().getTime())
  if (Vue.prototype.$isServer) return
  if (typeof options === 'string' || isVNode(options)) {
    options = {
      message: options
    }
    if (typeof arguments[1] === 'string') {
      options.title = arguments[1]
    }
    // callback 是在methods的doclose被调用的时候执行的
  } else if (options.callback && !callback) {
    callback = options.callback
  }
  return new Promise((resolve, reject) => {
    msgQueue.push({
      options: Object.assign({}, defaults,options),
      resolve,
      reject
    })
    showDialog()
  })
}
export default MessageBox
export { MessageBox }

最后index.js入口文件提供install方法挂载到Vue的原型对象上

import Dialog from './dialog.js'
export default {
  install (Vue) {
    Vue.prototype.$msgBox = Dialog
  }
}

看一看效果图

vue2设计实现一个全局弹窗组件

最后

coding过程中碰到些小的迷惑点 总结记录下

  1. 使用extend的时候如果写成instance = new InstanceCtor({...}).mount()为什么可以拿到组件实例后面看vue源码可以知道extend返回的VueComponent就是执行了下init()而mount() 为什么可以拿到组件实例 后面看vue源码可以知道 extend返回的VueComponent就是执行了下_init() 而mount()为什么可以拿到组件实例后面看vue源码可以知道extend返回的VueComponent就是执行了init()mount()的返回值就是当前实例
  2. new InstanceCtor().mount()是相当于(newInstanceCtor()).mount() 是相当于(new InstanceCtor()).mount()是相当于(newInstanceCtor()).mount()还是相当于new (InstanceCtor().$mount()) mdn上其实看解释也看不出来 自己想办法测下就可以知道是相当于第一种 基础考点
  3. 为什么插槽不是响应的 可以参考这篇结合源码 # 深入剖析Vue源码 - Vue插槽,你想了解的都在这里!

最后

基于对插槽的认识 ,浅析如果想实现自定义可编辑的表单弹窗也就是灵活的用jsx的方式写弹窗表单还能支持事件绑定和数据相应,

参考element实现是拿的createElement返回的vnode给到slots.default上 插槽只能做数据渲染 无法满足。

其实可以直接传render函数去实现 暂时有实现的方法但不太简洁等优化完再更新

目前大致是这样的思路简单写下

vue2设计实现一个全局弹窗组件

vue2设计实现一个全局弹窗组件

vue2设计实现一个全局弹窗组件

对于弹窗还有很多可完善的地方比如加动画 皮肤可配置 可拖拽控制位置大小 抛出更多的事件 关闭之后到底应该销毁还是缓存 弹窗内容涉及到表单是否还要这么设计等等 可以想想但懒得写了

本文随便写写记录下 记录的不太清晰 没什么重点 期待大佬评论指教

古德拜

转载自:https://juejin.cn/post/7212621497809403941
评论
请登录