Vue 中你不知道但却很实用的黑科技 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a Javascript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
Javascript 权威指南第 5 版
Closure: The Definitive Guide
Aresn
V2EX    Javascript

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

  •  
  •   Aresn 2016-12-05 09:50:51 +08:00 8779 次点击
    这是一个创建于 3311 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    写在前面

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

    目录

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

    递归组件

    递归组件在文档中有介绍,只要给组件指定一个 name字段,就可以在该组件递归地调用自己,例如:

    var iview = Vue.extend({ name: 'iview', template: '<div>' + // 递归地调用它自己 '<iview></iview>' + '</div>' }) 

    这种用法在业务中并不常见,在 iView 的级联选择组件中使用了该特性 (https://github.com/iview/iview/tree/master/src/components/cascader) 效果如下图所示: 图中每一列是一个组件(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 中都支持。

    自定义组件使用 v-model

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

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

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

    <my-component :data.sync="data"></my-component> 

    在 Vue 2.x 中,可以直接在自定义组件上使用 v-model了,比如:

    <my-component v-model="data"></my-component> 

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

    <template> <input type="text"> </template> 

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

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

    注:该方法是在 Vue 1.x 中的使用介绍,官方文档并没有给出该方法的任何说明,不可过多依赖此方法。 使用$compile()方法,可以在任何一个指定的上下文( Vue 实例)上手动编译组件,该方法在 iView 新发布的表格组件 Table 中有使用: https://github.com/iview/iview/tree/master/src/components/table/cell.vue 由于表格的列配置是通过一个 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 来管理。相关代码地址: https://github.com/iview/iview/tree/master/src/components/base/notification

    下面我们来看一下具体实现: 上图是最终效果图,这部分 .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 :

    https://github.com/iview/iview

    20 条回复    2017-02-18 15:41:53 +08:00
    maelon
        1
    maelon  
       2016-12-05 10:11:33 +08:00
    扩展 Vue 我们一般是通过 plugin 的方式:
    'use strict';

    const i18n = {
    install(Vue, options) {
    Vue.prototype.$lang = (options && options['lang']) || 'cn';
    Vue.prototype.$i18n = k => {
    if(typeof k === 'string') {
    const lang = (options && options['lang']) || 'cn';
    if(window.language_desc && typeof window.language_desc === 'object') {
    if(window.language_desc[k] && window.language_desc[k][lang] !== undefined) {
    return window.language_desc[k][lang];
    } else {
    return k;
    }
    }
    return k;
    }
    throw new Error('this.$i18n() receive a string parameter as key!');
    };
    }
    };

    export default i18n;

    便于代码管理
    soli
        2
    soli  
       2016-12-05 10:45:47 +08:00
    大赞!

    请问,思维导图是用啥工具画的哈?
    shyling
        3
    shyling  
       2016-12-05 10:59:14 +08:00
    噗,命了个名后高大上了好多
    wujunze
        4
    wujunze  
       2016-12-05 11:07:56 +08:00
    感谢分享 赞一个
    mactaew
        5
    mactaew  
       2016-12-05 11:09:29 +08:00
    很精致, Stared
    zhenjiachen
        6
    zhenjiachen  
       2016-12-05 11:09:46 +08:00
    star
    kiros
        7
    kiros  
       2016-12-05 11:15:16 +08:00
    同问思维导图工具。。。
    kiros
        8
    kiros  
       2016-12-05 11:20:12 +08:00
    @kiros @Aresn 同问思维导图工具。。。
    ss098
        9
    ss098  
       2016-12-05 11:26:39 +08:00
    非常酷。

    网站首页感觉好炫啊。
    kikyous
        10
    kikyous  
       2016-12-05 11:28:36 +08:00
    能不能把 Select 选择器分离出来,现在找到的几个都不太好用
    Aresn
        11
    Aresn  
    OP
       2016-12-05 11:32:11 +08:00   2
    @kiros @soli 思维导图 MindNode
    soli
        12
    soli  
       2016-12-05 11:34:26 +08:00
    @Aresn 多谢。

    希望尽快支持 Vue 2.0 。 加油!
    Aresn
        13
    Aresn  
    OP
       2016-12-05 11:36:02 +08:00
    @kikyous 暂时依赖的组件比较多,不便于独立。你可以研究下源码自己实现也行
    iRiven
        14
    iRiven  
       2016-12-05 13:02:38 +08:00
    虽然看不懂,但是感觉给个星就对了,好东西
    peneazy
        15
    peneazy  
       2016-12-05 13:28:34 +08:00
    mark 一下
    ragnaroks
        16
    ragnaroks  
       2016-12-05 14:44:15 +08:00
    先+1s,
    然后 https://www.iviewui.com/components/modal ,在空白处或动作按钮上多次点击会多次触发事件
    HustLiu
        17
    HustLiu  
       2016-12-05 15:04:17 +08:00
    其实有些并不是黑科技,大家不知道也完全是因为没有仔细阅读官方文档。。比如 v-model 的利用,文档里写的很明白,它只是 <tag :value="prop" @input="prop = arguments[0]">的语法糖了。。可惜大部分人(我身边就是有一大堆。。)根本不仔细看文档。。得亏了有 po 主这样的有心人总结罗列了。。
    markyun
        18
    markyun  
       2016-12-05 16:10:56 +08:00
    很赞,已经 fork 了 iview 源码在细看
    teledius
        19
    teledius  
       2016-12-05 16:34:07 +08:00
    大多数人看到还是基于 1.0 的组件就直接放弃了,迁移到 2.0 也不是什么大工程,希望尽快支持 2.0 吧 。 加油
    bombless
        20
    bombless  
       2017-02-18 15:41:53 +08:00
    为啥 template 要直接给 html 字符串呢……感觉好蛋疼
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3473 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 32ms UTC 10:42 PVG 18:42 LAX 02:42 JFK 05:42
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86