假设我们有一个 Vue
单文件组件 Toast.vue
, 我们不想通过 <Toast />
的方式来使用它,而是想通过 this.$toast()
的方式来使用它。
那么也就是说 this.$toast()
触发时,我们需要手动创建一个 Toast
组件实例,并将其挂载到目标元素下。
如果我们想要实现上述功能,就需要用到 Vue.extend
。
但在此之前,我们需要了解 Vue
实例的 $el
和 $mount
方法。
16-1.$el
$el
是 Vue
实例的一个属性,表示当前 Vue
实例使用的根 DOM
元素。
有两个注意点:
- 如果
Vue
实例在实例化时没有声明el
选项,则它处于“未挂载”状态,vm.$el
为undefined
。 - 如果
Vue
实例在实例化没有声明el
选项,且调用vm.$mount()
方法时没有提供elementOrSelector
参数,则vm.$el
为undefined
。
16-2.$mount
基础语法如下:
js
vm.$mount([elementOrSelector])
实现一个组件实例的挂载,一共有以下 3
种方式:
在组件实例化时传入
el
选项。使用
vm.$mount([elementOrSelector])
手动地挂载一个未挂载的实例。如果没有提供
elementOrSelector
参数,模板将被渲染为文档之外的元素,那么我们可以使用原生DOM API
把它插入文档中。
js
// 1
const vm = new Vue({
el: '#app'
})
// 2
const vm = new Vue()
vm.$mount('#app')
// 3
const vm = new Vue()
vm.$mount()
document.body.appendChild(vm.$el)
16-3.Vue.extend
Vue.extend
用于创建一个 Vue
组件的子类。它返回的是一个扩展实例构造器。
基本语法如下:
js
const SubConstructor = Vue.extend(options)
const subInstance = new SubConstructor()
subInstance.$mount()
document.body.appendChild(subInstance.$el)
现在,以开头所述为例。假设 Toast.vue
组件的 vue
单文件内容如下:
vue
<template>
<transition name="fade" @after-leave="handleAfterLeave">
<div class="toast" v-show="visible">
<span>{{ message }}</span>
</div>
</transition>
</template>
<script>
export default {
name: 'Toast',
props: {
autoClose: {
type: Boolean,
default: true
},
duration: {
type: Number,
default: 3000
},
message: {
type: [String, Number],
default: ''
}
},
data () {
return {
visible: false
}
},
mounted () {
// 需要自动关闭时,调用startTimer
if (this.autoClose) this.startTimer()
},
beforeDestroy () {
this.stopTimer()
},
methods: {
startTimer () {
this.visible = true
if (this.duration > 0) {
this.timer = setTimeout(() => {
this.visible = false
// this.$nextTick(() => {
// this.$el.parentNode.removeChild(this.$el)
// })
}, this.duration)
}
},
stopTimer () {
if (this.timer) clearTimeout(this.timer)
},
// 动画结束之后 如果在定时器结束时removeChild,那么将不会触发leave动画
handleAfterLeave () {
// 触发 beforeDestroy 和 destroyed 的钩子
this.$destroy(true)
this.$el.parentNode.removeChild(this.$el)
}
}
}
</script>
<style lang='less' scoped>
.toast {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.85);
border-radius: 10px;
font-size: 14px;
color: #FFFFFF;
line-height: 14px;
padding: 16px 46px;
white-space: nowrap;
text-align: center;
}
.fade-enter, .fade-leave-to {
opacity: 0;
transform: translateY(calc(-50% - 50px)) translateX(-50%);
}
.fade-enter-to, .fade-leave {
opacity: 1;
transform: translateY(-50%) translateX(-50%);
}
.fade-enter-active, .fade-leave-active {
transition: all ease .5s;
}
@media screen and (max-width: 680px) {
border-radius: 4px;
font-size: 12px;
line-height: 12px;
padding: 14px 30px;
}
</style>
那么我们可以通过 Vue.extend
来创建一个 Toast
组件的子类,并通过函数手动挂载它:
js
import Vue from 'vue'
import Toast from './Toast'
Vue.prototype.$toast = (options = {}) => {
generateInstance(options)
}
const ToastConstructor = Vue.extend(Toast)
let zIndex = 9999
function generateInstance (options) {
options = typeof options === 'object' ? options : {
message: String(options)
}
const instance = new ToastConstructor({
propsData: options
})
const { appendToElement, top, left } = options
// 父级元素
const mountTarget = appendToElement || document.querySelector('body')
const { position } = window.getComputedStyle(mountTarget)
if (position !== 'fixed' && position !== 'absolute') {
mountTarget.style.position = 'relative'
}
// 挂载
instance.$mount()
const dom = instance.$el
// 支持更改定位
if (appendToElement) {
dom.style.position = 'absolute'
} else {
dom.style.position = 'fixed'
}
typeof top !== 'undefined' && (dom.style.top = top)
typeof left !== 'undefined' && (dom.style.top = left)
dom.style.zIndex = zIndex
zIndex++
// 插入
mountTarget.appendChild(dom)
return instance
}