前端组件开发心得

这篇文档总结了一些前端组件的开发心得,整理出了一个我们理想中的Best Practise。

它更多的是Vue相关的内容,如果你有其他库(React,Angular)的心得也可以发Merge Request来提交。

希望会对你的组件库开发有一定帮助。

样式篇

组件大小所见即所得

一般情况下,组件所能看到的大小即是它的真实大小。尽量不要在组件上使用看不见的边界,例如在外部加入margin样式。

这样做的好处是:从设计师的角度出发去设计你的代码,设计师给到的标注稿的间距标注一般不会考虑组件外部的透明边界。而组件的使用者(其他开发同学)可能不知道这样的边界的存在,就会造成一些返工。

所以为了提高组件的易用性,一般推荐组件所能看到的大小即是它的真实大小。

<template>
    <span class="box"></span>
    <span class="box"></span>
    <span class="box"></span>
</template>
<style>
.module-css .rule-1 {
    // 组件样式
    .rule-demo .box {
        display: inline-block;
        margin: 0;
        width: 40px;
        height: 40px;
        border: 1px solid #228ae6;
        background: #4dadf7;
    }

    // 使用组件时候的样式
    .rule-demo .box {
        margin-right: 10px;
    }
}
</style>

当然也会存在一些例外情况,比如说组件的鼠标响应区域比组件的实际展示区域要大。

这时候我们推荐和设计师做好充分地沟通,确认设计稿中的标注要考虑到这些隐藏边界,或者设计师根本不关心这些隐藏边界导致的几像素偏差。以简化组件使用者在使用此组件时候的难度。

<template>
    <span class="box"></span>
    <span class="box"></span>
    <span class="box"></span>
</template>
<style>
.module-css .rule-2 {
    // 组件样式
    .box {
        display: inline-block;
        margin: 0;
        padding: 5px;
    }
    .box::after {
        content: '';
        display: block;
        width: 40px;
        height: 40px;
        background: #4dadf7;
        border: 1px solid #228ae6;
    }
    .box:hover::after {
        background: #228ae6;
    }
    // 使用组件时候的样式
    .box {
        margin-right: 10px - 5px * 2;
    }
}
</style>

使用box-sizing

组件的根节点样式优先使用box-sizing:border-box;,这样在组件外部就可以轻松控制组件的宽高。不需要考虑根节点的borderpadding值。

<template>
    <span class="box">200x200</span>
</template>
<style>
.module-css .rule-3 {
    // 组件样式
    .rule-demo .box {
        display: inline-block;
        margin: 0;
        width: 50px;
        height: 50px;
        padding: 10px;
        vertical-align: middle;
        text-align: center;
        border: 1px solid #228ae6;
        background: #4dadf7;
        box-sizing: border-box;
    }

    // 使用组件时候的样式
    .rule-demo .box {
        width: 200px;
        height: 200px;
    }
}
</style>

避免CSS类选择器冲突

为了避免组件之间或组件与使用页面之间的CLASS类冲突,导致CSS样式的冲突。推荐使用BEMSUITSMACSS之类的CLASS命名规范。当然也可以自己规范一套命名规范。

使用BEM或SUIT规范时,推荐使用postcss-bem插件。它提供了@规则,可以简化CSS代码书写。

// BEM规范
@component-namespace v {
    @b input {
        // TODO
        @m search {
            // TODO
        }
        @e placeholder {
            // TODO
        }
    }
}

// 自定义规范
.input {
    // TODO
}
.input-search {
    // TODO
}
.input-placeholder {
    // TODO
}

使用currentColorcolor:inherit

如果组件颜色较为单一,可以使用currentColor来设置颜色。这样在组件外部就可以通过设置根节点的color属性来轻松控制组件的颜色。

如果需要设置颜色的节点不是根节点,还可以和color:inherit配合使用,继承根节点的color值。

