手写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中的对象和数组的应用技巧
Jan 07 Javascript
javascript获取作用在元素上面的样式属性代码
Sep 20 Javascript
jquery1.9 下检测浏览器类型和版本的方法
Dec 26 Javascript
jQuery中find()方法用法实例
Jan 07 Javascript
jQuery Easyui使用(二)之可折叠面板动态加载无效果的解决方法
Aug 17 Javascript
js获取Get值的方法
Sep 29 Javascript
node.js入门教程之querystring模块的使用方法
Feb 27 Javascript
原生js实现密码输入框值的显示隐藏
Jul 17 Javascript
js导出Excel表格超出26位英文字符的解决方法ES6
Nov 15 Javascript
Js 利用正则表达式和replace函数获取string中所有被匹配到的文本(推荐)
Oct 28 Javascript
使用异步controller与jQuery实现卷帘式分页
Jun 18 jQuery
selenium 反爬虫之跳过淘宝滑块验证功能的实现代码
Aug 27 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函数)
2006/10/09 PHP
php ctype函数中文翻译和示例
2014/03/21 PHP
javascript 利用Image对象实现的埋点(某处的点击数)统计
2012/12/28 Javascript
jquery插件开发注意事项小结
2013/06/04 Javascript
浅析Js中的单引号与双引号问题
2013/11/06 Javascript
利用jquery操作Radio方法小结
2014/10/20 Javascript
浅析javascript中函数声明和函数表达式的区别
2015/02/15 Javascript
Javascript闭包(Closure)详解
2015/05/05 Javascript
浅谈原生JS实现jQuery的animate()动画示例
2017/03/08 Javascript
jquery中$.fn和图片滚动效果实现的必备知识总结
2017/04/21 jQuery
node-sass安装失败的原因与解决方法
2017/09/04 Javascript
javascript自定义事件功能与用法实例分析
2017/11/08 Javascript
使用vux实现上拉刷新功能遇到的坑
2018/02/08 Javascript
用vue-cli开发vue时的代理设置方法
2018/09/20 Javascript
Vue.Draggable拖拽功能的配置使用方法
2020/07/29 Javascript
微信小程序表单验证插件WxValidate的二次封装功能(终极版)
2019/09/03 Javascript
layui中的switch开关实现方法
2019/09/03 Javascript
使用Vue 自定义文件选择器组件的实例代码
2020/03/04 Javascript
vue中使用腾讯云Im的示例
2020/10/23 Javascript
[01:02]2014 DOTA2国际邀请赛中国区预选赛 现场抢先看
2014/05/22 DOTA
[01:02]DOTA2上海特锦赛SHOWOPEN
2016/03/25 DOTA
[02:49]DOTA2完美大师赛首日观众采访
2017/11/23 DOTA
[02:00]DAC2018主宣传片——龙征四海,剑问东方
2018/03/20 DOTA
Flask框架WTForm表单用法示例
2018/07/20 Python
Python 音频生成器的实现示例
2019/12/24 Python
python GUI库图形界面开发之PyQt5信号与槽基本操作
2020/02/25 Python
Django 设置admin后台表和App(应用)为中文名的操作方法
2020/05/10 Python
python之随机数函数的实现示例
2020/12/30 Python
利用CSS3实现文字折纸效果实例代码
2018/07/10 HTML / CSS
2015年银行工作总结范文
2015/04/01 职场文书
工厂员工辞职信范文
2015/05/12 职场文书
国庆节新闻稿
2015/07/17 职场文书
获奖感言一句话
2015/07/31 职场文书
《索溪峪的野》教学反思
2016/02/19 职场文书
2016年幼儿园教师师德承诺书
2016/03/25 职场文书
Oracle删除归档日志及添加定时任务
2022/06/28 Oracle