手写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 相关文章推荐
10个实用的脚本代码工具
May 04 Javascript
jquery写个checkbox——类似邮箱全选功能
Mar 19 Javascript
基于jQuery实现模拟页面加载进度条
Apr 01 Javascript
JS中window.open全屏命令解析及使用示例
Dec 11 Javascript
jQuery实现的在线答题功能
Apr 12 Javascript
js点击列表文字对应该行显示背景颜色的实现代码
Aug 05 Javascript
Bootstrap3学习笔记(三)之表格
May 20 Javascript
ionic开发中点击input时键盘自动弹出
Dec 23 Javascript
Vue.js:使用Vue-Router 2实现路由功能介绍
Feb 22 Javascript
vue.js从安装到搭建过程详解
Mar 17 Javascript
vue.js实现点击后动态添加class及删除同级class的实现代码
Apr 04 Javascript
Js实现粘贴上传图片的原理及示例
Dec 09 Javascript
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
解析php中call_user_func_array的作用
2013/06/07 PHP
深入解析yii权限分级式访问控制的实现(非RBAC法)
2013/06/13 PHP
几道坑人的PHP面试题 试试看看你会不会也中招
2014/08/19 PHP
PHP中加密解密函数与DES加密解密实例
2014/10/17 PHP
php实现读取和写入tab分割的文件
2015/06/01 PHP
php查询及多条件查询
2017/02/26 PHP
PHP观察者模式示例【Laravel框架中有用到】
2018/06/15 PHP
php策略模式简单示例分析【区别于工厂模式】
2019/09/25 PHP
JQuery 浮动导航栏实现代码
2009/08/27 Javascript
Javascript学习笔记6 prototype的提出
2010/01/11 Javascript
JavaScript去掉数组中的重复元素
2011/01/13 Javascript
jQuery 对Select的操作备忘记录
2011/07/04 Javascript
jquery select动态加载选择(兼容各种浏览器)
2013/02/01 Javascript
Js,alert出现乱码问题的解决方法
2013/06/19 Javascript
jQuery动画效果-slideUp slideDown上下滑动示例代码
2013/08/28 Javascript
js实现浮动在网页右侧的简洁QQ在线客服代码
2015/09/04 Javascript
如何在AngularJs中调用第三方插件库
2017/05/21 Javascript
JS基于递归实现网页版计算器的方法分析
2017/12/20 Javascript
jQuery md5加密插件jQuery.md5.js用法示例
2018/08/24 jQuery
实例详解vue中的$root和$parent
2019/04/29 Javascript
JavaScript定时器设置、使用与倒计时案例详解
2019/07/08 Javascript
easyUI使用分页过滤器对数据进行分页操作实例分析
2020/06/01 Javascript
[05:36]DOTA2 2015国际邀请赛中国区预选赛第四日TOP10
2015/05/29 DOTA
在Django框架中编写Contact表单的教程
2015/07/17 Python
python开发之thread线程基础实例入门
2015/11/11 Python
Python文件时间操作步骤代码详解
2020/04/13 Python
python3爬虫中多线程进行解锁操作实例
2020/11/25 Python
如何使用 Flask 做一个评论系统
2020/11/27 Python
台湾饭店和机票预订网站:Expedia台湾
2016/08/05 全球购物
英国地毯卖家:The Rug Seller
2019/07/18 全球购物
《蒲公英》教学反思
2014/02/28 职场文书
奶茶店创业计划书
2014/08/14 职场文书
2019银行竞聘书
2019/06/21 职场文书
100句人生哲理语录集锦:强者征服今天,懒汉坐等明天
2019/10/18 职场文书
golang日志包logger的用法详解
2021/05/05 Golang
Android Flutter实现3D动画效果示例详解
2022/04/07 Java/Android