<template>
    <span class="x-button">
        <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
        width="24px" height="24px" viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
            <g>
                <path d="M12,0C5.373,0,0,5.373,0,12s5.373,12,12,12s12-5.373,12-12S18.627,0,12,0z M12,22
                C6.477,22,2,17.521,2,12C2,6.477,6.477,2,12,2c5.523,0,10,4.478,10,10C22,17.521,17.523,22,12,22z"/>
                <path d="M12.5,6h-1C11.224,6,11,6.224,11,6.5v7c0,0.275,0.224,0.5,0.5,0.5h1c0.275,0,0.5-0.225,0.5-0.5v-7
                C13,6.224,12.775,6,12.5,6z"/>
                <path d="M12.5,16h-1c-0.276,0-0.5,0.225-0.5,0.5v1c0,0.275,0.224,0.5,0.5,0.5h1c0.275,0,0.5-0.225,0.5-0.5
                v-1C13,16.225,12.775,16,12.5,16z"/>
            </g>
        </svg>
        按钮
    </span>
</template>
<style>
// 组件样式
@component-namespace x {
    @b button {
        display: inline-block;
        margin: 0;
        padding: 9px 0;
        width: 80px;
        height: 40px;
        line-height: 20px;
        font-size: 14px;
        vertical-align: middle;
        text-align: center;
        color: #c92a2a;
        cursor: pointer;
        border: 1px solid currentColor;
        background: transparent;
        box-sizing: border-box;

        svg {
            color: inherit;
            fill: currentColor;
            width: 16px;
            height: 16px;
            display: inline-block;
            vertical-align: -3px;
        }
    }
}

.module-css .rule-5 {
    // 使用组件时候的样式
    .x-button {
        color: #4dadf7;

        &:hover {
            color: #228ae6;
        }
    }
}
</style>

组件尽量自适应高宽

组件的高宽很可能会被修改,此时如果组件内部是自适应变化的,那么就可以大大提高组件的易用性。组件的使用者在外部只需要通过CSS的widthheight就可以方便的修改组件的高宽,通常会配合box-sizing:border-box一起使用。

在开发组件时一定要把高宽变化的情况考虑在内,尽量做到组件内部自适应高宽。

<template>
    <span class="x-input x-input--search">
        <svg version="1.1" x="0px" y="0px"
        width="20px" height="20px" viewBox="0 0 20 20" xml:space="preserve">
        <path style="fill-rule:evenodd;clip-rule:evenodd;" d="M17.696,16.227l-2.531-2.532c2.5-2.748,2.431-6.999-0.223-9.653
        c-2.734-2.733-7.166-2.733-9.899,0c-2.734,2.734-2.734,7.166,0,9.9c2.306,2.306,5.819,2.659,8.503,1.073l2.681,2.682
        c0.406,0.406,1.063,0.406,1.469,0S18.102,16.633,17.696,16.227z M6.457,12.527c-1.953-1.952-1.953-5.118,0-7.071
        s5.118-1.953,7.071,0c1.952,1.953,1.952,5.119,0,7.071C11.575,14.48,8.409,14.48,6.457,12.527z"/>
        </svg>
        <input class="x-input__txt"></input>
    </span>
</template>
<style>
// 组件样式
@component-namespace x {
    @b input {
        display: inline-block;
        margin: 0;
        padding: 0 10px;
        width: 400px;
        height: 40px;
        line-height: 40px;
        vertical-align: middle;
        text-align: center;
        color: #495057;
        border: 2px solid currentColor;
        background: #FFF;
        box-sizing: border-box;

        @m search {
            padding-left: 40px;
        }

        svg {
            margin: 8px 0 0 -30px;
            float: left;
            width: 20px;
            height: 20px;
            fill: currentColor;
            color: inherit;
        }

        @e txt {
            display: block;
            width: 100%;
            height: 100%;
            padding: 0;
            border: 0;
            font-size: 20px;
            color: inherit;
            outline: none;
        }
    }
}

.module-css .rule-6 {
    // 使用组件时候的样式
    .x-input {
        width: 300px;
    }
}
</style>

单个组件内CSS REST

为了确保组件在各个页面下最终呈现的样子是一致的,需要对组件内CSS进行一定程度上的样式重置。

我们推荐的为根节点添加display样式,以确保其样式的稳定性。对于块级组件display值是block;对于行内组件值是inline-block;对于占位类型的组件值是inline;所谓的占位类型,就是其本身不提供任何样式,只是留一个标签在那里做占位。

CSS RESET可以参考右边例子。

