乐趣区

面试被问到Vue想进一步提升那就停下来看一下吧

Vue 作为最近最炙手可热的前端框架,其简单的入门方式和功能强大的 API 是其优点。而同时因为其 API 的多样性和丰富性,所以他的很多开发方式就和一切基于组件的 React 不同,如果没有对 Vue 的 API(有一些甚至文档都没提到)有一个全面的了解,那么在开发和设计一个组件的时候有可能就会绕一个大圈子,所以我非常推荐各位在学习 Vue 的时候先要对 Vue 核心的所有 API 都有一个了解。这篇文章我会从实践出发,遇到一些知识点会顺带总结一下。
进入正题, 我相信不论什么项目几乎都会有一个必不可少的功能,就是用户操作反馈、或者提醒. 像这样(简单的一个 demo)

其实在 vue 的中大型项目中, 这些类似的小功能会更加丰富以及严谨,而在以 Vue 作为核心框架的前端项目中,因为 Vue 本身是一个组件化和虚拟 Dom 的框架,要实现一个通知组件的展示当然是非常简单的。但因为通知组件的使用特性,直接在模板当中书写组件并通过 v -show 或者 props 控制通知组件的显示显然是非常不方便的并且这样意味着你的代码结构要变,当各种各样的弹层变多的时候,我们都将其挂载到 APP 或者一个组件下显然不太合理,而且如果要在 action 或者其他非组件场景中要用到通知,那么纯组件模式的用法也无法实现。那么有没有办法即用到 Vue 组件化特性方便得实现一个通知组件的展现,那么我们可否用一个方法来控制弹层组件的显示和隐藏呢?

目标一
实现一个简单的反馈通知,可以通过方法在组件内直接调用。比如 Vue.$confirm({…obj})

首先,我们来实现通知组件,相信这个大部分人都能写出来一个像模像样的组件,不啰嗦,直接上代码

<template>
    <div
        :class="type"
        class="eqc-notifier">
        <i
            :class="iconClass"
            class="icon fl"/>
        <span>{{msg}}</span>
    <!-- <span class="close fr eqf-no" @click="close"></span> -->
    </div>
</template>

<script>
export default {
    name: 'Notification',
    props: {
        type: {
            type: String,
            default: ''
        },
        msg: {
            type: String,
            default: ''
        }
    },
    computed: {iconClass() {switch (this.type) {
                case 'success':
                    return 'eqf-info-f'
                case 'fail':
                    return 'eqf-no-f'
                case 'info':
                    return 'eqf-info-f'
                case 'warn':
                    return 'eqf-alert-f'
            }
        }
    },
    mounted() {setTimeout(() => this.close(), 4000)
    },
    methods: {close() {}}
}
</script>

