通过vue.extend实现消息提示弹框

6,113 阅读3分钟

前提回顾

  • 在项目开发中我们经常使用的组件注册分为两种,一个是全局注册和另一个是局部注册,假设我们的业务场景是用户在浏览注册页面时,点击页面中的注册按钮后,前端根据用户的注册信息先做一次简单的验证,并根据验证弹出一个对应消息提示弹框
  • 我们拿到这个需求后,便开始着手准备要通过局部注册消息弹框组件的方法来实现这个场景,在通过局部注册消息弹框组件的方法解决完这个需求后,自然是沾沾自喜,紧接着又迎来了一个需求,该需求是用户在点击该注册按钮时,点击几次就要出现几次这个消息弹框,你开始犯了难,并思考难道我要在页面中提前插入n个组件标签,不然我怎么知道用户要点击几次注册按钮?
  • 在你还没有解决第二个需求的时候,又一个需求来了,第三个需求是不仅仅是注册页面需要用到这个消息弹框组件,在其他多个页面中也需要用到这个消息弹框组件。
  • 基于上述的业务需求,我们可以通过vue.extend编程式的使用组件,从而实现功能性的动态的消息提示弹框

局部注册消息弹框组件

  • 先通过局部注册的方法来实现消息弹框组件
  • 效果图如下:
  • 构造目录如下:
  • 'src/main.js'文件的代码:
import Vue from 'vue'
import App from './App.vue'
//全局引入样式文件
import  './assets/css.css';
Vue.config.productionTip = false

new Vue({
  render: h => h(App)
}).$mount('#app')
  • 'src/bus/bus.js'文件的代码:
import vue from 'vue';
var bus=new vue()
export default bus;
  • 'src/App.vue'文件的代码:
<template>
  <div id="app">
    <button @click="handleShowMessage">点击出现弹框</button>
    <TMessage :offsetTop='50'></TMessage>
    <TMessage :offsetTop='100'></TMessage>
    <TMessage :offsetTop='150'></TMessage>
    
    <!-- 我是不是得在这里埋下几万个消息弹框组件??? -->
    
  </div>
</template>

<script>
import TMessage from './components/TMessage/TMessage.vue';
import bus from './bus/bus';
export default {
  name:'app',
  data() {
    return {
      
    }
  },
  components: {
    TMessage,
  },
  methods: {
    handleShowMessage(){
      
      //打印查看消息弹框的组件对象
      console.log(TMessage);
      
      //点击按钮后出现消息弹框
      bus.$emit('showMessage')
    }
  },
}
</script>

<style>
#app {
  display: flex;
  justify-content: center;
}  

#app button{
  margin-top: 250px;
}
</style>
  • 'src/components/TMessage/TMessage.vue'文件的代码:
<template>
    <transition name="message-fade">
        <div :class="[
                        'message',
                        'message-' + type,
                        center ? 'is-center' : ''
                    ]"
            :style="{top: offset + 'px'}"
            v-if="!closed"
        >
            <p class="message-content">提示信息:{{message}}</p>
            <i class="icon icon-close"></i>
        </div>
    </transition>
</template>



<script>
    export default {
        name: 'TMessage',

        data() {
            return {
                message: '这是默认信息', //弹框的提示内容
                type: 'success',  //弹框的样式 success、warning、error
                center: true, //弹框是否居中显示
                offset: 20, //弹框默认的偏移量
                closed: true, //弹框默认隐藏 通过v-if="!closed"控制
                duration: 1000, //弹框消失的时间
                timer: null, //准备一个定时器
            }
        },

        mounted() {
            this.offset=this.offsetTop
            bus.$on('showMessage',()=>{
                this.closed=false;
                this.timer = setTimeout(() => {
                    //如果弹框是显示状态的话在duration后会变为隐藏状态
                    if (!this.closed) {
                        this.close();
                    }
                }, this.duration);
            })
        },
        props:['offsetTop'],

        methods: {
            close() {
                this.closed = true;
            }
        }
    }
</script>
  • 写到这里,我们实现的效果为(动图如下):
  • 'src/assets/css.css'文件的代码:
