当前位置 : 主页 > 网络编程 > JavaScript >

手写Vue弹窗Modal的实现代码

来源:互联网 收集:自由互联 发布时间:2021-04-05
Vue作为最近最炙手可热的前端框架,其简单的入门方式和功能强大的API是其优点。而同时因为其API的多样性和丰富性,所以他的很多开发方式就和一切基于组件的React不同,如果没有对

Vue作为最近最炙手可热的前端框架,其简单的入门方式和功能强大的API是其优点。而同时因为其API的多样性和丰富性,所以他的很多开发方式就和一切基于组件的React不同,如果没有对Vue的API(有一些甚至文档都没提到)有一个全面的了解,那么在开发和设计一个组件的时候有可能就会绕一个大圈子,所以我非常推荐各位在学习Vue的时候先要对Vue核心的所有API都有一个了解。这篇文章我会从实践出发,遇到一些知识点会顺带总结一下。文章很长,一次看不完可以先收藏,如果你刚入门vue,那么相信这篇文章对你以后的提升绝对有帮助

进入正题,我相信不论什么项目几乎都会有一个必不可少的功能,就是用户操作反馈、或者提醒.像这样(简单的一个demo)

其实在vue的中大型项目中,这些类似的小功能会更加丰富以及严谨,而在以Vue作为核心框架的前端项目中,因为Vue本身是一个组件化和虚拟Dom的框架,要实现一个通知组件的展示当然是非常简单的。但因为通知组件的使用特性,直接在模板当中书写组件并通过v-show或者props控制通知组件的显示显然是非常不方便的并且这样意味着你的代码结构要变,当各种各样的弹层变多的时候,我们都将其挂载到APP或者一个组件下显然不太合理,而且如果要在action或者其他非组件场景中要用到通知,那么纯组件模式的用法也无法实现。那么有没有办法即用到Vue组件化特性方便得实现一个通知组件的展现,那么我们可否用一个方法来控制弹层组件的显示和隐藏呢?

目标一

实现一个简单的反馈通知,可以通过方法在组件内直接调用。比如Vue.$confirm({...obj})

首先,我们来实现通知组件,相信这个大部分人都能写出来一个像模像样的组件,不啰嗦,直接上代码

<template>
  <div
    :class="type"
    class="eqc-notifier">
    <i
      :class="iconClass"
      class="icon fl"/>
    <span>{{ msg }}</span>
  <!-- <span class="close fr eqf-no" @click="close"></span> -->
  </div>
</template>

<script>
export default {
  name: 'Notification',
  props: {
    type: {
      type: String,
      default: ''
    },
    msg: {
      type: String,
      default: ''
    }
  },
  computed: {
    iconClass() {
      switch (this.type) {
        case 'success':
          return 'eqf-info-f'
        case 'fail':
          return 'eqf-no-f'
        case 'info':
          return 'eqf-info-f'
        case 'warn':
          return 'eqf-alert-f'
      }
    }
  },
  mounted() {
    setTimeout(() => this.close(), 4000)
  },
  methods: {
    close() {
    }
  }
}
</script>

<style lang="scss">
  .eqc-notifier {
    position: fixed;
    top: 68px;
    left: 50%;
    height: 36px;
    padding-right: 10px;
    line-height: 36px;
    box-shadow: 0 0 16px 0 rgba(0, 0, 0, 0.16);
    border-radius: 3px;
    background: #fff;
    z-index: 100; // 层级最高
    transform: translateX(-50%);
    animation: fade-in 0.3s;
  .icon {
    margin: 10px;
    font-size: 16px;
  }
  .close {
    margin: 8px;
    font-size: 20px;
    color: #666;
    transition: all 0.3s;
    cursor: pointer;
    &:hover {
      color: #ff296a;
    }
  }
  &.success {
    color: #1bc7b1;
  }
  &.fail {
    color: #ff296a;
  }
  &.info {
    color: #1593ff;
  }
  &.warn {
    color: #f89300;
  }
  &.close {
    animation: fade-out 0.3s;
  }
  }
