手写Vue弹窗Modal的实现代码


Posted in Javascript onSeptember 11, 2019

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

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

手写Vue弹窗Modal的实现代码

手写Vue弹窗Modal的实现代码

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

目标一

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

首先,我们来实现通知组件,相信这个大部分人都能写出来一个像模像样的组件,不??拢?苯由洗??/p>

<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把它插入文档中,所以我上面写的你应该明白了吧!

手写Vue弹窗Modal的实现代码

这是$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以及原理就差不多了,代码如有不懂得地方可以随时提问,欢迎交流。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript性能陷阱小结(附实例说明)
Dec 28 Javascript
5分钟理解JavaScript中this用法分享
Nov 09 Javascript
JavaScript DOM节点添加示例
Jul 16 Javascript
浅谈jQuery.easyui的datebox格式化时间
Jun 25 Javascript
js文件中直接alert()中文出来的是乱码的解决方法
Nov 01 Javascript
Bootstrap源码解读媒体对象、列表组和面板(10)
Dec 26 Javascript
原生JS实现$.param() 函数的方法
Aug 10 Javascript
详解Vue中的基本语法和常用指令
Jul 23 Javascript
茶余饭后聊聊Vue3.0响应式数据那些事儿
Oct 30 Javascript
javascript设计模式 ? 抽象工厂模式原理与应用实例分析
Apr 09 Javascript
vant 自定义 van-dropdown-item的用法
Aug 05 Javascript
VUE+Element实现增删改查的示例源码
Nov 23 Vue.js
js回溯法计算最佳旅行线路代码实例
Sep 11 #Javascript
layer更改皮肤的实现方法
Sep 11 #Javascript
node 解析图片二维码的内容代码实例
Sep 11 #Javascript
浅谈layer的Icon样式以及一些常用的layer窗口使用方法
Sep 11 #Javascript
如何解决日期函数new Date()浏览器兼容性问题
Sep 11 #Javascript
JS中封装axios来管控api的2种方式
Sep 11 #Javascript
浅谈Vue3.0之前你必须知道的TypeScript实战技巧
Sep 11 #Javascript
You might like
这部好评如潮的动漫 知名梗频出 但是画风劝退很多人
2020/03/08 日漫
德劲1107的电路分析与打磨
2021/03/02 无线电
php时间函数用法分析
2016/05/28 PHP
php 读取文件夹下所有图片、文件的实例
2018/10/17 PHP
laravel框架中控制器的创建和使用方法分析
2019/11/23 PHP
用jscript实现新建word文档
2007/06/15 Javascript
JavaScript对象模型-执行模型
2008/04/28 Javascript
IE8 浏览器Cookie的处理
2009/01/31 Javascript
JavaScript 类的定义和引用 JavaScript高级培训 自定义对象
2010/04/27 Javascript
JQuery操作tr和td内容的方法实例
2013/03/06 Javascript
jQuery弹性滑动导航菜单实现思路及代码
2013/05/02 Javascript
Jquery.Form 异步提交表单的简单实例
2014/03/03 Javascript
PHP使用方法重载实现动态创建属性的get和set方法
2014/11/17 Javascript
JavaScript实现基于十进制的四舍五入实例
2015/07/17 Javascript
js实现可控制左右方向的无缝滚动效果
2016/05/29 Javascript
JavaScript制作简单分页插件
2016/09/11 Javascript
基于javascript实现按圆形排列DIV元素(二)
2016/12/02 Javascript
vue.js获得当前元素的文字信息方法
2018/03/09 Javascript
vue项目中使用tinymce编辑器的步骤详解
2018/09/11 Javascript
Vue自定义指令结合阿里云OSS优化图片的实现方法
2019/11/12 Javascript
jQuery实现简易聊天框
2020/02/08 jQuery
vue实现带过渡效果的下拉菜单功能
2020/02/19 Javascript
在vue中封装的弹窗组件使用队列模式实现方法
2020/07/23 Javascript
[36:33]2018DOTA2亚洲邀请赛 4.3 突围赛 EG vs Newbee 第二场
2018/04/04 DOTA
python代码实现ID3决策树算法
2017/12/20 Python
Python走楼梯问题解决方法示例
2018/07/25 Python
python tkinter canvas使用实例
2019/11/04 Python
Python如何通过Flask-Mail发送电子邮件
2020/01/29 Python
纯HTML5+CSS3制作生日蛋糕(代码易懂)
2016/11/16 HTML / CSS
canvas实现烟花的示例代码
2020/01/16 HTML / CSS
高一新生军训感言
2014/03/02 职场文书
留学推荐信范文
2014/05/10 职场文书
集中采购方案
2014/06/10 职场文书
2015入党自荐书范文
2015/03/05 职场文书
2015年乡镇环保工作总结
2015/04/22 职场文书
nginx反向代理配置去除前缀案例教程
2021/07/26 Servers