/*
样式重点解析:

1.'message'

2.'message-' + type:
    2.1:message-success
    2.2:message-warning
    2.3:message-error
    
3.'is-center' //决定了弹框居中显示

4.'message-fade-enter' //4和5决定了弹框的的过渡效果

5.'message-fade-leave-active'

6. .message {
		top:20px; //决定了弹框的偏移量
	}
*/
.message {
    min-width: 380px;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    border-radius: 4px;
    border-width: 1px;
    border-style: solid;
    border-color: #EBEEF5;
    position: fixed;
    left: 50%;
    top: 20px;
    z-index: 999999999;
    transform: translateX(-50%);
    background-color: #edf2fc;
    transition: opacity .3s, transform .4s, top .4s;
    overflow: hidden;
    padding: 15px 15px 15px 20px;
    display: flex;
    align-items: center
}

.message.is-center {
    -webkit-box-pack: center;
    -ms-flex-pack: center;
    justify-content: center
}

.message p {
    margin: 0
}

.message-info .message-content {
    color: #909399
}

.message-success {
    background-color: #f0f9eb;
    border-color: #e1f3d8
}

.message-success .message-content {
    color: #67C23A
}

.message-warning {
    background-color: #fdf6ec;
    border-color: #faecd8
}

.message-warning .message-content {
    color: #E6A23C
}

.message-error {
    background-color: #fef0f0;
    border-color: #fde2e2
}

.message-error .message-content {
    color: #F56C6C
}

.message-content {
    padding: 0;
    font-size: 14px;
    line-height: 1
}

.message-content:focus {
    outline-width: 0
}

.message .icon-close {
    position: absolute;
    top: 50%;
    right: 15px;
    -webkit-transform: translateY(-50%);
    transform: translateY(-50%);
    cursor: pointer;
    color: #C0C4CC;
    font-size: 16px
}

.message .icon-close:focus {
    outline-width: 0
}

.message .icon-close:hover {
    color: #909399
}

.message-fade-enter, .message-fade-leave-active {
    opacity: 0;
    transform: translate(-50%, -100%)
}

编程式的使用组件

  • 上方在通过局部注册消息弹框组件时体现的局限性:灵活性低、可复用性低、代码观感较差
  • 紧接着我们就要使用vue.extend来实现消息提示弹框,做到编程式的使用组件
  • 该构造目录为:
  • 'src/App.vue'文件的代码:
<template>
  <div id="app">
    <button @click="handleShowMessage">点击出现弹框</button>
  </div>
</template>

<script>
import Message from './components/TMessage/TMessage.js';
export default {
  name:'app',
  data() {
    return {
      
    }
  },
  methods: {
    handleShowMessage(){

      /**
       * 每点击一次按钮就调用一次该工厂函数
       * 每调用一次该工厂函数就创建一个弹框组件对象
       */
      return Message('我好帅啊我好帅啊我好帅啊')

    }
  },
}
</script>

<style>
#app {
  display: flex;
  justify-content: center;
}  

#app button{
  margin-top: 250px;
}
</style>
  • 'src/components/TMessage/TMessage.vue'文件的代码:
<template>
    <transition name="message-fade">
        <div :class="[
                        'message',
                        'message-' + type,
                        center ? 'is-center' : ''
                    ]"
            :style="{top: offset + 'px'}"
            v-if="!closed"
        >
            <p class="message-content">提示信息:{{message}}</p>
            <i class="icon icon-close"></i>
        </div>
    </transition>
</template>



<script>
    export default {
        name: 'TMessage',

        data() {
            return {
                message: '这是默认信息', //弹框的提示内容
                type: 'success',  //弹框的样式 success、warning、error
                center: true, //弹框是否居中显示
                offset: 20, //弹框默认的偏移量
                closed: false, //弹框默认隐藏 通过v-if="!closed"控制
                duration: 1000, //弹框消失的时间
                timer: null, //准备一个定时器
            }
        },

        mounted() {
            /*为了方便演示先不让弹框消失
            
            this.timer = setTimeout(() => {
                    //在规定的this.duration后该消息弹框消失
                    if (!this.closed) {
                        this.close();
                    }
                }, this.duration);
            */
        },

        methods: {
            close() {
                this.closed = true;
            }
        }
    }
</script>
  • 'src/components/TMessage/TMessage.js'文件的代码:
import Vue from 'vue';
import TMessage from "./TMessage.vue";


