组件设计修炼之道
组件设计剖析(前序)
本文是该系列的开篇一,在开始前,我想聊聊这个系列将做什么、希望达成什么样的目标,以及未来如何不断挖坑。
市面上有非常多的独立组件、组件框架,身为开发者我们最常做的是被市场调教 API 使用习惯,我们会局限于一类常用的组件暴露出来的使用方式而难以去适应另一类组件,就像是一个个社交群,如果你一往而深地融入 A ,就难以去接受 B ,而你创造的文化(组件)也在不断的加深这个文化圈。身处“改变世界”伟大理想的一名壮士,我们需要一起去探索各种组件设计及架构的“同”与“不同”,探索那些历史遗留的不好,挖掘那些新兴的好、复古的好,辩证看那些组件设计中的“纠结点”,给更多基础建设开发者参考,让整个大社群往更好的方向走。因为在这个当下,很多“好”是不被习惯的,它需要时间给予变迁。
想借这个系列,跟大家一起去多看看各种组件的设计思路, 我们很难界定组件建设到底属于基础建设还是业务复用性封装,从大环境来看,越是基础的建设越是稳定,而越是接近使用层,那些更需要考虑人性化的东西越是百花齐放。我们在经验中也发现,公司的业务项目更多地选择自己创造组件,而后台项目则会更多使用市面上的框架,有的框架很基础自定义程度太高,我们会抱怨,有的框架封装程度很高,总会遇到不匹配的场景,我们也抱怨。为什么,为什么不会有刚刚好的组件?这其中有太多设计之初的考虑、圈定的范围,一个优秀的组件要考虑的实在太多了,而大多数,能用就好。
这恰恰也是我们需要警觉的,我们在创造组件的过程中,是否只是在自己的认知圈内创作,是否需要去调研市面上更多的实践,是否要去挖掘你所身处的团队的认知能力水平。在创作组件之前,是设计组件,不是一味地照抄某一框架,我们需要认识到,最合适你的团队现在和未来的,是最了解自身业务和未来变化的你,不是市场上的任何框架。
这是第一次做一个系列,当然过程中可能产生一些错误(我会尽量避免),希望大家一起帮助它做的更好。开该系列的目的,是想和大家一起去学习市面上主流组件设计的理念、实现方式、抉择,继往开来做得更好。
本系列将视角着重于 Vue 体系 PC 端 UI 框架,选取有国内 ElementUI、iView、muse-ui, 国外 Vuetify、quasar 这五个框架, 选取标准为当前 Github star 数为参考。
下图为写文当前各框架版本情况及 star 数
框架 | version | star |
---|---|---|
ElementUI | 2.13.0 | 43.7k |
iView | 3.5.4 | 23k |
Muse-UI | 3.0.0-beta.4 | 7.9k |
Vuetify | 2.2.9 | 23.6k |
Quasar | 1.8.5 | 13.5k |
本文是开篇一,不会过于复杂,我们从一个小视角来窥探组件设计,就从一个 input 组件的设计来聊聊差异。在这个过程中,会不停地挖坑,作为之后的选材内容,能不能继续更就看奶茶够不够了。
一个 Input 组件需要哪些功能
我们来一起梳理一下,一个 Input 组件需要哪些功能。接下来我将功能需求归为三类,其中第一类最为基础,基本与底层规范一致;第二类的功能更具有一个时代视觉和交互的惯性;第三类则更个性化,各框架有自己更多的考虑和取舍,争议也最多。
总的来说,划分出的三类,一类是这个组件必须要有功能,二类大概率是这个组件应该有的功能,三类是可以让这个组件做的更好/坏的。越基础,越是通用。
在开始之前,基础方向上就有几点需要明确
⑴ Input 组件不包含 type 为 button、checkbox、file、radio、file、datepicker 的情况,这些划分为独立组件
⑵ Input 组件是否要包含 textarea 的情况 Link Q1
⑶ Input 组件由于要考虑复合情况,常见的其中带有icon、按钮等,其本身需要包装成 div ,那么对于它的命名和定位究竟应该叫 输入框(Input) 还是 本文框(TextFields) , 哪种更合理? Link Q2
一类功能
一类功能属于基础中的基础,这个层次优秀的组件应该是相似的好
一、 原生属性系列,常用的原生功能需要暴露给调用者
- 类型 type
text 为默认,可选 password、number、color 等,按照 html5 规范实现 - 占位文本 placeholder
- 值 value
- 只读模式 readonly
只读模式和禁用模式在视觉交互上的差异设计是否需要 Link Q3 - 禁用模式 disabled
只读模式和禁用模式在视觉交互上的差异设计是否需要 Link Q3 - 自动聚焦 autofocus
二、 常规事件,包括
- 聚焦 fucos
- 失焦 blur
- 选择文本 select
- 输入时 input
- 键盘事件 keydown、keyup、keypress
ElementUI | iView | Muse-UI | Vuetify | Quasar | |
---|---|---|---|---|---|
focus | O | O | O | O | |
blur | O | O | O | O | |
select | O | ▲ | ▲ | ||
input | O | O (对应事件名 change) | O (对应事件名 change) | O | O |
keydown | O | O | ▲ | ||
keyup | O | ▲ | ▲ | ||
keypress | O | ▲ | ▲ | ||
对拼音输入法处理 compositionxxx | O | O | O | ||
防抖处理 debounce | O |
以上是很基本的五个事件,各框架的实现情况如上表整理
O 表示支持或有处理
▲ 表示源码中对所有传入事件都做了传递处理,即原生事件全部支持
可以看到对键盘事件 keyxxx 大多没实现,这意味着调用者无法在该组件基础上做更多快捷键工作;
select 事件也是大多忽略的事件,我们在需求场景中有时也需要用到,比如选中快速(自动)复制;
compositionxxx 国内框架理应处理拼音输入法的情况,如果没做不是经验不足就是懒,印象分极差;
debounce 防抖处理调查结果是比较意外的,竟然只有一家做了;(扩展问题:防抖频率设置多少最佳 Link Q4)
当然在这些框架中也有很多积极的想法,比如封装提供回车事件、对鼠标事件进行更多的封装。
三、 若有所思
对比这几个框架,我们有几个问题
㈠ 事件名 change 和 input 的定义不全一致
① input 表示值改变: ElementUI、Vuetify、Quasar
② change 表示值改变: iView、Muse-UI、
③ change 同原生 change 事件:Vuetify、ElementUI
input | change | |
---|---|---|
ElementUI | ① | ③ |
iView | ① | ② |
Muse-UI | ② | |
Vuetify | ① | ③ |
Quasar | ① | |
我们可以看到对 input 事件的使用是一致的,从一方面来说这是 Vue 官方给出的默认实践方案(如下图),采用 input 事件与 v-model 绑定,对于熟知该规范的人较为友好;从另一方面——语义化理解来说,change 更为贴近 v-model 绑定值改变的含义;再从另一角度——原生 onchange,仅在失焦时才触发,对应的理解是“我输入完了”。这么多看法真让人迷惑,语义化和实际的结合怎么这么混乱。
综上,我们要角逐的问题是
- 要遵循原生 change 的定义 vs 原生 change 设计不合理
- input 是值改变 vs change 是值改变
这里我建议使用 change 是值改变,并且 input 也是值改变,理由如下
- change 最贴近语义使用,新手友好
- 实际使用中,原生 onchange 大多被当做值改变而入手去使用,最终踩坑换方案
- 前时代的框架 jQuery 都将 change 事件处理为了值改变,并不遵循原始含义
- input 由于在 Vue 中是 v-model 的默认绑定事件,被大多数被 Vue 调教熟悉的用户所熟悉,可以保留
㈡ 事件命名规范
- ElementUI、Muse-UI、Vuetify、Quasar 均采用直接动词,如 focus、blur
- iView 采用 on-动词,如 on-focus、on-blur
更推荐使用直接动词,原因如下:
- Vue 中事件绑定采用的写法是
v-on:focus
或简写@focus
, 在此基础上 on 显得多次一举 - 据我观察市面上 90% 的都是直接动词(90%当然是乱编的),尽可能减少认知偏差
㈢ 所有的事件属性是否通过 $attrs、$listeners 一次传入
埋个坑,有机会再聊。link Q5
二类功能
这个层次封装了大部分市面上常用的情况,有当下的时代惯性,各个组件已经能体现出设计方案上的差别和针对性。
一、 封装清除按钮
如下图,给输入框带上清除按钮,是当下非常普遍的一种交互形式。
字段名 | 默认值 | icon独立占位 | 显示触发条件 | 鼠标形式 | |
---|---|---|---|---|---|
ElementUI | clearable | false | O | 有内容时 && (鼠标移入 or 聚焦) | pointer |
iView | clearable | false | X | 有内容时 && 鼠标移入 | default |
Muse-UI | - | - | - | - | - |
Vuetify | clearable | false | O | 有内容时 | pointer |
Quasar | clearable | false | O | 有内容时 | pointer |
调研框架结果如上图,几个点
㈠ 字段名一致为 clearable,无争议
㈡ 默认值均为 false,无争议
㈢ 鼠标形式
从严格交互规范来讲,可点击态应统一使用 pointer
。但考虑到历史原因角度来讲,当视觉已经给予明确可点击形式的情况下,可以无需使用 pointer
形式。正如浏览器默认给 button 设置的是 default,而像 link 这样和文本混淆在一起的可点击态则默认会设置为 pointer,以增强用户的理解。
所以,我的建议是,框架需要为自身的鼠标 cursor 使用设定规范,包括明确目标群体对交互习惯的认知程度,及每个部位在视觉上可点击态的认知感程度,比如带阴影的 button 是强可点击态的,鼠标手势意义不大。整个过程的平衡点在于 “鼠标强烈存在感所造成的突兀感(俗称丑)”和 “交互认知感更强”上。但一定,一定要有自身的规范约束。
- ㈣ 事件监听
独立定义事件 | 触发值改变事件 | change(同原生事件)的表现 | |
---|---|---|---|
ElementUI | clear | input | 不失焦,change会触发 |
iView | on-clear | input, on-change | - |
Muse-UI | - | - | - |
Vuetify | click:clear | input | 不失焦,change不会触发 |
Quasar | clear | input | - |
提供独立定义和触发值改变事件这两点都是一致的最佳实践。
在事件命名体系中,我们看到 Vuetify 有内部自洽的一套规范,个人立场上还挺喜欢的。组件体系事件设计方案 Link Q6。
change 事件按照原生规范的两个框架 Ele 和 Vuetify 在这里的表现并不一致,正如上文所建议的那样,原生 change 在使用中开发者明显容易踩坑,这点上还是建议避免遵循原生,在细节的处理上会显得非常两难。
二、 前/后置内容
如下图,常规情况有输入框内部前后带有 icon ,或是输入框外部(并排)带有按钮等操作。
对于块形态(边框和色块)的输入框,前/后置内容分为 输入聚焦时高亮[内部]的前/后置内容 和 输入聚焦时高亮[外部]的前/后置内容;
对于下划线形态的,由于无需封装处理积木(组件)组合情况时各积木能否拼接上(尺寸),基础通用下仅需封装 输入聚焦时高亮[内部]的前/后置内容 。
调研的几个框架里, Muse-UI 和 Vuetify 较为特别,由于视觉交互规范定义在下划线形态聚焦时,外部 icon 也需要一致高亮(如下图),这两者都封装了 外部前/后置 Icon 字段,算是【三类功能】里的,我们后文再聊
形态(下划线/框) | 内部前/后置字段(slot) | 外部前/后置字段(slot) | 内部前/后置Icon字段 | 外部前/后置Icon字段 | 内部前/后置文本字段 | |
---|---|---|---|---|---|---|
ElementUI | X/O | prefix/suffix | prepend/append | prefix-icon/suffix-icon | - | - |
iView | X/O | prefix/suffix | prepend/append | prefix/suffix | - | - |
Muse-UI | O/X | prepend/append | - | - | icon/action-icon | prefix/suffix |
Vuetify | O/O | prepend-inner/append | prepend/append-outer | prepend-inner-icon/append-icon | prepend-icon/append-outer-icon | prefix/suffix |
Quasar | O/O | prepend/append | before/after | - | - | prefix/suffix |
我们在该需求功能下将基本形态分为 下划线形式、线框和色块形式,两者的业务使用场景都很频繁,通用框架建议都支持。
字段命名看一圈会很懵和混乱,我们先看 slot 的部分。首先,slot 灵活度最高,最佳实践建议都暴露,这里 Muse-UI 做的不好。然后,命名上,两组单词 prefix/suffix 和 prepend/append,从词汇的定义来说, prefix/suffix 比 prepend/append 更贴近一层,这么看国内框架 Ele 和 iView 是比较符合语义的;从语言本身使用来说,这两者可替换性强,国外的用户不查字典可能也不知道差别,所以国际化角度讲,确实应该避免一起使用。
- 这部分我的建议是使用 prepend-inner/append-inner 和 prepend/append, 这样的认知应该是最平衡的。
Vuetify 在 slot 上的命名不建议,一组 prepend-inner/append,另一组 prepend/append-outer,很不和谐,调用者惯性使用上一定会踩坑,需要反复查阅文档。
是否提供前/后置 Icon 或 text 的属性设置,取决于各框架判断对于其受众在这两点上复用程度是否高。我的建议是结合实际,大多数情况有比没有好,毕竟这点上代码量的增加是微乎其微的。
- 这部分我的建议是使用 prepend-inner-icon/append-inner-icon、prepend-icon/append-icon、prepend-inner/append-inner、prepend/append,命名规则与 slot 部分保持一致。
从代码维护性角度来讲,外部前/后置 是维护性价比最低的,功能复杂,使用量不算大,尤其是线框和色块形式是要对各种可能组装的情形进行微处理,需要更多考量。
三类功能
这个层次封装了更多框架自身的定位和创新,我们为什么选择一个框架,很多时候选择它就是靠这样一个“运营的好”的功能。
三类涉及的东西较多,为了愉悦的阅读体验,将单独开一篇来谈论,
涉及
- 结合联想弹窗功能
- 尺寸定义与实现
- 多行输入 & Textarea
- 字数限制功能
- 封装确认/搜索按钮
- 常规事件之外,更精确场景的事件
- 样式形态的场景考量
- 输入提示
- 规则校验
- 切换密码显示功能
总结
最佳实践建议
1. 常用原生的功能需要暴露给调用者,按照 html5 规范核对一遍,这点最为基础
容易遗漏的有,
- min, max, step
- autofocus
- tabindex
2. 常规事件
- 聚焦 fucos
- 失焦 blur
- 选择文本 select
- 输入时 input
- 键盘事件 keydown、keyup、keypress
这几个基础事件都要做上,同时别忘了通过 compositionxxx
对拼音输入法处理,以及防抖优化。
3. 值改变的事件名
建议 change 是值改变,并且 input 也是值改变,舍弃原生对 change 的定义。
4. 事件命名规范用直接动词
对外提供如 change
而非 on-change
5. 清除按钮功能
- 字段名为 clearable,默认值 false 。
- 对外提供独立事件 clear,并且触发值改变事件 input/change。
- 鼠标手势的规范未必要用 pointer,更重要的是框架整体需要制定好规范。
6. 前/后置内容
- 下划线形态 和 块形态 都要提供。
- 内部前/后置 和 外部前/后置 slot 都要提供,命名建议 prepend-inner/append-inner 和 prepend/append 这两组,不要同时存在 prefix/suffix 这组单词。
- 是否提供前/后置 Icon 或 text 的属性设置,取决于各框架判断对于其受众在这两点上复用程度是否高。我的建议是结合实际,大多数情况有比没有好,毕竟这点上代码量的增加是微乎其微的。
挖开的坑
Q1
Input 组件是否要包含 textarea 的情况
Q2
输入框组件命名和定位究竟应该叫 输入框(Input) 还是 本文框(TextFields) ?
Q3
只读模式和禁用模式在视觉交互上的差异设计是否需要
Q4
防抖频率设置多少最佳
Q5
所有的事件属性通过 $attrs、$listeners 一次传入的利弊
Q6
组件体系事件设计方案探索
喝奶茶续命去了,下期有缘再见