记得有一次组内分享,以弹窗为例讲了如何创建可复用的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源码进行研究。
原创声明,本文系作者授权云+社区发表,未经许可,不得转载。
如有侵权,请联系 yunjia_community@tencent.com 删除。
我来说两句