function Message(data) {
    data = data || {};

    if (typeof data === 'string') {
        data = {
            message: data
        }
    }

    const TMessageClass = Vue.extend(TMessage);
    
    //得到的是一个组件对象VueComponent实例
    //new TMessageClass接收的是一个包含组件选项的对象 覆盖
    let instance = new TMessageClass({
        data
    });

    instance.$mount();
    console.log(instance.$el,'现在才可以访问$el');
    /*	instance.$el的打印结果如下:
    
    <div class="message message-success is-center" style="top: 20px;">
    	<p class="message-content">提示信息:我好帅啊我好帅啊我好帅啊</p>
    	<i class="icon icon-close"></i>
    </div>
    */
    document.body.appendChild(instance.$el);
}
export default Message
  • 写到这里,我们来看一下效果,如下图:

解决消息弹框覆盖问题

  • 我们已经做到了每点击一次按钮就出现一个消息弹框组件,但是因为定位的问题出现了相互覆盖,所以得再接着去'TMessage.js'文件中去完善逻辑:
//file:'src/components/TMessage/TMessage.js'

/*
解决方法:
通过维护一个队列来存储每一个消息弹框组件对象
在每一次生成消息弹框组件时都需要重新计算其top值

通过该队列来计算上一个消息弹框组件对象的top值
*/


import Vue from 'vue';
import TMessage from "./TMessage.vue";

//装有instance消息弹框组件对象的容器
let instances = [];
function Message(data) {

    data = data || {};

    if (typeof data === 'string') {
        data = {
            message: data
        }
    }
    
    data.onClose = function() {
        console.log('onClose');
        // 每消失一个消息弹框就会触发一个onClose

        //instance是消息弹框组件的实例对象即VueComponent
        console.log(instance,'instance');
        
        //每消失一个就得把在instances容器中对应的该组件对象给删除掉
        Message.close(instance);
    };

    const TMessageClass = Vue.extend(TMessage);


    let instance = new TMessageClass({
        data
    });
    
    instance.$mount();
    document.body.appendChild(instance.$el);

 
    //如果data数据中有设置偏移量则使用该偏移量
    //否则使用默认的偏移量值20
    let offset = data.offset || 20;

    //规定每一个消息弹框的间隔
    //这里直接使用offset值做为间隔
    let offsetTop = offset;


    /**思路如下:
     * let offsetTop=20;
     * [].forEach(()=>{offsetTop+=10});
     * console.log(offsetTop) //还是20
     * 
     * [{a:'1'}].forEach(()=>{offsetTop+=10});
     * console.log(offsetTop) //才是30 
     */
     
    /*这里是在循环之后才去push
    因为生成的第一个消息弹框是不需要计算offsetTop的
    生成的第一个消息弹框直接使用offset值即可
    */   
    //从第一个起instances里有值了(组件对象)以后再去循环计算offsetTop值
    instances.forEach( item => {
    	//根据上一个计算的offsetTop+自身的高度+规定的间隔
        offsetTop += item.$el.offsetHeight + offset;
    });
    
    //当前生成的消息弹框的高度为offsetTop
    //offsetTop是根据上一个生成的消息弹框的三个值计算得到的
    //instances容器中第0个是不需要参与计算的即采用默认的offset值
    instance.$el.style.top = offsetTop + 'px';
    instances.push(instance);
}

Message.close = function(instance) {
    //每消失一个就得把在instances容器中对应的该组件对象给删除掉
    instances = instances.filter( item => item !== instance );
};
export default Message
  • 'src/components/TMessage/TMessage.vue'文件的代码:
//file:'src/components/TMessage/TMessage.vue'

<template>
    <transition name="message-fade">
        <div :class="[
                        'message',
                        'message-' + type,
                        center ? 'is-center' : ''
                    ]"
            :style="{top: offset + 'px'}"
            v-if="!closed"
        >
            <p class="message-content">提示信息:{{message}}</p>
            <i class="icon icon-close"></i>
        </div>
    </transition>
</template>

<script>
    export default {
        name: 'TMessage',

        data() {
            return {
                message: '这是默认信息', //弹框的提示内容
                type: 'success',  //弹框的样式 success、warning、error
                center: true, //弹框是否居中显示
                offset: 20, //弹框默认的偏移量
                closed: false, //弹框默认隐藏 通过v-if="!closed"控制
                duration: 1000, //弹框消失的时间
                timer: null, //准备一个定时器,
                onClose: null   //扩充一个功能 弹框消失后触发
            }
        },

        mounted() {
            //在规定的this.duration后该消息弹框消失
            //消息框消失后会触发this.close()函数方法
            this.timer = setTimeout(() => {
                    if (!this.closed) {
                        this.close();
                    }
                }, this.duration);
            
        },

        methods: {
            close() {
                this.closed = true;
                //如果该组件可以接收到this.onClose方法则调用该方法
                //该方法是在该消息弹框消失的时候被触发
                if (typeof this.onClose === 'function') {
                    this.onClose();
                }
            }
        }
    }