.reset {
    display: inline-block;      // 或者 block,特殊情况下用inline
    margin: 0;
    padding: 0;
    border: 0;
    box-sizing: border-box;     // IE8+以后支持,在有`border`的时候外部可以更轻松的控制组件宽高
    font-size: 14px;            //
    line-height: 20px;          //
    outline: 0;                 // 不适用浏览器默认focus状态,可以自定义

    // 重置组件内部的常用标签
    div, span, object, iframe, h1, h2, h3, h4, h5, h6, p,
    pre, a, abbr, address, code, del, dfn, em, img,
    dl, dt, dd, ol, ul, li, fieldset, form, label,
    legend, caption, tbody, tfoot, thead, tr,
    // html5 标签
    article, aside, details, figcaption,
    figure, footer, header, hgroup, menu, nav,
    section, summary, main {
        // 盒模型重置
        margin: 0;
        padding: 0;
        border: 0;
        box-sizing: content-box;
        // focus状态
        outline: 0;
        // 文本
        font-weight: inherit;
        font-style: inherit;
        font-family: inherit;
        font-size: 100%;
        color: inherit;
        vertical-align: baseline;
    }

    // 设置成块级标签
    div, p, pre,
    iframe, audio, canvas, video,   // 本来不是块级元素,但大都作为块级元素使用
    h1, h2, h3, h4, h5, h6,
    dl, dt, dd, ol, ul, li,
    fieldset, form, legend,
    // html5 标签
    article, aside, details, figcaption,
    figure, footer, header, hgroup, menu, nav,
    section, summary, main {
        display: block;
        line-height: inherit;
    }
    ol, ul {
        list-style: none;
    }
    table {
        border-collapse: separate;
        border-spacing: 0;
        vertical-align: middle;
    }
    caption, th, td {
        text-align: inherit;
        font-weight: inherit;
        vertical-align: middle;
    }

    // 行内元素
    span, a, label,
    abbr, address, code, del, dfn, em {
        display: inline;
    }
    img, object, audio, canvas, video {
        display: inline-block;
    }
}

Javascript篇

使用constlet

ES6 现在支持了constlet关键字,相比var它们支持了块级作用域。并且const用于常量的声明,let用于变量的声明。它们本身有更加明确的用途,对代码的可读性也有一定的提高。

// 错误例子1
function getValueByCode(code) {
    var VALUE_MAP = {
        200: 'success',
        300: 'REDIRECT',
        400: 'CLIENT ERROR',
        500: 'SERVER ERROR'
    };
    return VALUE_MAP[code];
}
// 正确例子1
function getValueByCode(code) {
    const VALUE_MAP = {
        200: 'success',
        300: 'REDIRECT',
        400: 'CLIENT ERROR',
        500: 'SERVER ERROR'
    };
    return VALUE_MAP[code];
}


// 错误例子2
function getAncestor(component, ancestorName) {
    var parent = component.$parent;
    while (parent.$options.name !== ancestorName) {
        parent = parent.$parent;
    }
    return parent;
}
// 正确例子2
function getAncestor(component, ancestorName) {
    let parent = component.$parent;
    while (parent.$options.name !== ancestorName) {
        parent = parent.$parent;
    }
    return parent;
}

减少使用else关键字

我们在编写javascript代码时,会经常用到if else来处理分支逻辑。当分支逻辑只有两层时,这不是什么问题。但当分支逻辑越来越多时,就会出现很多层的if else嵌套。这会大大降低代码的可读性。

推荐按照如下规则来尽量减少else关键字的使用:

  • 如果代码逻辑有两个以上的分支
    • 如果代码逻辑简单只是返回对应值的匹配值,则推荐使用字典对象;
    • 如果只有一条分支逻辑负责其余都很简单,则推荐使用if return
    • 如果有两条以上分支逻辑复杂,推荐使用switch关键字(如果分支逻辑非常复杂推荐);
  • 如果代码逻辑只有两个分支
    • 如果分支逻辑简单,则优先尝试使用条件运算符;
    • 如果分支逻辑复杂,则推荐使用if return

PS:在代码逻辑只有两个分支并且使用if return时,推荐把简单的分支逻辑立即先return掉。把复杂的逻辑放在后面,这样复杂逻辑的缩进会少。代码的可读性会进一步提高。

