Vue中你不知道但却很实用的黑科技

本文纯技术干货,首发于 掘金,转载请注明出处和做者。
最近数月一直投身于 iView 的开源工做中,完成了大大小小 30 多个 UI 组件,在 Vue 组件化开发中积累了很多经验。其中也有不少带有技巧性和黑科技的组件,这些特性有的是 Vue 文档中提到但却容易被忽略的,有的更是没有写在文档里,今天就说说 Vue 组件的高级玩法。javascript

写在前面

本文所讲内容大多在 iView 项目中使用,你们能够前往关注,并结合源代码来研究其中的奥妙。项目地址:
github.com/iview/iviewhtml

目录

  • 递归组件
  • 自定义组件使用 v-model
  • 使用$compile()在指定上下文中手动编译组件
  • 内联模板inline-template
  • 隐式建立 Vue 实例

递归组件

递归组件在文档中有介绍,只要给组件指定一个 name字段,就能够在该组件递归地调用本身,例如:vue

var iview = Vue.extend({
  name: 'iview',
  template:
    '<div>' +
      // 递归地调用它本身
      '<iview></iview>' +
    '</div>'
})复制代码

这种用法在业务中并不常见,在 iView 的级联选择组件中使用了该特性
github.com/iview/iview…
效果以下图所示:
java


图中每一列是一个组件( caspanel.vue),一开始想到用 v-for来渲染列表,但后面发现扩展性极低,并且随着功能的丰富,实现起来很困难,处理的逻辑不少,因而改写成了递归组件:

<ul v-if="data && data.length" :class="[prefixCls + '-menu']">
    <Casitem v-for="item in data" :prefix-cls="prefixCls" :data.sync="item" :tmp-item="tmpItem" @click.stop="handleClickItem(item)" @mouseenter.stop="handleHoverItem(item)"></Casitem>
</ul><Caspanel v-if="sublist && sublist.length" :prefix-cls="prefixCls" :data.sync="sublist" :disabled="disabled" :trigger="trigger" :change-on-select="changeOnSelect"></Caspanel>复制代码

props 比较多,能够忽略,但其中关键的两个是datasublist,即当前列数据和子集的数据,由于预先不知道有多少下级,因此只需传递下级数据给组件自己,若是为空时,递归就结束了,Vue 这样设计的确很精妙。
注:该方法在 Vue 1.x 和 2.x 中都支持。android

自定义组件使用 v-model

咱们知道,v-model是在表单类元素上进行双向绑定时使用的,好比:webpack

<template>
    <input type="text" v-model="data">
    {{ data }}
</template>
<script> export default { data () { return { data: '' } } } </script>复制代码

这时data就是双向绑定的,输入的内容会实时显示在页面上。在 Vue 1.x 中,自定义组件可使用 props 的.sync双向绑定,好比:git

<my-component :data.sync="data"></my-component>复制代码

在 Vue 2.x 中,能够直接在自定义组件上使用 v-model了,好比:github

<my-component v-model="data"></my-component>复制代码

在组件my-component中,经过this.$emit('input')就能够改变data的值了。
虽然 Vue 1.x 中没法这样使用,可是若是你的组件的模板外层是 inputselecttextarea等支持绑定 v-model 特性的元素,也是可使用的,好比 my-component 的代码是:web

<template>
    <input type="text">
</template>复制代码

那也可使用上面2.x的写法。app

使用$compile()在指定上下文中手动编译组件

注:该方法是在 Vue 1.x 中的使用介绍,官方文档并无给出该方法的任何说明,不可过多依赖此方法。
使用$compile()方法,能够在任何一个指定的上下文(Vue实例)上手动编译组件,该方法在 iView 新发布的表格组件 Table 中有使用:
github.com/iview/iview…
因为表格的列配置是经过一个 Object 传入 props 的,所以不能像 slot 那样自动编译带有 Vue 代码的部分,由于传入的都是字符串,好比:

{
    render (row) {
        return `<i-button>${row.name}</i-button>`
    }
}复制代码

render函数最终返回一个字符串,里面含有一个自定义组件 i-button,若是直接用{{{ }}}显示,i-button 是不会被编译的,那为了实如今单元格内支持渲染自定义组件,就用到了$compile()方法。
好比咱们在组件的父级编译:

// 代码片断
const template = this.render(this.row);    // 经过上面的render函数获得字符串
const div = document.createElement('div');
div.innerHTML = template;
this.$parent.$compile(div);    // 在父级上下文编译组件
this.$el.appendChild(cell);    // 将编译后的html插入当前组件复制代码

这样一来, i-button就被编译了。
在某些时候使用$compile()确实能带来益处,不过也会遇到不少问题值得思考:

  • 这样编译容易把做用域搞混,因此要知道是在哪一个Vue实例上编译的;
  • 手动编译后,也须要在合适的时候使用$destroy()手动销毁;
  • 有时候容易重复编译,因此要记得保存当前编译实例的id,这里能够经过 Vue 组件的_uid来惟一标识(每一个Vue实例都会有一个递增的id,能够经过this._uid获取)

另外,Vue 1.x 文档也有提到另外一个$mount()方法,能够实现相似的效果,在 Vue 2.x 文档中,有 Vue.compile()方法,用于在render函数中编译模板字符串,读者能够结合来看。

内联模板inline-template

内联模板并非什么新鲜东西,文档中也有说明,只是平时几乎用不到,因此也容易忽略。简短解说,就是把组件的 slot 当作这个组件的模板来使用,这样更为灵活:

<!-- 父组件: -->
<my-component inline-template>
    {{ data }}
</my-component>

