上周工作中在用elementUI的mesagebox组件的自定义功能jsx写法时,发现只能做数据渲染无法做事件绑定数据响应。参阅elementUI源码后才发现这块是用插槽实现的,creatEelement生成的vnode被赋值到this.$slots.default 插槽的数据是没有响应的也就解释得通了。
实际业务开发过程中其实一般不需要去自己设计实现这种弹窗 除非有很必要的定制化要求 以及这个第三方组件的功能无法满足开发需要 自己去实现可以做一些功能扩展及改造
接下来参考elementUI的messagebox源码,使用vue2提供的extend api动态创建组件去实现的一个简易的全局弹窗(vue3就是createapp)
目录结构如图:
设计一个弹窗
按顺序首先是考虑样式布局
- 首先分header content bottom三部分 header放title content放展示的主体内容 bottom放按钮
- 然后是否需要有全屏遮罩层 有的话考虑层叠顺序及实现方式
- 是否居中 居中如何实现有哪些方案
- …等等等等
然后是组件实例的配置
- props
- events
- 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
}
}
看一看效果图
最后
coding过程中碰到些小的迷惑点 总结记录下
- 使用extend的时候如果写成instance = new InstanceCtor({…}).mount()的返回值就是当前实例
- new InstanceCtor().mount()还是相当于new (InstanceCtor().$mount()) mdn上其实看解释也看不出来 自己想办法测下就可以知道是相当于第一种 本人js基础还是太菜
- 为什么插槽不是响应的 可以参考这篇结合源码 # 深入剖析Vue源码 – Vue插槽,你想了解的都在这里!
最后 基于对插槽的认识 浅析如果想实现自定义可编辑的表单弹窗也就是可传参配置 element实现是拿的createElement返回的vnode给到slots.default上了 其实可以直接穿render函数去实现 暂时有实现的方法但不太简洁等优化完再更新
目前大致是这样的思路简单写下
对于弹窗还有很多可完善的地方比如加动画 皮肤可配置 可拖拽控制位置大小 抛出更多的事件 关闭之后到底应该销毁还是缓存 弹窗内容涉及到表单是否还要这么设计等等
本文随便写写记录下 记录的不太清晰 没什么重点 期待大佬评论指教
古德拜
原文链接:https://juejin.cn/post/7212621497809403941 作者:前端Vegetable_dog