// 错误样例1:两个分支,且逻辑简单
function hasValue(value) {
    if (value && (value.length != null)) {
        return !!value.length;
    }
    else {
        return !!value;
    }
}
// 正确样例1:使用条件运算符
function hasValue(value) {
    // 增加一个有意义的变量名,提高代码可读性
    const isStringOrArray = value && (value.length != null);
    return !!(isStringOrArray ? value.length : value);
}


// 错误样例2:两个以上分支,且逻辑简单
function getType(code) {
    if (code === 1) {
        return 'success';
    }
    else if (code === 2) {
        return 'client error';
    }
    else if (code === 3) {
        return 'server error';
    }
    else {
        return 'unknown error';
    }
}
// 正确样例2
function getType(code) {
    // 此样例中也可以使用数组来做map,不过对象支持更普遍一些
    const CODE_MAP = {
        1: 'success',
        2: 'client error',
        3: 'server error'
    };
    return CODE_MAP[code] || 'unknown error';
}


// 错误样例3:两个分支,且只有一个分支逻辑复杂
function getConfig(id, callback) {
    if (id != null) {
        const params = {
            // TODO calculate params
        };
        $.post('/config', params, function (data) {
            if (data.success) {
                callback(data.data);
            }
            else {
                callback(null);
            }
        });
    }
    else {
        callback(null);
    }
}
// 正确样例3
function getConfig(id, callback) {
    if (id == null) {
        callback(null);
        return;
    }

    const params = {
        // TODO calculate params
    };
    $.post('/config', params, function (data) {
        callback(data.success ? data.data : null);
    });
}


// 错误样例4:两个以上分支,且有多个分支逻辑复杂
function calc(position, relative) {
    if (position === 'top' || position === 'bottom') {
        calcVertical(relative);
    }
    else if (position === 'left' || position === 'right') {
        calcHorizontal(relative);
    }
    else {
        // TODO
    }
}
// 正确样例4
function calc(position, relative) {
    switch (position) {
        case 'top':
        case 'bottom':
            calcVertical(relative);
            break;
        case 'left':
        case 'right':
            calcHorizontal(relative);
            break;
        default:
            // TODO
            break;
    }
}

使用map/filter/reduce替代for循环

在ES5之前,我们操作Javascript数组的常用方式是使用for循环。这是一种传统且非常高效的做法。在ES5之后,Javascript的数组引入了很多的原生方法,例如Array.prototype.forEach方法。这些特性的支持程度可以看这个表

于是大家开始争论使用for循环还是forEach方法,甚至有人做了性能对比for循环的优势在于:性能好,并且能够通过break关键字快速退出。 forEach方法的优势在于:可读性高,易于使用(天然带有闭包)。

这里要说的不是for循环 VS forEach方法,而是for循环 VS map/filter/reduce方法。通过下面这张图你可以清楚地知道它们的作用。

虽然for循环有着更好的性能,但是只要不是超大型数组,性能优势并不明显。相反map/filter/reduce方法却可以大大提高代码的可读性,所以更推荐使用他们。

// 错误例子1
function getValues(opts) {
    const values = [];
    for (let i = 0; i < opts.length; i++) {
        values.push(opts[i].value);
    }
    return values;
}
// 正确例子1
function getValues(opts) {
    return opts.map((opt) => opt.value);
}


// 错误例子2
function getSelectedOptions(opts) {
    const selectedOpts = [];
    for (let i = 0; i < opts.length; i++) {
        if (opts[i].selected) {
            selectedOpts.push(opts[i]);
        }
    }
    return selectedOpts;
}
// 正确例子2
function getSelectedOptions(opts) {
    return opts.filter((opt) => opt.selected);
}


// 错误例子3
function sumSelectedValue(opts) {
    let sum = 0;
    for (let i = 0; i < opts.length; i++) {
        if (opts[i].selected) {
            sum += opts[i].value;
        }
    }
    return sum;
}
// 正确例子3
function sumSelectedValue(opts) {
    return opts
        .filter((opt) => opt.selected)
        .reduce((result, opt) => result + opt.value, 0);
}

Vue篇

