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

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

自定义组件

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

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

<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。