<style lang="scss">
    .eqc-notifier {
        position: fixed;
        top: 68px;
        left: 50%;
        height: 36px;
        padding-right: 10px;
        line-height: 36px;
        box-shadow: 0 0 16px 0 rgba(0, 0, 0, 0.16);
        border-radius: 3px;
        background: #fff;
        z-index: 100; // 层级最高
        transform: translateX(-50%);
        animation: fade-in 0.3s;
    .icon {
        margin: 10px;
        font-size: 16px;
    }
    .close {
        margin: 8px;
        font-size: 20px;
        color: #666;
        transition: all 0.3s;
        cursor: pointer;
        &:hover {color: #ff296a;}
    }
    &.success {color: #1bc7b1;}
    &.fail {color: #ff296a;}
    &.info {color: #1593ff;}
    &.warn {color: #f89300;}
    &.close {animation: fade-out 0.3s;}
    }
</style>

在这里需要注意,我们定义了一个 close 方法,但内容是空的,虽然在模板上有用到,但是似乎没什么意义,在后面我们要扩展组件的时候我会讲到为什么要这么做。

创建完这个组件之后,我们就可以在模板中使用了 <notification type=”xxx” msg=”xxx” />

实现通过方法调用该通知组件
其实在实现通过方法调用之前,我们需要扩展一下这个组件,因为仅仅这些属性,并不够我们使用。在使用方法调用的时候,我们需要考虑一下几个问题:

  • 显示反馈的定位
  • 组件的出现和自动消失控制
  • 连续多次调用通知方法,如何排版多个通知

在这个前提下,我们需要扩展该组件,但是扩展的这些属性不能直接放在原组件内,因为这些可能会影响组件在模板内的使用,那怎么办呢?这时候我们就要用到 Vue 里面非常好用的一个 API,extend,通过他去继承原组件的属性并扩展他。

来看代码

import Notifier from './Notifier.vue'

function install(Vue) {
    Vue.notifier = Vue.prototype.notifier = {
        success,
        fail,
        info,
        warn
    }
}

function open(type, msg) {let UiNotifier = Vue.extend(Notifier)
    let vm = new UiNotifier({propsData: { type, msg},
        methods: {close: function () {
                let dialog = this.$el
                dialog.addEventListener('animationend', () => {document.body.removeChild(dialog)
                    this.$destroy()})
                dialog.className = `${this.type} eqc-notifier close`
                dialog = null
            }
        }
    }).$mount()
    document.body.appendChild(vm.$el)
}

function success(msg) {open('success', msg)
}

function fail(msg) {open('fail', msg)
}

function info(msg) {open('info', msg)
}

function warn(msg) {open('warn', msg)
}

Vue.use(install)

export default install

可以看到 close 方法在这里被实现了,那么为什么要在原组件上面加上那些方法的定义呢?因为需要在模板上绑定,而模板是无法 extend 的,只能覆盖,如果要覆盖重新实现,那扩展的意义就不是很大了。其实这里只是一个消息弹窗组件, 是可以在模板中就被实现, 还有插件怎么注入,大家都可以自己抉择。

同时在使用 extend 的时候要注意:

  • 方法和属性的定义是直接覆盖的

  • 生命周期方法类似余 mixin,会合并,也就是原组件和继承之后的组件都会被调用,原组件先调用

首先通过 let UiNotifier = Vue.extend(Notifier),我们得到了一个类似于 Vue 的子类,接着就可以通过 new UiNotifier({…options})的方式去创建 Vue 的实例了,同时通过该方式创建的实例,会有组件定义里面的所有属性。

在创建实例之后,vm.$mount()手动将组件挂载到 DOM 上面,这样我们可以不依赖 Vue 组件树来输出 DOM 片段,达到自由显示通知的效果。
扩展:
(
说一下 $mount,我们也许很多项目的主文件是这样的

new Vue({
    router,
    store,
    el: '#app',
    render: h => h(App)
})

其实 el 与 $mount 在使用效果上没有任何区别,都是为了将实例化后的 vue 挂载到指定的 dom 元素中。如果在实例化 vue 的时候指定 el,则该 vue 将会渲染在此 el 对应的 dom 中,反之,若没有指定 el,则 vue 实例会处于一种“未挂载”的状态,此时可以通过 $mount 来手动执行挂载。值得注意的是如果 $mount 没有提供参数,模板将被渲染为文档之外的的元素,并且你必须使用原生 DOM API 把它插入文档中,所以我上面写的你应该明白了吧!

这是 $mount 的一个源码片段, 其实 $mount 的方法支持传入 2 个参数的,第一个是 el,它表示挂载的元素,可以是字符串,也可以是 DOM 对象,如果是字符串在浏览器环境下会调用 query 方法转换成 DOM 对象的。第二个参数是和服务端渲染相关,在浏览器环境下不需要传第二个参数。

)

好了,我们现在其实就可以在组件中:

this.notifier[state](msg)来调用了, 是不是很方便?

进阶

我们刚才实现了在 Vue 中通过方法来进行用户反馈的提醒, 再增加一个难度:

我们 vue 项目中应该也遇到过这种情况,弹出一个对话框或是选择框?不但要求用方法弹出,并且能接收到对话框交互所返回的结果。
这里就不详细的分析了直接上代码说(之前的代码, 用 render 来写的组件,懒得改了,直接拿来用 …),先创建一个对话框组件 —Confirm.vue

<script>
    let __this = null
    export default {
        name: 'Confirm',
        data() {
            return {
                config: {
                    msg: '',
                    ifBtn: '',
                    top: null
                }
            }
        },
        created() {__this = this},
        methods: {createBox(h) {let config = {}
                config.attrs = {id: '__confirm'}
                let children = []
                children.push(this.createContainer(h))
                children.push(this.createBg(h))
                return h('div', config, children)
            },
            createBg(h) {
                return h('div', {
                    class: 'bg',
                    on: {click: __this.$cancel}
                })
            },
            createContainer(h) {let config = {}
                config.class = {'box-container': true}
                if (__this.config.top) {
                    config.style = {
                        'top': __this.config.top + 'px',
                        'transform': 'translate(-50%, 0)'
                    }
                }
                let children = []
                children.push(this.createContentBox(h))
                children.push(this.createClose(h))
                if (__this.config.ifBtn) {children.push(__this.createBtnBox(h))
                }
                return h('div', config, children)
            },
            createContentBox(h) {let config = {}
                config.class = {'content-box': true}
                return h('div', config, [__this.createContent(h)])
            },
            createContent(h) {let config = {}
                config.domProps = {innerHTML: __this.config.msg}
                return h('p', config)
            },
            createClose(h) {
                return h('i', {
                    class: 'eqf-no pointer close-btn',
                    on: {click: __this.$cancel}
                })
            },
            createBtnBox(h) {
                return h(
                    'div', {
                        class: {'btn-box': true}
                    }, [__this.createBtn(h, 'btn-cancel middle mr10', '取消', __this.$cancel),
                        __this.createBtn(h, 'btn-primary middle mr10', '确定', __this.$confirm)
                    ])
            },
            createBtn(h, styles, content, callBack) {
                return h('button', {
                    class: styles,
                    on: {click: callBack}
                }, content)
            }
        },
        render(h) {return this.createBox(h)
        }
    }
    </script>
    
    <style scoped>
    #__confirm {
        position: fixed;
        top: 0;
        left: 0;
        z-index: 10;
        width: 100%;
        height: 100%;
    }
    #__confirm .bg {
        position: fixed;
        top: 0;
        left: 0;
        z-index: 0;
        width: 100%;
        height: 100%;
    }
    #__confirm .box-container {
        position: absolute;
        width: 500px;
        padding: 20px;
        padding-top: 30px;
        border-radius: 3px;
        background: #fff;
        z-index: 1;
        box-shadow: 2px 2px 10px rgba(0,0,0,0.4);
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
    }
    #__confirm .content-box {
        font-size: 14px;
        line-height: 20px;
        margin-bottom: 10px;
    }
    #__confirm .btn-box {
        margin-top: 20px;
        text-align: right;
    }
    #__confirm .close-btn {
        position: absolute;
        top: 15px;
        right: 20px;
        font-size: 16px;
        color: #666666;
    }
    #__confirm .close-btn:hover {color: #1593FF;}
        #__confirm .bg {position: fixed;}
    </style>