使用v-bindv-on的快捷方法

Vue 为v-bindv-on指令提供了两个快捷方法,虽然这看起来和普通的HTML风格不一样,但是:@字符也是可以作为HTML属性名的。

推荐优先使用它们,这可以大大提高写代码的速度以及代码的可读性。

<!-- 错误例子1 -->
<a v-on:click="doSomething"></a>
<!-- 正确例子1 -->
<a @click="doSomething"></a>

<!-- 错误例子2 -->
<a v-bind:href="url"></a>
<!-- 正确例子2 -->
<a :href="url"></a>

简化props的类型

Vue组件的props是其最重要的对外API,推荐在设计这些props时尽量确保它们的类型简化,不要使用一个非常复杂的对象或者数组。尽量确保其props类型是字符串、数字、布尔值之类的基本类型,即使真的需要用到对象和数组,也可以尽量简化其内部项的类型。

这么设计有几个好处:

  1. 属性分离,可以使得props有更好的语义化、功能更加清晰、易于理解
  2. 基本类型的props可以直接在模板中作为字面量使用。但如果是对象和函数类型的字面量,那么因为模板每次都会重新生成props值,相当于每次渲染时得到的props是不同的,会导致watch回调函数的不必要触发

但是也并不是所有的组件都一定要使用基本类型的props,如果基本类型并不能满足需求或者并不是特别好用,那么还是推荐使用对象或数组类型。

举个例子,tab组件的可选tab数据更多是当前页面固定不变的,而tree的可选值数据更多的是动态输出(存储在后端)的。所以推荐tab组件的可选tab数据使用简单类型,而tree使用数组类型。

<!-- 正确例子 -->
<x-tabs value="3">
    <x-tab value="1">首页</x-tab>
    <x-tab value="2">动画</x-tab>
    <x-tab value="3">电影</x-tab>
    <x-tab value="4">电视剧</x-tab>
    <x-tab value="5">其他</x-tab>
</x-tabs>

<x-tree :value="selectedProductionId" :options="productionTree"></x-tree>

propsdata

Vue 的propsdata配置有点相似,但又完全不一样。props其实是组件对外部的属性接口,而data是组件内部的可变状态。

从Vue 2.0开始,props便不能通过this.prop = newValue;的赋值方式来修改(在debug模式下会报错),必须在组件外部通过属v-bind/v-model指令的方式由父组件传输过来新的值。而data则是可以在组件内外部来直接用Javascript赋值来修改的,不过很少会在组件外直接修改组件的data值。

所以我们已经可以得到如下几个结论:

  1. 如果这个状态不应该被组件外部修改,则只使用data
  2. 如果这个状态应该被组件外部修改
    1. 如果这个状态不会被内部修改(用户交互或其他原因导致),则只使用props
    2. 如果这个状态会被内部修改,则需要同时使用dataprops

因为propsdata不能同名,所以必须要区分一下。我们来看一个简单的输入框组件x-input的案例。

输入框的值即需要被外部组件控制修改,也同时需要能够让用户键盘输入来修改。所以定义了一个props名为value被外部使用,再定义一个currentValue作为data被内部修改。具体结构看下图:

绿色为组件实例,蓝色为props,黄色为data。初始化时currentValue的值会被赋值为value的值,并watch value值变化,如果外部修改inputValue则会立即更新currentValue。如果用户输入,则会更新currentValue并触发input事件然后通知inputValue新的值。

这里会有一个死循环在内部,但是由于Vue的watch机制是在值发生变化(===)时才触发回调,所以不存在问题。所以你需要确保一个状态不应该有多个不同的引用值。

<!-- x-container -->
<template>
    <x-input name="username" v-model="inputValue"></x-input>
    <div>{{inputValue}}</div>
</template>
<script>
    export default {
        name: 'XContainer',
        data() {
            return {
                inputValue: '输入框文字'
            };
        }
    }
</script>

<!-- x-input -->
<template>
    <span class="x-input">
        <input class="x-input__txt" :name="name" v-model="currentValue"></input>
    </span>
