设计 react 组件

从新设计 React 组件库

从新设计 React 组件库

诚身 诚身
7 个月前
 
在 react + redux 已经成为大部分前端项目底层架构的今天,
让咱们再次回到软件工程界一个永恒问题的探讨上来,
那就是如何提高一个开发团队的开发效率?
 

从宏观的角度来说,其实只有良好的抽象才能真正提升一个团队的开发效率,前端

而囿于不一样产品所面临的不一样业务需求,当咱们抽丝剥茧般地将一个个前端工程抽象到最后一层,react

那么剩下的其实就只有按钮、输入框、日历、对话框、图标等这些毫无业务含义的 UI 组件了。数据库

 

选择或开发一套适合本身团队使用的 UI 组件库应该是每个前端团队在底层架构达成共识后下一件就要去作的事情,redux

那么咱们就以今天为始,分别从如下几个方面来一块儿探讨如何构建一套优秀的 UI 组件库。后端

 

第一个问题:选择开源 vs 本身造轮子

在 React 界,优秀且开源的 UI 组件库有不少,国外的如 material-ui,国内的如 ant-design,api

都是通过众多使用者检验,组件丰富且代码质量过硬的组件库。数组

因此当你决定本身再造一套 UI 组件库以前,不妨先尝试下这些在 UI 组件库界口碑良好的标品,再决定是否要亲自进入这个看似简单但实则困难重重的领域。数据结构

 

在这里,咱们并不会去比较任何组件库之间的区别或优劣,但却能够从产品层面给出几个开发自有组件库的判断依据,仅供参考。架构

  1. 产品有独立的设计规范,包括但不限于组件样式、交互模式。
  2. 产品业务场景较为复杂,须要深度定制某些通用组件
  3. 团队须要同时支撑多个类似产品

设计思想:规范 vs 自由

 

在选择了本身造轮子这样一条路以后,下一个摆在面前的艰难的选择就是,框架

要造一个规范的组件库仍是一个自由的组件库?

 

规范的组件库能够从根本上保证产品视觉、交互风格的一致性,

也能够很大程度上下降业务开发的复杂度,从而提高团队总体的开发效率。

但在遇到一些看似类似实则不一样的业务需求时,规范的组件库每每会走入一个可怕的死循环,

 

那就是 A 需求须要使用 A 组件,可是现有的 A 组件不能彻底支持 A 需求。

这时摆在工程师面前的就只有两条路,从零开始把 A 需求开发一遍或者侵入 A 组件代码去支持 A 需求。

方法一费时费力,会极大地增长本次项目的开发成本,方法二会致使 A 组件代码膨胀且逻辑复杂,极大地增长组件库后期的维护成本。

 

 

在屡次陷入上面所描述的这个困境以后,最近一次内部组件库重构时,咱们选择了拥抱自由,

这其中既有业务方面的考虑,也有 React 在组件自由组合方面的自然优点,让咱们来看一个例子。

 

// traditional select <div className={dropdownClass}> <div className={`${baseClassName}-control ${disabledClass}`} onMouseDown={this.handleMouseDown.bind(this)} onTouchEnd={this.handleMouseDown.bind(this)} > {value} <span className={`${baseClassName}-arrow`} /> </div> {menu} </div> 

这是一个很是传统的 Select 组件,触发 Select 的部分为 Select 的值及一个箭头,咱们来看下面的一个业务场景:

 

这里的选择器再也不是 value 加一个箭头,而是一个自定义元素,

点击后展开下拉列表。虽然它的交互和 Select 如出一辙,

但这时候咱们就不能再用当前的这个 Select 去实现它了。

 

// Customizeable Select <div {...filterProps} className={classes} onClick={::this.handleInnerClick}> { children || <span> <span className={`${prefixCls}-container`}> {label ? <span className={`${prefixCls}-container-label`}>{label}</span> : null} <span className={`${prefixCls}-container-value`} style={valueStyle}> {currentValue !== '' ? currentValue : selectPlaceholder} </span> </span> <Icon className={iconClasses} name="angle-down" /> </span> } {this.renderPopup()} </div> 

