组件库实现

跨平台通用性保障

React Native 提供了一些内置组件,我们能使用 JS 来实现功能都是基于这些内置组件,这些内置的组件一些是跨平台通用的组件,如:View、Text、TextInput;而另一些是两个平台分别实现的,如:DatePickerIOS 和 DatePickerAndroid、AlertIOS 和 ToastAndroid。跨平台组件当然没有什么问题,我们可以专注业务功能的开发,问题是这些非跨平台的组件,给我们的业务功能开发带来极大困扰,下面举例说明。

iOS 平台的 DatePickerIOS 组件:

image

Android 平台的 DatePickerAndroid 组件:

image

不仅功能交互完全不同,而且类名、调用方式各异,这不仅满足不了业务需求,而且也有很高的学习和使用成本。这样类似的组件还有很多,如何抹平平台的差异,实现跨平台?我们提出的方案是优先使用 JS 来实现功能,这也是我们组件库的设计原则。

针对上面的问题我们开发了基于 ScrollView 的 Datepicker 组件,统一类名与调用方式,保证了跨平台通用性。

iOS 平台的 Datepicker 组件:

image

Android 平台的 Datepicker 组件:

image

Datepicker 是使用 JS 完全实现了一个完整功能,但是有的情况不需要实现完整的功能,我们可以通过 React Native 提供的 Platform 来进行局部的跨平台处理,例如 TextInput 组件。

iOS 平台的 TextInput 组件:

image

Android 平台的 TextInput 组件:

image

我们可以看到在 Andriod 平台并没有清空图标,为了抹平平台的差异,提供更好的通用性,我们对开发了 Input 组件,对 TextInput 进行封装与优化,利用 Platform 定位 Android 平台提供清空功能,

Input 组件在 Android 平台的效果:

image

总之,Beeshell 对跨平台通用性做了进一步的优化,遵循 JS 实现优先的原则,配合 Platform 平台定位 API 为组件的易用性、通用性提供了更好的保障。

定制化支持

随着移动互联网的快速发展,各类移动端产品层出不穷的出现并且不断发展,这些使得软件知识不断普及,业务方对产品功能的定位逐渐从厂商主导转变为用户主导。产品功能更加精准,个性化、细化、深化是必然趋势,这时候需要定制化服务来满足产品发展的要求也应运而生。不同行业、不同类型的产品,功能、特点各不相同,用某一种既定的软件产品来满足不同类型的需求,其适用性可想而知。定制化有良好的技术架构和技术优势,可定制、可扩展、可集成、跨平台,在个性化需求的处理方面,有着很好的优势,所以我们需要定制化。

Beeshell 把定制化作为核心特性,力求满足不同产品的定制化需求,下面从组件的样式定制化和功能定制化两方面来具体介绍。

样式定制化

Beeshell 的设计规范支持一定程度的样式定制,以满足业务和品牌上多样化的视觉需求,包括但不限于品牌色、圆角、边框等的视觉定制。

image

在组件库设计之初,就已经统一好了 UI 规范。我们根据 UI 规范,统一定义样式变量并放置在基础工具层中,即 beeshell/common/styles/varibles.js 文件中,在 React Native 应用中,样式变量其实就是普通的 JS 变量,可以很方便的进行复用与重写操作。React Native 提供了 StyleSheet 通过创建一个样式表,使用 ID 来引用样式,减少频繁创建新的样式对象,在组件库的样式变量应用中灵活使用 StyleSheet.createStyleSheet.flatten 来获取样式 ID 和样式对象。

在每个组的实现中,会事先引入基础工具层中的样式变量,使用统一的变量对象而不是在组件中自行定义,这样就保证了 UI 样式的一致性。同时,Beeshell 提供了重置样式变量的 API,可以实现一键换肤。我们推荐 Beeshell 的用户在开发移动应用时事先定义好样式变量,一方面使用自己的样式变量重置 Beeshell 的样式变量,另一方面在业务功能开发时使用自己定义好的样式变量,从而保证整体 UI 的一致性。

功能定制化

样式定制化可以从宏观和整体的角度来实现,而功能的定制化则需要具体问题具体分析,从微观和局部的角度来分析和实现。下面以 Modal 系列的实现为例来详细介绍功能定制化。

在移动端的弹窗交互,与 PC 端相比一般会比较简单,我们把模态框、下拉菜单、信息提示等交互类似的组件统一归类为 Modal 系列,使用继承的方式实现。有人可能会问为什么使用继承而不用使用组合?前面已经讲过,组合的主要目的是代码复用,而继承的主要目的是扩展,考虑到弹窗交互有很多定制化的可能性,为了更好的扩展性,我们选择继承。

首先我们看下几个组件的实现效果图,对 Modal 系列有个直观的认识。

Modal 组件:

image

提供了遮罩、弹出容器以及淡入淡出(Fade)动画效果,弹出内容部分完全由用户自定义。这个组件通用性极强,没有任何定制化的功能。这里需要说明下,动画部分独立实现,提供了 FadeAnimated 和 SlideAnimated 两个子类,使用了策略模式与 Modal 系列集成,Modal 组件默认集成 FadeAnimated。