</template>
<script>
    export default {
        name: 'XInput',
        props: {
            name: String,
            value: [String, Number]
        },
        data() {
            return {
                currentValue: this.value
            };
        },
        watch: {
            value(nextValue) {
                this.currentValue = nextValue;
            },
            currentValue(nextValue) {
                this.$emit('input', nextValue);
            }
        }
    }
</script>

适度且灵活地使用模板的行内表达式

Vue 内置的模板很强大,设置提供了行内表达式功能。这让过去很麻烦难以处理的情况变得很简单,比如获取列表内点击行的数据。

过去我们需要在渲染列表每一行的时候,把对应数据项的ID或者索引值作为li元素的data-id属性的值;然后在ul元素上绑定点击事件,通过事件代理拿到当前点击的li元素;然后获取li元素的data-id值,然后得到对应的数据项。

而使用Vue之后,只需要通过行内表达式就可以轻易地做到,见右边。

虽然行内表达很强大,但是并不推荐在表达式内写太复杂的逻辑。因为它可读性差,且难以复用。

如果只是用来作为显示,则更推荐使用computed;如果是用来做复杂的执行,则推荐把值传给methods的方式;

<template>
    <ul>
        <li v-for="item in list" @click="clickedItem = item">
            {{item.name}}
        </li>
    </ul>

    <div>Clicked item name: {{clickedItem.name || ''}}</div>
</template>
<script>
    export default {
        data() {
            return {
                list: [
                    {name: 'Vue'},
                    {name: 'Angular'},
                    {name: 'React'},
                    {name: 'jQuery'}
                ],
                clickedItem: {}
            };
        }
    }
</script>

尽量少使用$parent

在 Vue 中,父子组件的数据传输主要依靠属性的由上向下传输,也就是所谓的单向数据流。当子组件想要向父组件传输数据时,一般使用事件。

你可能会想到v-model或者v-bind.sync(2.0开时移除但2.3.0+重新增加)的双向绑定,其实它的内部实现机制也是依赖了单向数据流和事件,只是提供了一个简单的快捷方式而已。

当然还有另一个自下向上传输数据的方式,就是$parent。但是应该尽量减少它的使用,如果使用了$parent则要考虑到不包含指定$parent的情况。

主要原因是:一个组件应该能独立运行,不应该过度依赖其父组件。这会导致其不能在其他更为复杂的上下文中不能使用。例如父子组件之间插入了另一个其他组件,见右边代码。

不过这个情况也并不绝对,比如右边的x-formx-form-item。这两个组件是互相配合使用的,x-form-item不需要支持独立使用。但是我们也并不一定要使用$parent,更推荐使用provide && inject机制。

<!-- 正常例子 -->
<x-form>
    <x-form-item name="username">
        <x-input value="0067ED"></x-input>
    </x-form-item>
    <x-form-item name="password">
        <x-input password></x-input>
    </x-form-item>
    <x-form-item>
        <button type="submit">登录</button>
    </x-form-item>
</x-form>


<!-- 异常例子 -->
<x-form>
    <!-- 加入了水平布局的组件,导致了父子级关系改变 -->
    <x-layout align="horizontal">
        <x-form-item name="username">
            <x-input value="0067ED"></x-input>
        </x-form-item>
    </x-layout>
    <x-layout align="horizontal">
        <x-form-item name="password">
            <x-input password></x-input>
        </x-form-item>
    </x-layout>
    <x-layout align="horizontal">
        <x-form-item>
            <button type="submit">登录</button>
        </x-form-item>
    </x-layout>
</x-form>

provideinject 实现上下数据传输

在Vue中,直接的父子组件可以通过属性和事件机制轻松地实现数据传输。但是属性和事件机制并不能覆盖到所有的情况:

  • 祖孙关系的组件
  • 父组件传输数据给slot中的子组件

为了解决这些场景的数据传输问题。Vue自2.2.0开始,新增了provideinject配置。通过它们可以轻松实现上下级关系的组件之间的数据传输。它的机制与React的context类似。

上级组件通过provide配置暴露其指定名称的数据,下级组件通过inject配置注入其同名数据。由于provide配置支持函数类型,所以甚至可以将上级组件自己的引用传给下级组件。

provideinject配置相对隐晦,且名称容易冲突。所以一般只用在组件开发中使用,不建议在业务逻辑开发中使用。