</style>

在这里需要注意,我们定义了一个close方法,但内容是空的,虽然在模板上有用到,但是似乎没什么意义,在后面我们要扩展组件的时候我会讲到为什么要这么做。

创建完这个组件之后,我们就可以在模板中使用了<notification type="xxx" msg="xxx" />

实现通过方法调用该通知组件

其实在实现通过方法调用之前,我们需要扩展一下这个组件,因为仅仅这些属性,并不够我们使用。在使用方法调用的时候,我们需要考虑一下几个问题:

  • 显示反馈的定位
  • 组件的出现和自动消失控制
  • 连续多次调用通知方法,如何排版多个通知

在这个前提下,我们需要扩展该组件,但是扩展的这些属性不能直接放在原组件内,因为这些可能会影响组件在模板内的使用,那怎么办呢?这时候我们就要用到Vue里面非常好用的一个API,extend,通过他去继承原组件的属性并扩展他。

来看代码

import Notifier from './Notifier.vue'

function install(Vue) {
  Vue.notifier = Vue.prototype.notifier = {
    success,
    fail,
    info,
    warn
  }
}

function open(type, msg) {
  let UiNotifier = Vue.extend(Notifier)
  let vm = new UiNotifier({
    propsData: { type, msg },
    methods: {
      close: function () {
        let dialog = this.$el
        dialog.addEventListener('animationend', () => {
          document.body.removeChild(dialog)
          this.$destroy()
        })
        dialog.className = `${this.type} eqc-notifier close`
        dialog = null
      }
    }
  }).$mount()
  document.body.appendChild(vm.$el)
}

function success(msg) {
  open('success', msg)
}

function fail(msg) {
  open('fail', msg)
}

function info(msg) {
  open('info', msg)
}

function warn(msg) {
  open('warn', msg)
}

Vue.use(install)

export default install

可以看到close方法在这里被实现了,那么为什么要在原组件上面加上那些方法的定义呢?因为需要在模板上绑定,而模板是无法extend的,只能覆盖,如果要覆盖重新实现,那扩展的意义就不是很大了。其实这里只是一个消息弹窗组件,是可以在模板中就被实现,还有插件怎么注入,大家都可以自己抉择。

同时在使用extend的时候要注意:

  1. 方法和属性的定义是直接覆盖的
  2. 生命周期方法类似余mixin,会合并,也就是原组件和继承之后的组件都会被调用,原组件先调用

首先通过 let UiNotifier = Vue.extend(Notifier),我们得到了一个类似于Vue的子类,接着就可以通过new UiNotifier({...options})的方式去创建Vue的实例了,同时通过该方式创建的实例,会有组件定义里面的所有属性。

在创建实例之后,vm.$mount()手动将组件挂载到DOM上面,这样我们可以不依赖Vue组件树来输出DOM片段,达到自由显示通知的效果。

扩展:

说一下$mount,我们也许很多项目的主文件是这样的

new Vue({
  router,
  store,
  el: '#app',
  render: h => h(App)
})

其实el与$mount在使用效果上没有任何区别,都是为了将实例化后的vue挂载到指定的dom元素中。如果在实例化vue的时候指定el,则该vue将会渲染在此el对应的dom中,反之,若没有指定el,则vue实例会处于一种“未挂载”的状态,此时可以通过$mount来手动执行挂载。值得注意的是如果$mount没有提供参数,模板将被渲染为文档之外的的元素,并且你必须使用原生DOM API把它插入文档中,所以我上面写的你应该明白了吧!

这是$mount的一个源码片段,其实$mount的方法支持传入2个参数的,第一个是 el,它表示挂载的元素,可以是字符串,也可以是 DOM 对象,如果是字符串在浏览器环境下会调用 query 方法转换成 DOM 对象的。第二个参数是和服务端渲染相关,在浏览器环境下不需要传第二个参数。

