快应用常用组件开发-图标和表单

快应用 Jul 23, 2019

承接上一篇构建组件库的文章,这篇将详细讲一讲组件的开发过程,和两个组件开发示例:图标和表单。

自定义组件

快应用的组件化做得是比较精简的,一个组件的实现和一个页面基本一致,自定义组件和页面的区别在于多了一些属性,少了一些生命周期的钩子。

多的属性是你可以向组件内传递数据的参数,可以用下面这种方式:

<template>
  <div class="child-demo">
    <text>{{ propOne.name }}</text>
  </div>
</template>
<script>
  export default {
    props: ['propOne']
  }
</script>

你可以把要传递进组件的属性在props这个数组中声明,不过要注意命名方式:声明属性要采用驼峰命名,在外部调用组件时要把驼峰转换成分隔线。以上面为例,调用组件时应该写成prop-one

调用方式如下所示:

<import name="comp" src="./component.ux"></import>
<template>
  <div class="parent-demo">
    <comp prop-one="{{obj}}"></comp>
  </div>
</template>
<script>
  export default {
    private: {
      obj: {
        name: 'child-demo'
      }
    }
  }
</script>

如果你要为传入的属性设置默认值,可以采取下面的方式声明:

<script>
  export default {
    props: {
      prop1: {
        default: 'Hello' //默认值
      },
      prop2Object: {} //不设置默认值
    },
    onInit() {
      console.info(`外部传递的数据:`, this.prop1, this.prop2Object)
    }
  }
</script>

图标组件开发和实现

了解了怎么写一个自定义组件,现在我们来实现一个图标库的组件,这在我们前端开发中是一个很常用的组件。

第一步:制作字体图标文件

制作字体图标文件有个很方便的在线工具:icomoon,网站的操作简便,容易上手,这里不重复说明。

第二步:准备好图标库组件的代码

我这里有写好的组件代码,大家拷贝过去修改一下字体图标的路径就可以直接使用,不过有几点需要注意的地方,在下面说明:

  1. 字体图标只能用于text标签,其他标签不会显示;
  2. 字体图标通过@font-face引入;
  3. 字体图标需要使用html实体来使用,这是因为快应用不支持伪类导致的,所以一定要保存从icomoon上下载下来的字体图标包和网站上图标库项目,不然没法看到图标对应的html实体值;

做好上面的几点,我们的字体图标组件基本就可用了,代码大概长成下面这样:

<template>
  <text if="{{type === 'search'}}" class="font-icon" style="font-size: {{size}}px;color: {{color}};">&#xe918;</text>
  <text if="{{type === 'home'}}" class="font-icon" style="font-size: {{size}}px;color: {{color}};">&#xe919;</text>
</template>
<style lang="less">
  @font-face {
    font-family: iconfont;
    src: url('iconfonts.ttf');
  }

  .font-icon {
    font-family: iconfont;
    text-align: center;
  }
</style>
<script>
    export default {
        props: {
            type: {
                default: 'home'
            },
            size: {
                default: 14
            },
            color: {
                default: ''
            }
        },
    }
</script>

第三步:优化调用方式

通过上面的方式,在icon变多的时候,会增加很多text标签,组件的代码会变得很多,也不利于维护和阅读,所以得想个办法,把代码优化一下,你可能很自然就会想到,让type变量直接接收传入的图标对应的html实体值不就好了。就像下面这样:

<template>
  <text class="font-icon" style="font-size: {{size}}px;color: {{color}};">&#xe918;</text>
</template>
<style lang="less">
  @font-face {
    font-family: iconfont;
    src: url('iconfonts.ttf');
  }

  .font-icon {
    font-family: iconfont;
    text-align: center;
  }
</style>
<script>
    export default {
        props: {
            type: {
                default: '&#xe918;'
            },
            size: {
                default: 14
            },
            color: {
                default: ''
            }
        },
    }
</script>

如果你把上面的代码跑起来,会发现图标根本不会显示,而且就算能显示,在调用的时候你还得去查一遍图标库,看看要用的图标对应的html实体值,代码的可阅读性也会有所降低,所以这时候,还得有一个骚操作,代码如下:

<template>
  <text class="font-icon" style="font-size: {{size}}px;color: {{color}};">{{ unescapeFontIconCode(iconMap[type]) }}
  </text>
</template>
<style lang="less">
  @font-face {
    font-family: iconfont;
    src: url('iconfonts.ttf');
  }

  .font-icon {
    font-family: iconfont;
    text-align: center;
  }
