用vue优雅地编写UI组件的几条指导原则

·  阅读 2542

前言

最近在尝试写几个UI组件,并通过阅读element-ui的源码,与其反复比较,然后认真思考,最后总结出一些自己的一些心得和体会。在造轮子的过程中,既巩固了html,css,js基础,又加深了对vue源码的理解,更重要的是给了我一个温习和实践所学过的设计模式和思想的机会,来编写更加优雅的代码。
本文会介绍一些自己总结的,关于如何用vue优雅地设计和编码ui组件的指导原则,更多的是希望给大家提供一些选择和参考,而不是墨守成规的条例。

下拉菜单和轮播的主要构成(熟悉的朋友可以直接跳过看结论)

下拉菜单

一个简单的下拉菜单主要是一个输入框和列表构成
vue中调用的方式大致是这样的:

<yy-select v-model="value" placeholder="请选择">
  <yy-option v-for="item in options" :value="item.value" :label="item.label">
  </yy-option>
</yy-select>

这里有两个组件,分别是yy-selectyy-option,在内部实现中,yy-option可以是一个<li>标签,yy-select则包含一个<input>标签,并通过<slot><li>插入到一个<ul>中。

轮播图

轮播图的原理是通过将多个div放到一个父标签div(宽度width)中,并将父标签div设置成overflow:hidden,将当前显示的div设置成transformX(0),上一个div设置成transformX(-width),下一个div设置成transformX(width),再利用过渡即可实现切换的效果
一个简单的轮播图是由上面提到的父标签div,以及一些附加元素,比如左右两边放个左右切换按钮,下面放一排指定元素切换按钮构成
vue中调用的方式大致是这样的:

<yy-carousel>
  <yy-carousel-item v-for="item in 4" >
    <h3>{{item}}</h3>
  </yy-carousel-item>
</yy-carousel>

可以发现调用方式和下拉菜单很类似,在内部实现中,yy-carousel-item可以是一个<div>标签,yy-carousel则通过<slot>将这些<div>包含进来,并添加一些<button>之类的标签

指导原则

接下来将开始我的表演

子组件在created钩子中建立“父子”关系

这里要先解释一下vue插槽原理:通过阅读vue源码,我们可以知道,<yy-select><yy-option></yy-option></yy-select>会创建两个VNode,就像那模板代码给人的感觉一样,这个两个VNode是父子关系。然而,假设yy-select组件中的<template>是下面这样的:

<template>
    <yy-input></yy-input>
    <yy-scroll-bar>
        <slot></slot>
    </yy-scroll-bar>
</template

那么,在vueVNode进行patch的时候,yy-option作为插槽被插到的是yy-scroll-bar里面,此时yy-optionvue实例的父实例(也就是通过this.$parent获取到的)是yy-scroll-barvue实例,而不是yy-selectvue实例
在写组件的过程中,yy-select实例充当一个父亲,负责yy-inputyy-option这两个组件的实例相互通信,这个时候可以人为构造一个父子关系,即:在yy-option中找到yy-select这个实例,并设置this.parent为该实例,同理在yy-select中找到所有的yy-option实例,并赋值为一个数组:this.options。这个过程可以在yy-option实例的created钩子函数中完成。

只在内部改变自己的状态

场景:在下拉菜单的输入框里面输入文字,只有匹配到的option才会显示在列表里。
做法:在yy-select组件中监听yy-inputinput事件,然后写个方法判断this.options里面有哪些满足要求,并设置this.options[i].visible=True
这种在父组件中直接修改子组件属性的做法有诸多不好的地方。更合理的做法是将判断的方法放到yy-option中,通过computed watch监听父组件的一个比方说叫inputContent的属性,又或者利用事件订阅让父组件通过分发事件,来调用yy-option的方法 ,让yy-option在自己内部改变自己的visible属性(有种类似vuex的感觉,组件可以改变store,但不能在组件的方法里直接去改,得发个action,在规定的mutation里面改)

data设置原子属性,computed设置派生属性

这一条和上一条形式上一致,只是看问题的角度不同。vue的核心思想是组件化数据驱动,这意味着对于所有的显示(class,style),都要通过数据驱动,而不是直接操作dom。我在写UI组件的时候一个主要的思考就是如何能够减少data中的变量,毕竟为每一个状态设置一个变量去控制很冗余。比如说yy-option,根据输入框内容决定是否显示需要visible,鼠标点击了背景要变蓝需要selected,通过鼠标或者键盘上下键停留背景要变灰hover,这样就有三个变量放在data里面,更糟糕的是如果你在yy-select父组件中通过逻辑判断后直接修改这些变量值,这是一种非常ugly的做法。我的建议是在子组件的data中设置一些原子属性,比如该optionvalue label以及在父组件的this.options中的index;在父组件中的data中设置value hoverIndex,那么子组件中类似hover这样的派生变量就可以放到computed里面,由hoverIndexindex派生得到,它从原来的由父组件野蛮入侵式地修改变成了现在优雅地依赖于父组件

能用css控制的属性、状态就不要用js

说明:一般下拉菜单的输入框后面都会跟一个向下的箭头之类的icon,还可以前后各放一些修饰的元素,按照组件化的设计思想,我们将其封装成一个对外暴露的<yy-input>
场景:鼠标移动到下拉菜单的yy-input<input>输入框,整个yy-input的边界显示浅蓝色;点击按钮显示灰色;默认是浅灰色
方法:将<input>icon放到一个<div>里面,监听<input>hover focus事件,来改变isHover isFocus变量的值,从而改变<div>style或者class,达到修改border的目的
你会发现在data里面添加了很多额外的变量,在很多业务逻辑里面也要记得添加修改这些变量的语句(点击输入框后边界变蓝且弹出列表,此时isFocus=true,点击列表的某个option后,记得设置isFocus=false,要恢复成默认颜色)。有没有一种方法可以通过监听<input>的事件来修改整个边界的颜色?
答案是css。其实只要将icon绝对定位到输入框里面就好了,这时候整个边界的颜色,就是输入框边界的颜色……

结语

总的来说,上面的几条指导原则优化的目标主要是变量和通信。如何确定每个组件内部所需要的变量,以及这些组件内部变量如何通信,是用vue优雅地编写UI组件的关键。
要把一件事情做出来是简单的,比如写个下拉菜单或者轮播图,原理都不难。只是当考虑到API的设计,代码的质量,要关注的可能就不仅仅是一个一个孤立的知识点,
而是,一个面。

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改