<!-- 子组件 -->
<script> export default { data () { return { data: '' } } } </script>复制代码

由于使用了 inline-template 内联模板,因此子组件不须要<template>来声明模板,这时它的模板直接是从 slot 来的{{ data }},而这个 data 所在的上下文,是子组件的,并非父组件的,因此,在使用内联模板时,最容易产生的误区就是混淆做用域。

隐式建立 Vue 实例

在 webpack 中,咱们都是用 .vue 单文件的模式来开发,每一个文件即一个组件,在须要的地方经过 components: {}来使用组件。
好比咱们须要一个提示框组件,可能会在父级中这样写:

<template>
    <Message>这是提示标题</Message>
</template>
<script> import Message from '../components/message.vue'; export default { components: { Message } } </script>复制代码

这样写没有任何问题,但从使用角度想,咱们其实并不指望这样来用,反而原生的window.alert('这是提示标题')这样使用起来更灵活,那这时不少人可能就用原生 JS 拼字符串写一个函数了,这也没问题,不过若是你的提示框组件比较复杂,并且多处复用,这种方法仍是不友好的,体现不到 Vue 的价值。
iView 在开发全局提示组件(Message)、通知提醒组件(Notice)、对话框组件(Modal)时,内部都是使用 Vue 来渲染,但倒是 JS 来隐式地建立这些实例,这样咱们就能够像Message.info('标题')这样使用,但其内部仍是经过 Vue 来管理。相关代码地址:
github.com/iview/iview…

下面咱们来看一下具体实现:


上图是最终效果图,这部分 .vue 代码比较简单,相信你们都能写出这样一个组件来,因此直接说建立实例的部分,先看下核心代码:

import Notification from './notification.vue';
import Vue from 'vue';
import { camelcaseToHyphen } from '../../../utils/assist';

Notification.newInstance = properties => {
    const _props = properties || {};

    let props = '';
    Object.keys(_props).forEach(prop => {
        props += ' :' + camelcaseToHyphen(prop) + '=' + prop;
    });

    const div = document.createElement('div');
    div.innerHTML = `<notification${props}></notification>`;
    document.body.appendChild(div);

    const notification = new Vue({
        el: div,
        data: _props,
        components: { Notification }
    }).$children[0];

    return {
        notice (noticeProps) {
            notification.add(noticeProps);
        },
        remove (key) {
            notification.close(key);
        },
        component: notification,
        destroy () {
            document.body.removeChild(div);
        }
    }
};

export default Notification;复制代码

与上文介绍的$compile()不一样的是,这种方法是在全局(body)直接使用 new Vue建立一个 Vue 实例,咱们只须要在入口处对外暴露几个 API 便可:

import Notification from '../base/notification';

const prefixCls = 'ivu-message';
const iconPrefixCls = 'ivu-icon';
const prefixKey = 'ivu_message_key_';

let defaultDuration = 1.5;
let top;
let messageInstance;
let key = 1;

const iconTypes = {
    'info': 'information-circled',
    'success': 'checkmark-circled',
    'warning': 'android-alert',
    'error': 'close-circled',
    'loading': 'load-c'
};

function getMessageInstance () {
    messageInstance = messageInstance || Notification.newInstance({
        prefixCls: prefixCls,
        style: {
            top: `${top}px`
        }
    });

    return messageInstance;
}

function notice (content, duration = defaultDuration, type, onClose) {
    if (!onClose) {
        onClose = function () {

        }
    }
    const iconType = iconTypes[type];

    // if loading
    const loadCls = type === 'loading' ? ' ivu-load-loop' : '';

    let instance = getMessageInstance();

    instance.notice({
        key: `${prefixKey}${key}`,
        duration: duration,
        style: {},
        transitionName: 'move-up',
        content: ` <div class="${prefixCls}-custom-content ${prefixCls}-${type}"> <i class="${iconPrefixCls} ${iconPrefixCls}-${iconType}${loadCls}"></i> <span>${content}</span> </div> `,
        onClose: onClose
    });

    // 用于手动消除
    return (function () {
        let target = key++;

        return function () {
            instance.remove(`${prefixKey}${target}`);
        }
    })();
}

export default {
    info (content, duration, onClose) {
        return notice(content, duration, 'info', onClose);
    },
    success (content, duration, onClose) {
        return notice(content, duration, 'success', onClose);
    },
    warning (content, duration, onClose) {
        return notice(content, duration, 'warning', onClose);
    },
    error (content, duration, onClose) {
        return notice(content, duration, 'error', onClose);
    },
    loading (content, duration, onClose) {
        return notice(content, duration, 'loading', onClose);
    },
    config (options) {
        if (options.top) {
            top = options.top;
        }
        if (options.duration) {
            defaultDuration = options.duration;
        }
    },
    destroy () {
        let instance = getMessageInstance();
        messageInstance = null;
        instance.destroy();
    }
}复制代码

到这里组件已经能够经过Message.info()直接调用了,不过咱们还能够在 Vue 上进行扩展:
Vue.prototype.$Message = Message;
这样咱们能够直接用this.$Message.info()来调用,就不用 import Message 了。

后记

Vue 组件开发中有不少有意思的技巧,用好了会减小不少没必要要的逻辑,用很差反而还弄巧成拙。在开发一个较复杂的组件时,必定要先对技术方案进行调研和设计,而后再编码。
iView 还有不少开发技巧和有意思的代码,后面有时间咱们再继续探讨吧,最近发布的几个版本都有较大的更新,但愿你们能够关注和推广 iView 😝:

github.com/iview/iview

相关文章
相关标签/搜索