ConfirmModal 组件:

image

继承 Modal 组件,对弹出内容做了一定程度的定制化扩展,支持标题、确认按钮、取消按钮以及自定义 body 部分的功能,通用性减弱,定制化增强。

SlideModal 组件:

image

继承 Modal 组件,对动画、弹出容器做了重写,在初始化时实例化 SlideAnimated 类型对象,完成上拉、下拉动画,同时支持了自定义弹出位置的功能。

PageModal 组件:

image

继承 SlideModal 组件,对弹出内容做了定制化扩展,支持标题、确认按钮、取消按钮以及自定义 body 功能,通用性减弱,定制化增强。

CheckboxModal 组件:

image

CheckboxModal 组件由 PageModal 和 Checkbox 两个组件使用组合的方式实现,基于通用型组件组合出了更加强大功能,遵循继承与组合灵活运用的设计原则。

经过前面的介绍,我们已经对 Modal 系列有了直观的认识,然后我们来看下 Modal 系列的类图以及分层:

image

动画部分在基础工具(common)中实现;在通用组件(components)中 Modal 组件聚合 FadeAnimated 动画,同时因为 SlideModal、ConfirmModal 比较通用,也在该部分实现;CheckboxModal 则定制化比较强,归类到扩展组件(modules)中。通过这种方式的分层,三层各司其职,使得组件库的层次结构更加清晰,不仅实现了定制化,还保证了通用部分的简洁性和可维护性。

复杂 Case 处理

相互递归处理异步渲染

React Native 应用的 JS 线程和 UI 线程是两个线程,与浏览器中共用一个线程的实现不同,所以我们可以看到 React Native 提供的操作 UI 元素的 API 都是通过回调函数的方式。

受益于 React 我们一般不需要直接操作 UI 元素,但是有的组件确实需要复杂的 UI 操作,例如完全由 JS 实现的 Scrollerpicker 组件:

image

我们需要精确的计算容器以及每一项元素的高度,才能正确得到当前选中的项在数据模型(数组)中的索引。现在面临的问题是在组件渲染完成后的生命周期 componentDidMount 并不能拿到正确容器的高度为,而使用 setTimeout 也会有延迟时长设置为多少的问题。我们选择使用递归来解决,一次 setTimeout 不行就执行多次。

image

这里使用了交互递归,反复执行直到得到有效的元素尺寸。

UI 尺寸容错机制

React Native 为用户提供了 style 属性来控制元素的样式,我们可以手动设置相关 UI 元素的尺寸。但是,在一些 Android 机器上,我们设置的元素尺寸与 measure 方法获取的尺寸信息不一致,经过大量 Android 机器的实际的测试,我们得到的结论是有零点几像素的误差。

image

我们把通过 measure 方法得到尺寸信息进行向上与向下取整,得到一个阈值范围,手动设置的尺寸信息只要在这个阈值范围内就认为是有效尺寸,这种容错机制有效的兼容了极端情况,提高了组件的稳定性。

精细化布局控制

在使用 Form 组件时最常见的需求就是校验功能,通常组件库的 Form 组件都会内置校验功能,然而,因为校验方式有同步与异步两种,校验结果展示的样式、位置五花八门,导致了校验功能的很高的复杂度。

绝对定位:

image

Static 定位:

image

自定义位置

image

如何有效的兼顾不同的需求?我们提出了校验独立实现的方式,在使用 Form 组件的父组件中,使用 CVD 来定义、配置校验规则,校验结果输出到统一的数据结构(单一数据源),有了这个数据结构我们就能在任意时机、任意位置、使用任意样式展示校验信息。

下面我们先介绍下 CVD:

image

CVD 是一个针对复杂表单录入场景的分层解决方案,轻量级、跨平台、易扩展,内置在 Beeshell 组件库中,可以直接使用。

CVD 把表单某个控件的录入的流程分成三层:

  • Connector 连接器,把用户输入的信息转化成所需的数据格式。
  • Validator 校验器,对格式化的数据进行校验。
  • Dependency 依赖处理器,处理当前控件与其他控件的依赖关系。

每一层都对单一数据源 Store 进行不可变数据更新,符合交互内聚和顺序内聚,内聚程度高。

每一层使用函数式组合的方式,定义 key(表单控件的唯一标志)与 key 对应的回调函数,避免了批量 if else,可以有效降低程序的圆环复杂度。

下面以 Input 组件录入姓名为例来具体说明,代码如下:

image

onChange 中获取用户输入,调用 cvd.flow 然后就可以通过 cvd.getStore 获取到结果

image

通过校验功能独立实现,把校验信息输出到 Store 中,在需要的时候从 Store 中获取校验信息,可以更加精细化的控制元素的样式、位置与布局,兼容各种定制化需求,只有想不到、没有做不到。