好了,我们现在其实就可以在组件中:

this.notifier[state](msg)来调用了,是不是很方便?

进阶

我们刚才实现了在Vue中通过方法来进行用户反馈的提醒,再增加一个难度:

我们vue项目中应该也遇到过这种情况,弹出一个对话框或是选择框?不但要求用方法弹出,并且能接收到对话框交互所返回的结果。

这里就不详细的分析了直接上代码说(之前的代码,用render来写的组件,懒得改了,直接拿来用...),先创建一个对话框组件---Confirm.vue

<script>
  let __this = null
  export default {
    name: 'Confirm',
    data() {
      return {
        config: {
          msg: '',
          ifBtn: '',
          top: null
        }
      }
    },
    created() {
      __this = this
    },
    methods: {
      createBox(h) {
        let config = {}
        config.attrs = {
          id: '__confirm'
        }
        let children = []
        children.push(this.createContainer(h))
        children.push(this.createBg(h))
        return h('div', config, children)
      },
      createBg(h) {
        return h('div', {
          class: 'bg',
          on: {
            click: __this.$cancel
          }
        })
      },
      createContainer(h) {
        let config = {}
        config.class = {
          'box-container': true
        }
        if (__this.config.top) {
          config.style = {
            'top': __this.config.top + 'px',
            'transform': 'translate(-50%, 0)'
          }
        }
        let children = []
        children.push(this.createContentBox(h))
        children.push(this.createClose(h))
        if (__this.config.ifBtn) {
          children.push(__this.createBtnBox(h))
        }
        return h('div', config, children)
      },
      createContentBox(h) {
        let config = {}
        config.class = {
          'content-box': true
        }
        return h('div', config, [__this.createContent(h)])
      },
      createContent(h) {
        let config = {}
        config.domProps = {
          innerHTML: __this.config.msg
        }
        return h('p', config)
      },
      createClose(h) {
        return h('i', {
          class: 'eqf-no pointer close-btn',
          on: {
            click: __this.$cancel
          }
        })
      },
      createBtnBox(h) {
        return h(
          'div', {
            class: {
              'btn-box': true
            }
          }, [
            __this.createBtn(h, 'btn-cancel middle mr10', '取消', __this.$cancel),
            __this.createBtn(h, 'btn-primary middle mr10', '确定', __this.$confirm)
          ])
      },
      createBtn(h, styles, content, callBack) {
        return h('button', {
          class: styles,
          on: {
            click: callBack
          }
        }, content)
      }
    },
    render(h) {
      return this.createBox(h)
    }
  }
  </script>
  
  <style scoped>
  #__confirm {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 10;
    width: 100%;
    height: 100%;
  }
  #__confirm .bg {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 0;
    width: 100%;
    height: 100%;
  }
  #__confirm .box-container {
    position: absolute;
    width: 500px;
    padding: 20px;
    padding-top: 30px;
    border-radius: 3px;
    background: #fff;
    z-index: 1;
    box-shadow: 2px 2px 10px rgba(0,0,0,0.4);
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
  }
  #__confirm .content-box {
    font-size: 14px;
    line-height: 20px;
    margin-bottom: 10px;
  }
  #__confirm .btn-box {
    margin-top: 20px;
    text-align: right;
  }
  #__confirm .close-btn {
    position: absolute;
    top: 15px;
    right: 20px;
    font-size: 16px;
    color: #666666;
  }
  #__confirm .close-btn:hover {
    color: #1593FF;
  }
    #__confirm .bg {
      position: fixed;
    }
  </style>

然后创建confirm.js

'use strict'
import Confirm from './Confirm.vue'
const confirmConstructor = Vue.extend(Confirm)

const ConfirmViewStyle = config => {
  const confirmInstance = new confirmConstructor({
    data() {
      return {
        config
      }
    }
  })
  confirmInstance.vm = confirmInstance.$mount()
  confirmInstance.dom = confirmInstance.vm.$el
  document.body.appendChild(confirmInstance.dom)
}