</script>
  • 写到这里,我们来看一下效果,如下图:

优化消息弹框消失的效果

  • 我们可以进一步的优化消息弹框消失的效果,效果图如下:
  • 'src/components/TMessage/TMessage.vue'文件的代码:
<template>
    <transition name="message-fade">
        <div :class="[
                        'message',
                        'message-' + type,
                        center ? 'is-center' : ''
                    ]"
            :style="{top: offset + 'px'}"
            v-if="!closed"
        >
            <p class="message-content">提示信息:{{message}}</p>
            <i class="icon icon-close"></i>
        </div>
    </transition>
</template>


<script>
    export default {
        name: 'TMessage',

        data() {
            return {
                message: '这是默认信息', 
                type: 'success',  
                center: true, 
                offset: 20, 
                closed: false, 
                duration: 1000, 
                timer: null,
                onClose: null  //扩充一个功能 弹框消失后触发
            }
        },

        mounted() {
            this.timer = setTimeout(() => {
                    if (!this.closed) {
                        this.close();
                    }
                }, this.duration);
            
        },

        methods: {
            close() {
                this.closed = true;
                //当弹框消失时会调用this.onClose()该函数方法
                if (typeof this.onClose === 'function') {
                    this.onClose();
                }
            }
        }
    }
</script>
  • 'src/components/TMessage/TMessage.js'文件的代码:
import Vue from 'vue';
import TMessage from "./TMessage.vue";

//装有instance的容器
let instances = [];
function Message(data) {

    data = data || {};

    if (typeof data === 'string') {
        data = {
            message: data
        }
    }


    const TMessageClass = Vue.extend(TMessage);


    let instance = new TMessageClass({
        data
    });

    instance.$mount();
    document.body.appendChild(instance.$el);

    data.onClose = function() {
        console.log('onClose');
        // 每消失一个弹框就会触发一个onClose方法
        Message.close(instance);
    };

    //如果data数据中有设置偏移量则使用该偏移量
    //否则使用默认的偏移量20
    let offset = data.offset || 20;

    //规定每一个消息弹框的间隔
    let offsetTop = offset;

    instances.forEach( item => {
        //上一个实例对象的offsetTop+自身的高度+规定的间隔
        offsetTop += item.$el.offsetHeight + offset;
    });
    instance.$el.style.top = offsetTop + 'px';
    instances.push(instance);


}
Message.close = function(instance) {
    /*
      每次弹窗关闭都会调用一次这个函数
    * 获取当前这个instance的高度
    * 把这个instance后面的所有实例的top减去这个高度,再减去偏移
    * */
    let removeHeight = instance.$el.offsetHeight + instance.offset;
    //把传递进来的instance在容器instances中删除
    let index = instances.findIndex( item => item === instance );
    instances = instances.filter( item => item !== instance );

    //对应的消息弹框消失后在该消息弹框后面的消息弹框会依次出现顶上来的效果
    //原理是找到对应的消息弹框在instances容器中的下标位置
    //通过循环改变对应的消息弹框后面的所有消息弹框的高度
    for (let i = index; i<instances.length; i++) {
        instances[i].$el.style.top = parseFloat(instances[i].$el.style.top) - removeHeight + 'px';
    }
};


export default Message
  • 'src/App.vue'文件的代码:
<template>
  <div id="app">
    <button @click="handleShowMessage">点击出现弹框</button>
  </div>
</template>

<script>
import Message from './components/TMessage/TMessage.js';
export default {
  name:'app',
  data() {
    return {
      
    }
  },
  methods: {
    handleShowMessage(){

      /**
       * 调用一次就创建一个弹框组件对象
       */
      return Message('我好帅啊我好帅啊我好帅啊')
    }
  },
}
</script>

<style>
#app {
  display: flex;
  justify-content: center;
}  

#app button{
  margin-top: 250px;
}
</style>