在传统的 value 和箭头以外,更自由的 Select 添加了 label 及 children 支持,分别能够对应有名称的 Select

或相似上面这种自定义的选择器。

 

一样的还有 Select 的孪生兄弟 Dropdown。

// Customizeable Dropdown <div {...filterProps} className={classes}> { data.map((r, i) => { return ( <ItemComponent data={r} key={i} datas={data} className={itemClasses} onClick={onSelect.bind(null, r, i)} onMouseOver={onMouseOver.bind(null, r, i)} /> ); }) } </div> // Using Dropdown const demoData = [{ text: 'Robb Stark', age: 36 }] SelectItem(props) { const { data, ...other } = props; return (<div {...filterProps}> <div>{data.text}</div> <div>is {data.age} years old.</div> </div>); } 

 

这是一个常见的下拉列表的组件,是否容许用户传入 ItemComponent 其实就是一个规范与自由之间的博弈。

而在选择了拥抱自由以后,组件的使用者终于不用再被组件所定义好的 DOM 结构所束缚,能够自由地组织自定义下拉元素。

 

 

是的,相较于传统的规范组件,自由的组件须要使用者在业务项目中多写一些代码,

但若是咱们往深处多看一层,这些特殊的下拉元素本就是属于某个业务所独有的,

将其放在业务代码层偏偏是一种更合适的分层方法。

 

而另外一方面,咱们在这里所定义的自由,毫不仅仅是多暴露几个渲染函数那么简单,

这里的自由,指的是组件 DOM 结构的自由,由于一旦某个组件定死了本身的 DOM 结构,

外部除了重写样式去强行覆盖外没有任何其余可行的方式去改变它。

 

虽然咱们上面提到了许多自由的好处,但不少时候咱们仍是会被一个问题所挑战,

那就是自由的组件在大部分时候真的很难用,由于调用起来很麻烦。

这个问题实际上是有解的,那就是默认值。

 

咱们能够在组件内部内置许多经常使用的组成元素,当用户不指定组成元素时,

使用默认组成元素来渲染,这样就能够在规范与自由之间达到一个良好的平衡。

固然,这里也有一个贴心小提示,那就是若是你真得但愿在规范与自由之间达到一个良好的平衡,必定要提早作好组件库工做量增长三分之一的准备。

 

或者你也能够选择针对不一样的使用场景,作两套不一样的解决方案,

例如前端开源 UI 框架界的翘楚 antDesign,其底层依赖的 react-component 其实也是很是解耦的设计,

几乎看不到任何固定的 DOM 结构,而是使用自定义组件或 children prop 将 DOM 结构的决定权交给使用者。

 

// react-component/dropdown return ( <Trigger {...otherProps} prefixCls={prefixCls} ref="trigger" popupClassName={overlayClassName} popupStyle={overlayStyle} builtinPlacements={placements} action={trigger} showAction={showAction} hideAction={hideAction} popupPlacement={placement} popupAlign={align} popupTransitionName={transitionName} popupAnimation={animation} popupVisible={this.state.visible} afterPopupVisibleChange={this.afterVisibleChange} popup={this.getMenuElement()} onPopupVisibleChange={this.onVisibleChange} getPopupContainer={getPopupContainer} > {children} </Trigger> ); 

 

数据处理:耦合 vs 解耦

 

若是你问一个工程师在某个场景下,耦合好仍是解耦好?

我想他可能都不会问你是什么场景,就脱口而出:固然解耦好,耦合的代码根本无法维护!

 

但事实上,在传统的组件库设计中,咱们一直都默认组件是能够和数据源(通常组件都会有 data 这个 prop)相耦合的,

这样就致使了咱们在给某个组件赋值以前,要先写一个数据处理方法,

将后端返回回来的数据处理成组件要求的数据结构,再传给组件进行渲染。

 

这时,若是后端返回的或组件要求的数据结构再变态一些(如数组嵌套),

这个数据处理方法有可能会写得很是复杂,

并且也会带来许多的 edge case 致使组件在取某个特定的 attribute 时直接报错。

 

 

那么如何将组件与数据源解耦呢?答案就是不要在组件代码(不管是视图层仍是控制层)中出现 

