这个实现过程呢,会涉及render、Vue.extend、Vue插件等知识
分析需求
弹窗的特点:
- 位置不相对某个元素,而是相对于整个视窗,通常挂载于body,也就是要在vue的根实例app之外的。这样,不会影响别的内容的布局,也方便我们调整弹窗的位置
- 通过js创建的,不需要在任何组件中声明,即开即用型
大概是要这样的效果:
// 可定制弹窗的标题,内容,以及显示几秒,提示的类型,这个有点类似element的toast
this.$notice({
title: "自定义弹窗标题",
content: "弹窗内容",
duration: 1000,
type: "success" // 类似element组件,可选success, warning, error
})
实现过程
1. 先写要实现的组件Notice组件
<!-- Notice.vue -->
<template>
<div ref="notice" class="notice-wrap">
<h1 class="title">{{ title }}</h1>
<p class="content">
<span class="iconfont" :class="iconType"></span>
<span class="text">{{ content }}</span>
</p>
</div>
</template>
<script>
export default {
name: "notice",
components: {},
props: {
// 标题
title: {
type: String,
default: "提示",
},
// 提示内容
content: {
type: String,
default: "内容",
},
// 几秒后,关闭弹窗,默认1s
duration: {
type: Number,
default: 1000,
},
// 要显示的图标的类型:success, warning ,error
type: {
type:String,
default: 'success'
}
},
data() {
return {
isShow: false,
}
},
computed: {
// 这里的图标,我是用iconfont来实现,所以要先去官网把这个图下下来: https://www.iconfont.cn/collections/detail?spm=a313x.7781069.1998910419.d9df05512&cid=22664
// iconfont这里我就不详细介绍了,就是个图标,你不要这个,直接注释掉也不会影响你了解过程
iconType() {
if(this.type === 'success') {
return 'icon-success-filling'
}else if(this.type === 'warning'){
return 'icon-prompt-filling'
}else {
return 'icon-delete-filling'
}
}
},
methods: {}
};
</script>
<style scoped>
.notice-wrap {
/* height: 200px; */
width: 400px;
position: absolute;
top: 10%;
left: 30%;
border: 1px solid rgba(58,58,58, 0.2);
border-radius: 4px;
box-shadow: 10px 10px 5px #888888;
padding: 16px;
}
.title {
margin: 0;
font-size: 20px;
}
.content {
font-size: 14px;
}
.icon-success-filling{
color: green;
}
.icon-prompt-filling {
color: orange;
}
.icon-delete-filling {
color: red;
}
.text {
display: inline-block;
margin-left: 8px;
}
</style>
2. 将组件实例化,并转化成真实dom
组件的实例化
一说到实例化,学过面向对象的同学第一反应就是构造函数了。所以我们要先创建一个组件的构造函数,将我们的Notice.vue组件(配置对象)转化成一个虚拟节点。然后再将虚拟节点转化成真实dom,然后挂载到页面上。
构造函数的创建一般会有两种方法:
使用render渲染函数
使用render渲染函数,在我们可能还不知道怎么操作时,看看main.js文件中是怎么做的:
new Vue({
render: h => h(App),
}).$mount('#app')
其实它做这几件事:
- render函数中,h其实是createElement的意思,因其频繁使用,且在源码中被命名为h, 固我们也就都叫h。h的作用是将xxx.vue组件转化成了一个虚拟节点(VNode)。
- $mount的作用,是将VNode转化为真实Dom,并挂载在指定的真实节点中,这里,也就是挂载到App.vue组件中的id为app的div上,相当于:
js document.getElememntById("app").appendChild(this.$el)
- 如果$mount的函数不写任何参数(注意不能直接写body,官方说了不允许!),那么它依然会将VNode转化为真实Dom,但是不进行挂载追加。生成的dom呢,我们可以在它的实例对象$el获取
// notice/index.js
import Vue from 'vue'
import Notice from './Notice.vue'
function create(props) {
// 类似main.js中的用法
const vm = new Vue({
// props是传给Notice组件中的props属性
render: h => h(Notice, {props})
})
// 将vm转化成真实dom
vm.$mount()
// 将真实dom,挂载到body上
document.body.appendChild(vm.$el)
}
export default create;
使用Vue.extends
学习东西呢,老样子,官网先走一波,官网传送门: https://cn.vuejs.org/v2/api/index.html#Vue-extend
这里,白话解释一下,Vue.extend()其实就是用来创建组件的构造函数的,然后使用这个构造函数创建出Vue的虚拟节点Vnode
// notice/index.js
import Vue from 'vue'
import Notice from './Notice.vue'
function create(props) {
// 使用Vue.extend创建构造函数,MyComponent是自定义的vue组件(MyComponent.vue)
const NoticeConstrutor = Vue.extend(Notice)
// 构造函数的参数,propsData相当于我们组件MyComponent.vue里需要的props,这里为了和vue文件中的props冲突,所以官方取了个别名
// 然后实例化后,会生成一个vue组件对应的虚拟节点
const notice = new NoticeConstrutor({propsData:props})
// 有了实例后,最后和render一样,使用$mount进行挂载
notice.$mount()
// 将真实dom,挂载到body上,注意,这里的实例变成notice
document.body.appendChild(notice.$el)
}
export default create;
到了这一步,我们就可以实现一个弹窗的功能了,新建一个组件测试:
<!-- notice/test.vue -->
<script>
import create from '@/notice/index.js'
export default {
name: "Test",
mounted() {
create({
title: "测试弹窗",
content: "测试弹窗内容",
duration: 2000,
type: "error",
});
},
}
</script>
3. 销毁操作
我们弹窗设计,肯定某个动作后会触发,比如提交表单之类的,那么这个动作肯定也会不只一次触发,如果触发多次后,就会多次调用create方法后,如果不做销毁操作,就会一直往body上追加弹窗节点,这不是我们想看到的,所以做一下收尾工作了:
- 将弹窗的dom从body上销毁
- 将弹窗的实例对象销毁,释放内存
import Vue from 'vue'
import Notice from './Notice.vue'
function create(props) {
// 2. 使用Vue.extend的方法创建
const NoticeCons = Vue.extend(Notice)
const notice = new NoticeCons({propsData: props})
notice.$mount()
document.body.appendChild(notice.$el)
// 添加销毁操作
function remove() {
// 将真实dom节点干掉
document.body.removeChild(vm.$el)
// 将虚拟节点占的内存也释放掉
vm.$destroy()
}
// 在几秒后,进行销毁操作
if(props.duration) {
setTimeout(() => {
remove()
}, props.duration)
}
}
export default create;
这时候,再进行测试,就会发现弹窗2s后自动消失了
4. 使用插件的方式,将弹窗注入Vue原型上
弹窗肯定是不只一个地方会用到的,想想,如果我们在多个文件里要用到弹窗,是不是每次都得:
import create from '@/notice/index.js'
create({ //...
});
- 很麻烦,我们就想像element的弹窗一样,只使用this.$message就可以创建调用,这时候插件就派上用场了:
// notice/index
import Vue from 'vue'
import Notice from './Notice.vue'
function create(props) {
// 1. 使用render
// 类似main.js中的用法
// const vm = new Vue({
// render: h => h(Notice, {props})
// })
// // 将vm转化成真实dom
// vm.$mount()
// // 将真实dom,挂载到body上
// document.body.appendChild(vm.$el)
// console.log('vm.$el:', vm.$el);
// function remove() {
// // 将真实dom节点干掉
// document.body.removeChild(vm.$el)
// // 将虚拟节点占的内存也释放掉
// vm.$destroy()
// }
// 2. 使用Vue.extend
const NoticeCons = Vue.extend(Notice)
const notice = new NoticeCons({propsData: props})
notice.$mount()
document.body.appendChild(notice.$el)
// 将remove挂载到实例上,这样组件里,以后可以调用this.remove()来执行这个方法
notice.remove = function() {
// 将真实dom节点干掉
document.body.removeChild(notice.$el)
// 将虚拟节点占的内存也释放掉
notice.$destroy()
}
if(props.duration) {
setTimeout(() => {
notice.remove()
}, props.duration)
}
return notice;
}
// 插件走一波
const noticePlugin = {
install: function(Vue, options) {
// 将这个方法挂载到Vue.prototype.$notice上,就可以使用this.$notice来调用了
Vue.prototype.$notice = create
}
}
export default noticePlugin;
- 在入口文件main.js中调用插件:
// main.js
import Vue from 'vue'
// 导入插件
import noticePlugin from '@/notice/index'
// 使用插件
Vue.use(noticePlugin)
new Vue({
router, // 注意key是小写
store,
render: h => h(App),
}).$mount('#app')
- 这样,调用时就可以省了导入操作,直接使用this.$notice来调用
<!-- notice/test.vue -->
<script>
export default {
name: "Test",
mounted() {
this.$notice({
title: "测试弹窗",
content: "测试弹窗内容",
duration: 2000,
type: "error",
});
},
methods: {},
};
</script>