前言
最近在尝试写几个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-select
,yy-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复制代码
那么,在vue
对VNode
进行patch
的时候,yy-option
作为插槽被插到的是yy-scroll-bar
里面,此时yy-option
的vue实例的父实例(也就是通过this.$parent
获取到的)是yy-scroll-bar
的vue实例,而不是yy-select
的vue实例。
在写组件的过程中,yy-select
实例充当一个父亲,负责yy-input
和yy-option
这两个组件的实例相互通信,这个时候可以人为构造一个父子关系,即:在yy-option
中找到yy-select
这个实例,并设置this.parent
为该实例,同理在yy-select
中找到所有的yy-option
实例,并赋值为一个数组:this.options
。这个过程可以在yy-option
实例的created
钩子函数中完成。
只在内部改变自己的状态
场景:在下拉菜单的输入框里面输入文字,只有匹配到的option
才会显示在列表里。
做法:在yy-select
组件中监听yy-input
的input
事件,然后写个方法判断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
中设置一些原子属性,比如该option
的value label
以及在父组件的this.options
中的index
;在父组件中的data
中设置value hoverIndex
,那么子组件中类似hover
这样的派生变量就可以放到computed
里面,由hoverIndex
和index
派生得到,它从原来的由父组件野蛮入侵式地修改变成了现在优雅地依赖于父组件
能用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
的设计,代码的质量,要关注的可能就不仅仅是一个一个孤立的知识点,
而是,一个面。