如果浏览器支持ES6(SymbolReflect.ownKeys),那么可以使用Symbol作为名称来避免冲突。

var Ancestor = {
    provide() {
        return {
            specialAncestor: this
        };
    },
    // ...
};

var Child = {
    inject: [specialAncestor],
    created() {
        console.log(this.specialAncestor);
    }
};

非父子组件的数据传输

之前两条tips中已经提到了有上下级关系的组件之间的数据传输,那么如果是兄弟组件呢?这种情况下由于没有上下级关系,所以不能通过属性、事件或者provide && inject来传输数据。

一般有两种方法来解决非上下级关系的组件之间的数据传输。第一种是使用组件共同的祖先组件来实现数据共享。还有一种方法是使用类似Flux的架构来管理和实现全局的store。例如Vuex是一个不错的选择。

嵌套组件之scopedSlot传递

使用scoped slot能在插槽里自定义模板并且在模板中使用组件传递过来的context。但是在组件开发的过程我们往往会在大组件中嵌套使用小组件,这样就存在把最外层的scoped slot层层传递到最内层的的情况(n >= 2)。

那么问题来了,在文档中并没有找到使用template方式传递scoped slot的介绍和例子。但是我们找到这句话:“ Vue 推荐在绝大多数情况下使用 template 来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力,这就是 render 函数,它比 template 更接近编译器。” 于是我们可以把scoped slot作为createElement方法的第二参数(data object)的一个属性传递到子组件中。

// Scoped slots in the form of // { name: props => VNode | Array } scopedSlots: { default: props => h('span', props.text) },

只是render函数的缺点是不灵活,特别是在定义你的组件的dom结构模板的时候,如果写很多 render 函数,可能会觉得痛苦。它比较适用于外层组件仅仅是对内层组件的一次逻辑封装,而渲染的模板结构变化扩展不多的情况。

还好我们还有最后一把杀手锏--JSX。它可以让我们回到于更接近模板的语法上。 举一个栗子,我们要开发一个下拉选择组件(select),其中下来出来的列表(select-list)是一个子组件(它可以被应用到其他组件中)。 而列表的每一行被select-list组件作为了scoped slot,这样用户就能自定义这个列表的每一行如何组织长什么样。简化的template(代码片段1):

然后在写select组件时我们通过JSX在其render函数中这样定义模板(代码片段2):

关键点:

  • 在子组件的标签上通过scopedSlots属性可以向其传递自己的scoped slot;
  • 自身的scoped slot可以通过this.$scopedSlots对象获取,默认就是default,具名slot就是它的名字。本例为listItem
  • 如果不在标签上传递而是需要使用表达式传递,也可以通过this.$scopedSlots对象。并且一个具体的scoped slot对象其实就是一个函数,其内部的scope可以在参数中传入。比如本例中的this.$scopedSlots.headItem(this.current)

与普通template的不同

  • directives 参见本例代码
  • v-model 通过value属性传递值,并通过绑定input事件来响应变化。
  • v-if、v-for 本例中使用三目表达式来实现v-if

这就是深入底层要付出的,尽管麻烦了一些,但你可以更灵活地控制。

<!--代码片段1:-->
<template>
<ul>
    <li v-for="(item, index) in options">
        <slot name="item" :item="item">
            <span>{{ item.text }}</span>
        </slot>
    </li>
</ul>
</template>

<!--代码片段2:-->
<script>
render(h) {
    let directives = [{
        name: 'popper',
        arg: 'selector',
        modifiers: {click: true}
    }];

    return (
        <div
            class={this.calcClazz}>
            <v-popper>
                <v-select-list
                    options={this.options}
                    index={this.currentIndex}
                    value={this.currentValue}
                    onInput={this.handleListInput}
                    scopedSlots={{item: this.$scopedSlots.item}} >
                </v-select-list>
            </v-popper>
            <div
                class="v-select__header"
                { ...{directives} }>
                {
                    this.$scopedSlots.header
                        ? this.$scopedSlots.header({item: this.current})
                        : (<span>{this.placeholder}</span>)
                }
                <v-icon
                    class="v-select__header-arrow"
                    name="v-arrow_dropdown">
                </v-icon>
            </div>
        </div>
    );
}
</script>