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

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

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

接下来参考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填空

样式布局

以上是样式方面 关于居中方案提到的transform 涉及浏览器底层的原理想了解更多的可看网易云团队写的这篇 浏览器渲染魔法之合成层

组件构成

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

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

接下来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()的返回值就是当前实例
  2. new InstanceCtor().mount()是相当于(newInstanceCtor()).mount() 是相当于(new InstanceCtor()).mount()还是相当于new (InstanceCtor().$mount()) mdn上其实看解释也看不出来 自己想办法测下就可以知道是相当于第一种 本人js基础还是太菜
  3. 为什么插槽不是响应的 可以参考这篇结合源码 # 深入剖析Vue源码 – Vue插槽,你想了解的都在这里!

最后 基于对插槽的认识 浅析如果想实现自定义可编辑的表单弹窗也就是可传参配置 element实现是拿的createElement返回的vnode给到slots.default上了 其实可以直接穿render函数去实现 暂时有实现的方法但不太简洁等优化完再更新

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

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

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

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

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

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

古德拜

原文链接:https://juejin.cn/post/7212621497809403941 作者:前端Vegetable_dog

(0)
上一篇 2023年3月21日 下午3:55
下一篇 2023年3月21日 下午4:05

相关推荐

发表回复

登录后才能评论