</style>
<script>
    export default {
        data() {
            return {
                iconMap: {
                    empty: '',
                    home: '&#xe918;'
                }
            }
        },
        props: {
            type: {
                default: 'empty'
            },
            size: {
                default: 14
            },
            color: {
                default: ''
            }
        },
        unescapeFontIconCode(iconCode = '') {
            return unescape(iconCode.replace(/&#x/g, '%u').replace(/;/g, ''))
        }
    }
</script>

上面代码中,我们新增了一个方法unescapeFontIconCode,这个方法返回unescape后的字符实体值,这样在text中,图标就能正常显示了;同时,我们还新增了一个iconMap变量,用来保存icon的字符实体值,同时给每个对应的图标取一个可读性更高的别名,方面我们的调用,也增加了在调用时代码的可读性,一眼就能看出是什么图标。

到此,图标组件我们就编写完了,当然你还可以根据自身业务开发的需求,为这个图标组件增加更多定制化的属性或者方法。

表单组件开发和实现

接下来,我们再做一个表单组件的开发,此处以单选框为例。单选框组件一般会多个配合使用,所以我们这个组件实际上可以拆分成两个不同功能的部分来实现:一部分是单个单选框组件,一部分是多个单选框组件同时使用的时候的group组件。下面一个一个来。

radio组件实现如下:

<template>
  <div class="apex-radio apex-radio-{{position}}" onclick="clickHandler">
    <div class="apex-radio-checked {{my_checked?'apex-radio-active':''}}"
         style="background-color: {{(my_checked||disabled)?my_color:''}}">
      <div class="apex-radio-icon {{disabled?'apex-radio-disabled':''}}" show="{{my_checked}}"></div>
    </div>
    <div class="apex-radio-content">
      <text>{{value}}</text>
    </div>
  </div>
</template>

<style lang="less">
  @import "../styles/base.less";

  .apex-radio {
    padding: 20px;
    background-color: #FFFFFF;
    height: 86px;

    &-checked {
      height: @checkbox-size;
      width: @checkbox-size;
      justify-content: center;
      border-radius: @checkbox-size;
      border: 2px solid @border-color-base;
      margin-right: 10px;
    }

    &-icon {
      width: @checkbox-size / 2;
      height: @checkbox-size / 3.6;
      border-style: solid;
      border-width: 0 0 2px 2px;
      transform: rotate(-45deg);
      margin-top: @checkbox-size / 3;
      border-color: #FFFFFF;
    }

    &-active {
      border-color: transparent;
    }

    &-disabled {
      border-color: @border-color-base;
    }

    &-right {
      flex-direction: row-reverse;
      justify-content: space-between;
    }
  }

</style>

<script>
    export default {
        data() {
            return {
                my_checked: this.checked,
                my_position: this.position,
                my_color: this.color
            };
        },
        props: {
            checked: {  // 是否选中
                default: false
            },
            disabled: { // 是否禁用
                default: false
            },
            position: { // 提示文本位置
                default: 'left' // left right
            },
            value: { // 单选的提示文本
                default: ''
            },
            color: {  // 单选框的颜色
                default: '#2d8cf0'
            },
            group: {  // 所属的单选框组的值,用于在多个单选框中只选中一个
                default: ''
            }
        },
        onInit() {
            this.$watch('disabled', 'handleDisabled'); // 监测并处理传入的disabled值的变化
            this.$watch('checked', 'changeChecked'); // 监测并处理传入的checked值的变化
        },
        changeChecked() { // 处理传入checked值变化后的的方法
          this.my_checked = this.checked;
        },
        changeCurrent(current) {
            this.my_checked = current;
        },
        clickHandler() { // 单选框被点击时的处理方法
            if (this.disabled) return;
            const item = {current: !this.checked, value: this.value};
            const parent = this.$parent().$child(this.group); // 检测是否在一个单选框组中
            parent ? parent.emitEvent(item) : this.$emit('change', item);
        },
        handleDisabled(e) { // 处理传入disabled值变化后的的方法
            if (e) {
                this.my_color = '#bbbec4'
            } else {
                this.my_color = this.color
            }
        }
    };
</script>

这个组件的实现需要注意的有一下几点:

  1. 需要单独在组件内监测某几个传入值的变化;
  2. 单选框点击事件内需要判断当前的组件是否处于一个单选框组(radio-group)中,这里用到的判断方法主要是这一句代码const parent = this.$parent().$child(this.group)

下面是radio-group组件的实现方法:

<template>
  <div id="radio-group" class="apex-radio-group">
    <slot></slot>
  </div>
</template>

<style lang="less">
  @import "../styles/base.less";

  .apex-radio-group {
    flex-direction: column;
  }

</style>

<script>
    export default {
        data() {
            return {};
        },
        props: {
            current: {
                default: ''
            },
        },
        onInit() {
            this.$watch('current', 'changeCurrent');
        },
        changeCurrent(val = this.current) {
            let items = this.$child('radio-group')._slot.childNodes[0].childNodes;
            const len = items.length;
            if (len > 0) {
                items.forEach(item => {
                    item._vm.changeCurrent(val === item._vm.value);
                });
            }
        },
        emitEvent(params) {
            this.$emit('change', params);
        }
    };
</script>

单选框组的实现相对单选框就简单多了,不过有个需要之一的地方就是,如何获取slot中的元素,从而判断哪一个单选框是当前选中的,同时也保障一个组内有且仅有一个处于选中状态,这里用了一些hack的办法实现,也是官方文档中没有说明的方式。

关键代码是这两句:

let items = this.$child('radio-group')._slot.childNodes[0].childNodes; // 获取当前组内的单选框组件

item._vm.changeCurrent(val === item._vm.value); // 访问组内单选框组件的方法

另外:目前在快应用中只支持一个slot标签。

尾声

以上讲解了快应用自定义组件的开发方法,以及分析了两个常用组件的代码实现,你也可以试着自己实现一遍,看有没有更好的方法,这两个组件都是快应用开源组件库apex-ui中的组件,如果你有更好的方法,欢迎提issue,pr。

vivo developer

快应用引擎、工具开发者、快应用生态拓展达人(vivo)。

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.