而是在回调时将整个对象都抛给调用者供其按需使用。

这样咱们的组件就能够无缝适配于各类各样的后端接口,大大下降使用者或组件在数据处理过程当中犯错误的可能。

 

承接前文,其实这样的数据处理方式是和前面所提到的自由的设计思想一脉相承的,

正是由于咱们赋予了使用者自由定制 DOM 结构的能力,因此咱们同时也能够赋予他们在数据处理上的自由。

 

讲到这里,支持规范组件的人可能已经有些崩溃了,由于听起来自由组件既不强制 DOM 结构,

也不处理数据,代码都要咱们在外面写,那么为何还要用这个组件呢?

咱们以 Select(选择器)组件为例来回答这个问题。

 

是的,自由的 Select 须要使用者自定义下拉元素,

还须要在回调中本身处理使用 data 的哪一个 attribute 来完成下一步的业务逻辑,

但 Select 组件真的什么都没有作吗?

 

其实并非,Select 组件规范了选择这个交互方式,处理了何时显示或隐藏下拉列表

添加了下拉列表元素的 hover 和 click 事件,并控制了绝对定位的下拉列表的弹出位置。

这些通用的交互逻辑,才是 Select 组件的核心,

 

至于多变的渲染和数据处理逻辑,打包开放给用户反而更利于他们在多变的业务场景中更加方便地使用 Select 组件。

讲完了组件与数据源之间的解耦,咱们再来说一下组件各个 props 之间解耦的必要性。

假设一个需求:按照中国、美国、英国、日本、加拿大的顺序分别显示 5 个当地时间,当地时间需由服务端获取,且显示格式不一样。

 

这时咱们能够设计一个时间组组件,能够接收五个国家的时间数据做为其 data prop,

而展现一个当地时间至少须要英文惟一标识符(region)、中文显示名(name)、

当前时间(time)、显示格式(format)四个属性,由此咱们能够设计时间组组件的 data 属性为:

 

data: [ { region: 'china' name: '中国', time: 1481718888, format: 'MMMM Do YYYY, h:mm:ss a', }, ... ]

看起来很完美,但事实真的是这样吗?

我相信若是你把这份数据结构拿给后端同事看时,他必定会马上指出一个问题,

那就是后端数据库中是不会保存 name 及 format 字段的,由于这是由具体产品定义的展现逻辑,

 

而接口只负责告诉你这个地区是哪里(region)以及这个地区的当前时间是多少(time)。

事情到这里也许还不算那么糟糕,由于你能够在调用组件以前,

把异步获取到的数据再从新格式化一遍,补上缺失的字段。

 

但这时一个更棘手的问题来了,那就是接口返回的数组数据通常是不保证顺序的,

你还须要按照产品的要求,在补充完缺失的字段后,对这个数组进行一次重排,以保证每一次渲染出来的地区都在一样的位置。

换一种方式,若是咱们这样去设计时间组组件的 props 呢?

 

{ data: { china: { time: 1481718888, }, ... }, timeList: [ { region: 'china', name: '中国', format: 'MMMM Do YYYY, h:mm:ss a', }, ... ], ... } 

当咱们将须要异步获取的 props 抽离以后,这个组件就变得很是 data & api friendly 了,

咱们经过配置 timeList 就能够完美地控制时间组的渲染规则及渲染顺序

而且不再须要去对接口返回的数据进行补全或定制了。

 

甚至咱们还能够经过设置默认值的方式,让组件先同步渲染出来,

在异步的数据请求完成后,重绘数值部分,给予用户更好的视觉体验。

 

除了分离非必须耦合的 props 以外,

细心的朋友可能还会发现上面的 data prop 的数据结构从数组变为了对象,

这又是为何呢?让咱们来看下一小节。

 

回调规范:数组 vs 对象

设计思想能够是自由的,数据处理也能够是自由的,

但一个成熟的 UI 组件库做为一个独立的前端项目,

在代码层面必需要创建起本身的规范,抛开老生常谈的 JavaScript 或 Sass 层面的代码规范不表,

 