然后创建 confirm.js

'use strict'
import Confirm from './Confirm.vue'
const confirmConstructor = Vue.extend(Confirm)

const ConfirmViewStyle = config => {
    const confirmInstance = new confirmConstructor({data() {
            return {config}
        }
    })
    confirmInstance.vm = confirmInstance.$mount()
    confirmInstance.dom = confirmInstance.vm.$el
    document.body.appendChild(confirmInstance.dom)
}

const close = () => {let dom = document.querySelector('body .modelServe-container')
    dom && dom.remove()
    Vue.prototype.$receive = null
}

const closeConfirm = () => {let dom = document.getElementById('__confirm')
    dom && dom.remove()
    Vue.prototype.$confirm = null
}

function install(Vue) {
    Vue.prototype.modelServe = {confirm: (obj) => {
            return new Promise(resolve => {Vue.prototype.$confirm = (data) => {resolve(data)
                    closeConfirm()}
                ConfirmViewStyle(obj)
            })
        }
    }
    Vue.prototype.$dismiss = close
    Vue.prototype.$cancel = closeConfirm
}
Vue.use(install)
export default install

思路很简单,在我们创建的时候同时返回一个 promise,同时将 resolve 通行证暴露给 vue 的一个全局方法也就是将控制权暴露给外部,这样我们就可以向这样,我上面的 confiram.vue 是直接把取消绑定成了 $cancel, 把确定绑定成了 $confirm, 所以点击确定会进入 full,也就是.then 中,当然你也可以传参数

this.modelServe.confirm({
    msg: '返回后数据不会被保存, 确认?',
    ifBtn: true
}).then(_ => {this.goBack()
}).catch()    

写的有点多,其实还可以扩展出好多技巧,比如模态框中传一个完整的组件,并展示出来,简单地写一下,其实只需改动一点

import Model from './Model.vue'
const modelConstructor = Vue.extend(Model)
const modelViewStyle = (obj) => {
let component = obj.component
const modelViewInstance = new modelConstructor({data() {
        return {disabledClick: obj.stopClick // 是否禁止点击遮罩层关闭}
    }
})
let app = document.getElementById('container')
modelViewInstance.vm = modelViewInstance.$mount()
modelViewInstance.dom = modelViewInstance.vm.$el
app.appendChild(modelViewInstance.dom)
new Vue({
    el: '#__model__',
    mixins: [component],
    data() {
        return {serveObj: obj.obj}
    }
})
}

...

Vue.prototype.modelServe = {open: (obj) => {
        return new Promise(resolve => {modelViewStyle(obj, resolve)
            Vue.prototype.$receive = (data) => {resolve(data)
                close()}
        })
    }
}

调用:

sendCallBack() {
    this.modelServe.open({
        component: AddCallback,
        stopClick: true
    }).then(data => 
        if (data === 1) {this.addInit()
        } else {this.goBack()
        }
    })

},

这里我们用了 mixins, 最后最后再简单地介绍一下 mixins,extend,extends 的区别

**- Vue.extend 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

  • mixins 选项接受一个混入对象的数组。这些混入实例对象可以像正常的实例对象一样包含选项,他们将在 Vue.extend() 里最终选择使用相同的选项合并逻辑合并。举例:如果你的混入包含一个钩子而创建组件本身也有一个,两个函数将被调用。Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。
    注意(data 混入组件数据优先钩子函数将混合为一个数组,混入对象的钩子将在组件自身钩子之前调用, 值为对象的选项,例如 methods, components 和 directives,将被混合为同一个对象。两个对象键名冲突时,取组件对象的键值对。)
  • extends 允许声明扩展另一个组件(可以是一个简单的选项对象或构造函数),而无需使用 Vue.extend。这主要是为了便于扩展单文件组件。这和 mixins 类似。**

概括

extend 用于创建 vue 实例
mixins 可以混入多个 mixin,extends 只能继承一个
mixins 类似于面向切面的编程(AOP),extends 类似于面向对象的编程
优先级 Vue.extend>extends>mixins

总结
到这里,关于如何实现通过方法调用一个 Vue 组件内容以及用到的一些 API 以及原理就差不多了,代码如有不懂得地方可以随时提问,欢迎交流。

退出移动版