记得有一次组内分享,以弹窗为例讲了如何创建可复用的vue组件,后面发现这个例子并不恰当(bei tiao zhan),使用组件需要先import,再注册,然后再按照props in events out
原则使用,无论从流程或者使用方式来说都相当麻烦。
每个页面在使用弹窗时如果都按照这个流程走一遍的话,我们的脸基本上就黑了。
弹窗应该是插件,注册一次永久使用,如this.$alert('QQ音乐')
。下面我们就一起撸一个试试。
以下例子在vuetify.js的弹窗
v-dialog
组件基础上进行,这里查看完整demo源码。
// 引入插件
import dialogs from './plugins/dialogs';
// 安装
Vue.use(dialogs, {title: 'QQ音乐'});
new Vue({
el: '#app',
render: h => h(App)
})
是不是很眼熟,和vue-router用法一样,只要调用Vue.use()
,传入插件和初始化参数即可。重点就是传入的dialogs
到底是什么。
插件开发步骤在官方文档已经说得很清楚,可以看下。下面我们具体到dialogs这个插件上,来看看怎么实现。
// dialogs.js
import Dialog from '../components/Dialogs.vue';
const dialogs = {
install(Vue, options) {
Vue.prototype.$alert = (opt = {}) => {};
console.log('installed!');
}
};
export default dialogs;
要求很低,只要export的对象里有install
方法,其他的怎么折腾都可以。
调用Vue.use()
实际上就是调用install
方法,它会传入Vue对象和在use时传入的初始化参数{title: 'QQ音乐'}
。
可在install中添加全局/实例方法。
支持传入字符串,配置对象,支持指定回调函数,支持连续调用(用于二次确认)。
this.$alert('你好');
this.$confirm({
hideOverlay: false,
title: '我是弹窗',
content: '你好',
btnTxt: ['取消', '呵呵']
});
// 连环调用
this.$confirm({
content: '二次确认',
btnTxt: ['取消', '不要拦我'],
cb: (btnType) => {
if (btnType == 1) {
this.$confirm({
content: '三次确认',
btnTxt: ['好吧我放弃', '去意已决'],
cb: (btnType) => {
btnType == 1 && this.$alert('成功rm -rf /*');
}
});
}
}
});
Vue.prototype.$alert = (opt = {}) => {
...
// 创建包含组件的Vue子类
let Dialogs = Vue.extend(Dialog);
// 实例化,将组件放置在根DOM元素
let vm = new Dialogs({el: document.createElement('div')});
// 将上面实例使用的根DOM元素放到body中
document.body.appendChild(vm.$el);
// 保存当前弹窗实例
this.vm = vm;
...
// 以下代码与Dialogs.vue实现有关
// 显示弹窗组件
vm.show = true;
vm.$on('close', () => {
// 收到弹窗关闭事件时,移除根元素,并销毁实例
document.body.removeChild(vm.$el);
vm.$destroy();
this.vm = null;
});
};
从上面可以看到,$alert其实就是换了种方式调用组件,以下是Dialogs.vue的实现(对vuetify.js中的v-dialog
的进一步封装)。
show
和dialogShow
:组件显示隐藏type: 'alert' || 'confirm'
:弹窗类型(按钮个数)title
或slot name="title"
:标题content
或slot name="content"
:正文btnTxt
:按钮个数及文案closeDialog()
:按钮点击处理this.$emit('close', btnNo, this.type);
:触发弹窗关闭事件,并告知按钮编号组件的实现细节说明这里不过多展开。
<!-- Dialogs.vue -->
<template>
<v-dialog v-model="dialogShow" persistent :width="width" :hide-overlay="hideOverlay">
<v-card style="background:#fff;">
<v-card-title>
<div class="headline">
<template v-if="title">
{{title}}
</template>
<slot v-else name="title"></slot>
</div>
</v-card-title>
<v-card-text v-if="content" v-html="content"></v-card-text>
<slot v-else name="content"></slot>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
v-for="(item, idx) in btnTxt"
v-if="type == 'confirm' || (type == 'alert' && idx == 0)"
:key="idx"
class="green--text darken-1"
flat="flat"
@click.native="closeDialog(idx)"
>
{{item}}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
props: {
show: {
type: Boolean,
default: false
},
title: {
type: String,
default: '提示'
},
content: {
type: String,
default: ''
},
type: {
type: String,
default: 'alert'
},
btnTxt: {
type: Array,
default: function () {
return ['我知道了'];
}
},
width: {
type: Number,
default: 300
},
hideOverlay: {
type: Boolean,
default: false
}
},
data() {
return {
dialogShow: this.show
}
},
watch: {
show(showUp) {
this.dialogShow = showUp;
}
},
methods: {
closeDialog(btnNo) {
this.dialogShow = false;
this.$emit('close', btnNo);
}
}
}
</script>
下面看下从调用this.$alert(opt)
开始,怎样与默认参数结合,最终传递到Dialog.vue中去的。
Vue.prototype.$alert = (opt = {}) => {
...
// 默认参数
let defaultOpt = {
type: 'type',
title: 'QQ音乐',
content: '',
btnTxt: ['好的'],
width: 300,
cb: null
};
// 传入字符串时指定为content
if (typeof opt == 'string') {
defaultOpt.content = opt;
}
// 覆盖关系:调用参数 -> 插件安装时初始化参数 -> 默认参数
opt = {...defaultOpt, ...installOptions, ...opt};
let Dialogs = Vue.extend(Dialog);
let vm = new Dialogs({el: document.createElement('div')});
document.body.appendChild(vm.$el);
this.vm = vm;
// 最终传参给组件实例
Object.assign(vm, opt);
...
};
$alert
和$confirm
逻辑复用这两个弹窗其实就是type值不一样,因此将公共逻辑进行抽离复用。
// dialog.js
const dialog = {
vm: null,
create(componentType = 'alert', Vue, installOptions, opt) {
// 之前$alert的逻辑抽离到这里
},
install(Vue, options) {
Vue.prototype.$alert = (opt = {}) => {
this.create('alert', Vue, options, opt);
};
Vue.prototype.$confirm = (opt = {}) => {
this.create('confirm', Vue, options, opt);
};
}
};
之前的处理是:多次点击按钮时,销毁之前的弹窗。
这样就会造成其他弹窗干扰当前弹窗,当前弹窗会直接消失。
其实应该实现弹窗队列:同时多处调用弹窗方法,此时应该放进队列里,待当前弹窗消失后,再调取队列执行。
const dialogs = {
vm: null, // 保存当前实例
queue: [],
create(componentType = 'alert', Vue, installOptions, opt) {
// OUTDATE: 多次点击按钮时,销毁之前的弹窗
// UPDATE: 改为:当前弹窗未关闭再次调用时,保存到栈
if (this.vm) {
this.queue.push({type: componentType == 'confirm' ? '$confirm' : '$alert', opt});
return;
}
...
vm.$on('close', (btnType) => {
setTimeout(() => {
document.body.removeChild(vm.$el);
vm.$destroy();
typeof opt.cb == 'function' && opt.cb((componentType == 'confirm' && btnType == 1) ? 1 : 0);
this.vm = null;
// 查看栈中有无未执行的弹窗
if (this.queue.length > 0) {
let cur = this.queue.shift();
Vue.prototype[cur.type](cur.opt);
}
}, 400); // 缓出动画为300ms,因此延迟400ms后再销毁实例
});
}
}
vm.$on('close', (btnType) => {
setTimeout(() => {
document.body.removeChild(vm.$el);
vm.$destroy();
this.vm = null;
typeof opt.cb == 'function' && opt.cb((componentType == 'confirm' && btnType == 1) ? 1 : 0);
}, 400); // 缓出动画为300ms,因此延迟400ms后再销毁实例
});
实际上弹窗不应该只局限于在标题和正文中显示文字和html结构,如果想传入其他vue组件,实现一个上传文件的弹窗,像下面这样是不行的。
this.$confirm({
content: `
<v-flex xs10 offset-xs1 class="mr10">
<v-text-field
prepend-icon="attachment"
single-line
v-model="fileName"
:label="label"
required
readonly
ref="fileTextField"
@click.native="onFileInputFocus"
></v-text-field>
<input type="file" :style="{position:'absolute', left: '-9999px'}" :multiple="true" ref="fileInput" @change="onFileChange">
</v-flex>`
});
结果是会原封不动将未编译的vue组件标签直接塞入dom。
这个时候需要借助slot。
在上面的Dialogs.vue中,title和content是支持传入slot
。那么在插件中怎样传入slot?我们尝试在$alert
$confirm
基础上新增一个$uploadFile
方法。
Vue.prototype.$uploadFile = (opt = {}) => {
...
// 模板
const slotTemplate = `传入上例中content的内容`;
// 编译模板,返回渲染函数
const renderer = Vue.compile(slotTemplate);
const slotContent = {
data() {
return {uploadShow: false, fileName: '', label: '', formData: null}
},
methods: {
onFileChange($event) {
...
},
onFileInputFocus() {
...
},
getFormData(files) {
...
}
},
render: renderer.render,
staticRenderFns: renderer.staticRenderFns
};
// 将相关内容赋给名为content的slot
vm.$slots.content = [vm.$createElement(slotContent)];
}
然而在官方文档中看到
vm.$slots
是只读,这有点费解。
以上是对开发vue弹窗插件的梳理总结,vue的插件机制很强大,弹窗涉及的范围比较有限,有机会再对其他复杂插件开发以及vue源码进行研究。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。