const close = () => {
  let dom = document.querySelector('body .modelServe-container')
  dom && dom.remove()
  Vue.prototype.$receive = null
}

const closeConfirm = () => {
  let dom = document.getElementById('__confirm')
  dom && dom.remove()
  Vue.prototype.$confirm = null
}

function install(Vue) {
  Vue.prototype.modelServe = {
    confirm: (obj) => {
      return new Promise(resolve => {
        Vue.prototype.$confirm = (data) => {
          resolve(data)
          closeConfirm()
        }
        ConfirmViewStyle(obj)
      })
    }
  }
  Vue.prototype.$dismiss = close
  Vue.prototype.$cancel = closeConfirm
}
Vue.use(install)
export default install

思路很简单,在我们创建的时候同时返回一个promise,同时将resolve通行证暴露给vue的一个全局方法也就是将控制权暴露给外部,这样我们就可以向这样,我上面的confiram.vue是直接把取消绑定成了$cancel,把确定绑定成了$confirm,所以点击确定会进入full,也就是.then中,当然你也可以传参数

this.modelServe.confirm({
  msg: '返回后数据不会被保存,确认?',
  ifBtn: true
}).then(_ => {
  this.goBack()
}).catch()  

写的有点多,其实还可以扩展出好多技巧,比如模态框中传一个完整的组件,并展示出来,简单地写一下,其实只需改动一点

import Model from './Model.vue'
const modelConstructor = Vue.extend(Model)
const modelViewStyle = (obj) => {
let component = obj.component
const modelViewInstance = new modelConstructor({
  data() {
    return {
      disabledClick: obj.stopClick // 是否禁止点击遮罩层关闭
    }
  }
})
let app = document.getElementById('container')
modelViewInstance.vm = modelViewInstance.$mount()
modelViewInstance.dom = modelViewInstance.vm.$el
app.appendChild(modelViewInstance.dom)
new Vue({
  el: '#__model__',
  mixins: [component],
  data() {
    return {
      serveObj: obj.obj
    }
  }
})
}

...

Vue.prototype.modelServe = {
  open: (obj) => {
    return new Promise(resolve => {
      modelViewStyle(obj, resolve)
      Vue.prototype.$receive = (data) => {
        resolve(data)
        close()
      }
    })
  }
}

调用:

sendCallBack() {
  this.modelServe.open({
    component: AddCallback,
    stopClick: true
  }).then(data => 
    if (data === 1) {
      this.addInit()
    } else {
      this.goBack()
    }
  })

这里我们用了mixins,最后最后再简单地介绍一下mixins,extend,extends的区别

**- Vue.extend使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

mixins 选项接受一个混入对象的数组。这些混入实例对象可以像正常的实例对象一样包含选项,他们将在 Vue.extend() 里最终选择使用相同的选项合并逻辑合并。举例:如果你的混入包含一个钩子而创建组件本身也有一个,两个函数将被调用。Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。

注意(data混入组件数据优先钩子函数将混合为一个数组,混入对象的钩子将在组件自身钩子之前调用,值为对象的选项,例如 methods, components 和 directives,将被混合为同一个对象。两个对象键名冲突时,取组件对象的键值对。)

extends 允许声明扩展另一个组件(可以是一个简单的选项对象或构造函数),而无需使用 Vue.extend。这主要是为了便于扩展单文件组件。这和 mixins 类似。**

概括

extend用于创建vue实例
mixins可以混入多个mixin,extends只能继承一个
mixins类似于面向切面的编程(AOP),extends类似于面向对象的编程
优先级Vue.extend>extends>mixins

总结

到这里,关于如何实现通过方法调用一个Vue组件内容以及用到的一些API以及原理就差不多了,代码如有不懂得地方可以随时提问,欢迎交流。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持易盾网络。

网友评论