咱们从 CSS 类名、组件类别及回调规范三个方面来和你们分享一些最佳实践。

在组件库项目中,并不推荐使用 CSS Modules,

一方面是由于其编译出来的复杂类名不便于使用者在业务项目里进行简单覆盖,

 

更重要的是咱们其实能够很方便地将每个组件看做是一个独立的模块,

用添加 xui-componentName 类名前缀的方式来实现一套简化版的 CSS Modules。

另外,在 jsx 中咱们能够参考 antDesign 的作法,为每个组件添加一个名为 prefixCls 的 prop,

 

并将其默认值也设置为 xui-componentName,这样就在 jsx 层面也保证了代码的统一性,方便团队成员阅读及维护。

在此次内部的组件库重构项目中,咱们将全部的组件分为纯渲染与智能组件两类,

并规范其写法为纯函数与 ES6 class 两种,完全抛弃了 React.createClass 的写法。

 

这样一方面能够进一步规范代码,加强可读性,

另外一方面也可让后续的维护者在一秒钟内判断出某个组件是纯渲染组件仍是智能组件。

 

而在回调方面,全部的组件内部函数都以 handleXXX(handleClick, handleHover, handleMouseover 等)为命名模板,

全部对外暴露的回调函数都以 onXXX(onChange、onSelect 等)为命名模板,

这样在维护一些依赖层级较深的底层组件时,就能够在 render 方法中一眼看出某个回调是在处理内部状态,仍是会抛回到更高一层。

 

在设计回调数据的数据结构时,咱们只使用了单一值(如 Input 组件的回调)和对象两种数据结构,

尽可能避免了使用传统组件库中经常使用的数组。相较于对象,数组实际上是一种含义更为丰富的数据结构,

由于它是有向的(有顺序的),好比在上面时间组的例子中,timeList 就被设计为数组,

 

这样它就能够在承载展现数据的同时表达出时间组展现的顺序,极大地方便了组件使用。

但在给使用者抛出回调数据时,并非每一位使用者都可以像组件设计者那样清楚回调数据的顺序,

使用数组其实变相增长了使用者的记忆成本,并且笔者一直都不同意在代码中出现相似于

 

const value = data[0]; 

这样的表达式,由于没有人可以保证被取值的这个数组长度知足须要且当前位上的元素就是要取的值。

另外一方面,对象由于键值对的存在,在具体到某一个元素的表意上要比数组更为丰富。

例如选择日历区间后的回调须要同时返回开始日期及结束日期:

 

// array ['2016-11-11', '2016-12-12'] // object { firstDay: '2016-11-11', lastDay: '2016-12-12', } 

严格来说上述的两种表达方式没有对错之分,

只是对象的数据结构更可以清晰地表达每一个元素的含义并消除顺序的影响,

更利于不了解组件库内部代码的使用者快速上手。

 

小结

在本文中,咱们从设计思想、数据处理、回调规范三个方面

从整体上为各位剖析了在前端组件化已经成为了既定事实的今天,

咱们还能在组件化方面作出怎样新的尝试与突破。

 

也许这些新的尝试与突破并不会像一个新的框架那样给你带来全新的震撼,

但咱们相信这些实用的思考与经验能够帮助你少走许多弯路或打开一些新的思路,

而且跳脱出前端这个狭小的圈子,站在软件工程的高度去看待本身手头这些看似简单实则复杂的工做。

 

在稍后的文章中,咱们会从组件库总体代码架构、

组件库国际化方案及复杂组件架构设计等方面为你们带来更多细节上的经验与体会,

也会穿插更多的具体的代码片断来阐述咱们的设计思想与理念,敬请期待。

 

文末彩蛋

组件库是全部前端项目的基础,在和你们分享经验的同时,也但愿可以多和各位进行思想上的碰撞,

咱们会从全部留言的朋友中选出最有价值的一位,送上 pure render 专栏的最新力做《深刻 React 技术栈》

@流形 老师签名版)一本,欢迎各位多多留言,让咱们在交流与讨论中一块儿成长!

 

《深刻 React 技术栈》购买连接:

相关文章
相关标签/搜索