终极版实现版

  • 我们在上方'src/App.vue'文件中是通过引入TMessage.js后再通过Message()的方式调用使用该组件的,还可以将调用方式挂载到Vue全局上,来看看怎么操作:
  • 'src/main.js'文件的代码
import Vue from 'vue'
import App from './App.vue'
import  './assets/css.css';
import Message from '../src/components/TMessage/TMessage';
Vue.config.productionTip = false

//挂载到全局
Vue.prototype.$message = Message;

new Vue({
  render: h => h(App)
}).$mount('#app')
  • 'src/App.vue'文件的代码
<template>
  <div id="app">
    <button @click="handleShowMessage">点击出现弹框</button>
  </div>
</template>

<script>
export default {
  name:'app',
  data() {
    return {
      
    }
  },
  methods: {
    handleShowMessage(){
      this.$message.error('我好帅啊我好帅啊我好帅啊')
      this.$message.success('我好帅啊我好帅啊我好帅啊')
      this.$message.info('我好帅啊我好帅啊我好帅啊')
      this.$message.warning('我好帅啊我好帅啊我好帅啊')
    }
  },
}
</script>

<style>
#app {
  display: flex;
  justify-content: center;
}  

#app button{
  margin-top: 250px;
}
</style>
  • 'src/components/TMessage/TMessage.vue'文件的代码:
<template>
    <transition name="message-fade">
        <div :class="[
                        'message',
                        'message-' + type,
                        center ? 'is-center' : ''
                    ]"
            :style="{top: offset + 'px'}"
            v-if="!closed"
        >
            <p class="message-content">提示信息:{{message}}</p>
            <i class="icon icon-close"></i>
        </div>
    </transition>
</template>


<script>
    export default {
        name: 'TMessage',

        data() {
            return {
                message: '这是默认信息', 
                type: 'success',  
                center: true, 
                offset: 20, 
                closed: false, 
                duration: 1000, 
                timer: null,
                onClose: null  //扩充一个功能 弹框消失后触发
            }
        },

        mounted() {
            this.timer = setTimeout(() => {
                    if (!this.closed) {
                        this.close();
                    }
                }, this.duration);
            
        },

        methods: {
            close() {
                this.closed = true;
                //当弹框消失时会调用this.onClose()该函数方法
                if (typeof this.onClose === 'function') {
                    this.onClose();
                }
            }
        }
    }
</script>
  • 'src/components/TMessage/TMessage.js'文件的代码:
import Vue from 'vue';
import TMessage from "./TMessage.vue";


let instances = [];
function Message(data) {

    data = data || {};

    if (typeof data === 'string') {
        data = {
            message: data
        }
    }
    data.onClose = function() {
        console.log('onClose');


        //instance是消息弹框组件的实例对象即VueComponent
        Message.close(instance);
    };

    const TMessageClass = Vue.extend(TMessage);

    let instance = new TMessageClass({
        data
    });


    instance.$mount();
    // console.log(instance.$el,'现在才可以访问$el');
    document.body.appendChild(instance.$el);


    let offset = data.offset || 20;

    //规定每一个消息弹框的间隔
    let offsetTop = offset;
    
    //第一个弹框是不需要计算偏移量的
    //从第一个以后再去循环
    instances.forEach( item => {
        //上一个实例对象的offsetTop+自身的高度+规定的间隔
        offsetTop += item.$el.offsetHeight + offset;
    });
    instance.$el.style.top = offsetTop + 'px';
    instances.push(instance);


}
Message.close = function(instance) {
    let removeHeight = instance.$el.offsetHeight + instance.offset;
    let index = instances.findIndex( item => item === instance );
    instances = instances.filter( item => item !== instance );


    for (let i = index; i<instances.length; i++) {
        instances[i].$el.style.top = parseFloat(instances[i].$el.style.top) - removeHeight + 'px';
    }
};

['info', 'success', 'error', 'warning'].forEach( type => {
    Message[type] = function(data) {
        if (typeof data === 'string') {
            data = {
                message: data
            }
        }
        data.type = type;
        //整合data后再次去调用Message()
        return Message(data);
    };
} );


// Message.error=function(data){
//     if (typeof data === 'string') {
//         data = {
//             message: data
//         }
//     }
//     return Message({
//         ...data,
//         type:'error'
//     })
// }
export default Message
  • 完结撒花,最后来看一下效果图: