beeshell 是一个 React Native 应用的基础组件库,基于 0.53.3 版本,提供一整套开箱即用的高质量组件,包含 JavaScript(如下简称 JS)组件和复合组件(包含 Native 代码),涉及前端(FE)、iOS、Android 三端技术,兼顾通用性和定制化,支持自定义主题,用于开发和服务企业级移动应用。如今已经在 GitHub 上开源,地址:github.com/meituan/bee…前端
截止目前,beeshell 中的组件已经在美团外卖移动端应用蜜蜂 App 中普遍应用,并且已经持续了一年多时间,经过了各类业务场景、操做系统、机型的实战考验,具有很好的稳定性、安全性和易用性,因此咱们将其开源,以期发挥出更大的应用价值。node
在开源以前,咱们对业界已经开源的组件库进行了调研,这里主要对比了 beeshell 与其余组件库的优点与劣势,为你们选择组件库提供参考意见。目前,业界开源的组件库比较多,咱们在这里仅选取 Github Star 数 5000 以上的组件库,并从组件数量、通用性、定制化、是否包含原生功能、文档完善程度五个维度来进行对比分析react
组件库 | 组件数量 | 通用性 | 定制化 | 是否包含原生功能 | 文档完善程度 |
---|---|---|---|---|---|
react-native-elements | 16 | 强,提供一套风格一致的 UI 控件 | 弱,若要定制化可能须要重写 | 否 | 高 |
NativeBase | 28 | 强,提供一套风格一致的 UI 控件 | 中,支持主题变量 | 是 | 高 |
ant-design-mobile | 41 | 强,提供一套风格一致的 UI 控件 | 中,部分能够支持定制化需求 | 是 | 低 |
beeshell | 25 | 强,提供一套风格一致的 UI 控件 | 强,不只支持主题变量,还支持使用继承的方式进行定制化扩展 | 是 | 高 |
经过对比能够看出,beeshell 只在组件数量上稍有劣势,在其余方面都一致或者优于其余项目。由于 beeshell 具有了良好的系统架构,因此丰富组件数量只时间问题,并且咱们团队也已经有了详细的规划来完善数量上的不足。git
系统设计是将一个实际问题转换成相应解决方案的主动过程,是解决办法的描述。在通用的软件工程模型中,需求分析完成后的第一步就是系统设计。一个项目最终的稳定性、易用性在很大程度上也取决于系统设计这一步。github
beeshell 组件库是为了更加快速的搭建移动端应用,为业务开发提供基础技术支持,大幅提高开发人效。然而,面对不一样的业务方、不一样的功能需求、不一样的 UI 规范与交互方式,如何有效的兼顾全部的需求?这对系统设计提出了更高的要求,下面以抽象层次逐层下降的方式来详细介绍 beeshell 的系统设计。shell
这些年,React Native 的出现为移动端开发提供了一种新的选择。React Native 相比原生开发有着更高的开发效率,同时比 HTML五、Hybrid 的性能更好,因此可以脱颖而出,这也使得愈来愈多的开发者开始学习和使用 React Native。npm
beeshell 组件库基于 React Native,向下经过 React Native 与 iOS、Android 平台进行系统层面的交互,向上提供开发者友好的统一接口,抹平平台差别,为用户开发业务功能提供服务支持。beeshell 扮演了一个中间者的角色,从而保证了移动端应用基础功能的稳定性、易用性。编程
框架设计肯定了 beeshell 的系统边界,指明了包含的功能与不包含的功能之间的界限。明确了系统边界,咱们才能继续进行下面的分析、设计等工做。react-native
在进行组件库的详细设计以前,咱们提出了几个设计原则:设计模式
总体上使用 JS 做为统一入口,多层封装隐藏实现细节,抹平 JS 与 Native、iOS 平台与 Android 平台的差别,开箱即用,下降了用户的学习和使用成本。局部上基于 React Native 的技术特色,分红 JS 组件部分和复合组件部分,两部分推行“松耦合”的开发模式,使得 Native 部分拥有替换变动的能力,提高组件库的灵活性。
复合组件部分能够直接暴露 JS 接口,若是有须要,也能够在 JS 组件部分进行定制化封装。咱们尽可能保证 Native 部分功能的原子性、简洁性,有任何定制化需求都使用 JS 来统一实现,遵循 JS 实现优先的设计原则,保证跨平台通用的特性。下面分别介绍 JS 组件部分和复合组件部分的设计。
一个软件的设计分为三个设计层次:体系结构、代码设计和可执行设计。咱们使用自上而下的方法,从体系结构开始进行 JS 组件部分的设计。
软件的体系结构的风格一般有 7 种:管道和过滤器,面向对象,隐式请求,层次化,知识库,解释程序和过程控制。
JS 组件部分使用了层次化的体系结构风格,总体分红三层:基础工具、通用组件、扩展组件,从上到下通用性逐渐减弱、定制化逐渐加强,功能渐进式加强,经过分层设计,各层各司其职,兼顾通用性和定制化。
咱们扩展组件部分会提供大量的定制化组件,若是仍然不能知足需求,用户就能够借鉴扩展组件的实现,根据本身业务需求,在某一继承层级上继承通用组件,自行进行定制化扩展,这点充分体现了 beeshell 定制化的能力。
既然是 React Native 组件库固然少不了 Native 部分,复合组件包含 Native 的功能。beeshell 组件库已经完成了 Native 部分的集成方案与规范,有良好的开发与使用体验,能够不断的集成原生功能。
复合组件部分经过 JS 封装接口,保证了跨平台。Native 部分主要分红 Native Bridge 和纯 Native 两大部分,Bridge 是针对 React Native 的封装,必须在组件库中实现;而纯 Native 部分则能够经过 Pods/Gradle 依赖三方实现,有效的吸取利用原生开发的技术积累。
React Native 提供了一些内置组件,咱们能使用 JS 来实现功能都是基于这些内置组件,这些内置的组件一些是跨平台通用的组件,如:View、Text、TextInput;而另外一些是两个平台分别实现的,如 DatePickerIOS 和 DatePickerAndroid、AlertIOS 和 ToastAndroid。跨平台组件固然没有什么问题,咱们能够专一业务功能的开发,问题是这些非跨平台的组件,给咱们的业务功能开发带来极大困扰,下面举例说明。
iOS 平台的 DatePickerIOS 组件:
Android 平台的 DatePickerAndroid 组件:
不只功能交互彻底不一样,并且类名、调用方式各异,这不只知足不了业务需求,并且也有很高的学习和使用成本。这样相似的组件还有不少,如何抹平平台的差别,实现跨平台?咱们提出的方案是优先使用 JS 来实现功能,这也是咱们组件库的设计原则。
针对上面的问题咱们开发了基于 ScrollView 的 Datepicker 组件,统一类名与调用方式,保证了跨平台通用性。
iOS 平台的 Datepicker 组件:
Android 平台的 Datepicker 组件:
Datepicker 是使用 JS 彻底实现了一个完整功能,可是有的状况不须要实现完整的功能,咱们能够经过 React Native 提供的 Platform
来进行局部的跨平台处理,例如 TextInput 组件。
iOS 平台的 TextInput 组件:
Android 平台的 TextInput 组件:
咱们能够看到,在 Andriod 平台并无清空图标,为了抹平平台的差别,提供更好的通用性,咱们开发了 Input 组件,对 TextInput 进行封装与优化,利用 Platform
定位 Android 平台提供清空功能,
Input 组件在 Android 平台的效果:
总之,beeshell 对跨平台通用性作了进一步的优化,遵循 JS 实现优先的原则,配合 Platform
平台定位 API 为组件的易用性、通用性提供了更好的保障。
随着移动互联网的快速发展,各种移动端产品涌现而且不断发展,这也让软件知识不断被普及,业务方对产品功能的定位逐渐从厂商主导转变为用户主导。产品功能更加精准,个性化、细化、深化是必然趋势,经过定制化服务来知足产品发展的要求也应运而生。不一样行业、不一样类型的产品,功能、特色各不相同,用某一种既定的软件产品来知足不一样类型的需求,其适用性可想而知。定制化有良好的技术架构和技术优点,可定制、可扩展、可集成、跨平台,在个性化需求的处理方面,有着很好的优点,因此咱们须要定制化。
综上所述,beeshell 把定制化做为核心特性,力求知足不一样产品的定制化需求,下文将从组件的样式定制化和功能定制化两方面来进行阐述。
beeshell 的设计规范支持必定程度的样式定制,以知足业务和品牌上多样化的视觉需求,包括但不限于品牌色、圆角、边框等的视觉定制。
在组件库设计之初,就已经统一好了 UI 规范。咱们根据 UI 规范,统必定义样式变量并放置在基础工具层中,即 beeshell/common/styles/varibles.js
文件中,在 React Native 应用中,样式变量其实就是普通的 JS 变量,能够很方便的进行复用与重写操做。React Native 提供了 StyleSheet
经过建立一个样式表,使用 ID 来引用样式,减小频繁建立新的样式对象,在组件库的样式变量应用中灵活使用 StyleSheet.create
和 StyleSheet.flatten
来获取样式 ID 和样式对象。
在每一个组的实现中,会事先引入基础工具层中的样式变量,使用统一的变量对象而不是在组件中自行定义,这样就保证了 UI 样式的一致性。同时,beeshell 提供了重置样式变量的 API,能够实现一键换肤。咱们推荐 beeshell 的用户在开发移动应用时,事先定义好样式变量。一方面使用本身的样式变量重置 beeshell 的样式变量;另外一方面在业务功能开发时,使用本身定义好的样式变量,从而保证总体 UI 的一致性。
样式定制化能够从宏观和总体的角度来实现,而功能的定制化则须要具体问题具体分析,从微观和局部的角度来分析和实现。下文将以 Modal 系列的实现为例,来详细介绍功能定制化。
在移动端的弹窗交互,与 PC 端相比通常会比较简单,咱们把模态框、下拉菜单、信息提示等交互相似的组件统一归类为 Modal 系列,使用继承的方式实现。有人可能会问为何使用继承而不用使用组合?前文已经讲过,组合的主要目的是代码复用,而继承的主要目的是扩展。考虑到弹窗交互有不少定制化的可能性,为了知足更好的扩展性,咱们选择了继承。
首先咱们看下几个组件的实现效果图,对 Modal 系列先有一个直观的认识。
Modal 组件:
提供了遮罩、弹出容器以及淡入淡出(Fade)动画效果,弹出内容部分彻底由用户自定义。这个组件通用性极强,没有任何定制化的功能。这里须要说明下,动画部分独立实现,提供了 FadeAnimated 和 SlideAnimated 两个子类,使用了策略模式与 Modal 系列集成,Modal 组件默认集成 FadeAnimated。
ConfirmModal 组件:
继承 Modal 组件,对弹出内容作了必定程度的定制化扩展,支持标题、确认按钮、取消按钮以及自定义 body 部分的功能,通用性减弱,定制化加强。
SlideModal 组件:
继承 Modal 组件,对动画、弹出容器作了重写,在初始化时实例化 SlideAnimated 类型对象,完成上拉、下拉动画,同时支持了自定义弹出位置的功能。
PageModal 组件:
继承 SlideModal 组件,对弹出内容作了定制化扩展,支持标题、确认按钮、取消按钮以及自定义 body 功能,通用性减弱,定制化加强。
CheckboxModal 组件:
CheckboxModal 组件由 PageModal 和 Checkbox 两个组件使用组合的方式实现,基于通用型组件组合出了更增强大功能,遵循继承与组合灵活运用的设计原则。
经过以上部分,咱们已经对 Modal 系列已经有了直观的认识,而后咱们来看下 Modal 系列的类图以及分层:
动画部分在基础工具(common)中实现;在通用组件(components)中 Modal 组件聚合 FadeAnimated 动画,同时由于 SlideModal、ConfirmModal 比较通用,也在该部分实现;CheckboxModal 则定制化比较强,归类到扩展组件(modules)中。经过这种方式的分层,三层各司其职,使得组件库的层次结构更加清晰,不只实现了定制化,还保证了通用部分的简洁性和可维护性。
React Native 应用的 JS 线程和 UI 线程是两个线程,与浏览器中共用一个线程的实现不一样,因此咱们能够看到 React Native 提供的操做 UI 元素的 API,都是经过回调函数的方式进行调用。
受益于 React,咱们通常不须要直接操做 UI 元素,可是有的组件确实须要复杂的 UI 操做,例如彻底由 JS 实现的 Scrollerpicker 组件:
咱们须要精确的计算容器以及每一项元素的高度,才能正确获得当前选中的项在数据模型(数组)中的索引。如今面临的问题是:在组件渲染完成后的生命周期 componentDidMount
并不能拿到正确容器的高度为,而使用 setTimeout
也会有延迟时长设置为多少的问题。咱们选择使用递归来解决,一次 setTimeout
不行就执行屡次。
这里使用了交互递归,反复执行,直到获得有效的元素尺寸。
React Native 为用户提供了 style 属性来控制元素的样式,咱们能够手动设置相关 UI 元素的尺寸。可是,在一些 Android 机器上,咱们设置的元素尺寸与 measure
方法获取的尺寸信息不一致,通过大量 Android 机器的实际的测试,咱们获得的结论是:有零点几像素的偏差。
咱们把经过 measure
方法获得尺寸信息进行向上与向下取整,获得一个阈值范围,手动设置的尺寸信息只要在这个阈值范围内,就认为是有效尺寸,这种容错机制有效的兼容了极端状况,提升了组件的稳定性。
在使用 Form 组件时,最多见的需求就是校验功能,一般组件库的 Form 组件都会内置校验功能。然而,由于校验方式有同步与异步两种,校验结果展现的样式、位置五花八门,这就致使了校验功能的复杂度变得很高。
绝对定位:
Static 定位:
自定义位置
如何有效的兼顾不一样的需求?咱们提出了校验独立实现的方式,在使用 Form 组件的父组件中,使用 CVD 来定义、配置校验规则,校验结果输出到统一的数据结构(单一数据源),基于这个数据结构,咱们就能在任意时机、任意位置、使用任意样式来展现校验信息。
下面咱们先介绍下 CVD:
CVD 是一个针对复杂表单录入场景的分层解决方案,轻量级、跨平台、易扩展,内置在 beeshell 组件库中,能够直接使用。
CVD 把表单某个控件的录入的流程分红三层:
每一层都对单一数据源 Store 进行不可变数据更新,符合交互内聚和顺序内聚,内聚程度高。
每一层使用函数式组合的方式,定义 key(表单控件的惟一标志)与 key 对应的回调函数,避免了批量 if else
,能够有效下降程序的圆环复杂度。
下面以 Input 组件录入姓名为例,来具体说明,代码以下:
在 onChange
中获取用户输入,调用 cvd.flow
而后就能够经过 cvd.getStore
获取到结果:
经过校验功能独立实现,把校验信息输出到 Store 中,在须要的时候从 Store 中获取校验信息,能够更加精细化的控制元素的样式、位置与布局,兼容各类定制化需求。不少时候,只有咱们想不到,没有作不到。
代码的终极目标有两个,第一个是实现需求,第二个是提升代码质量和可维护性。测试是为了提升代码质量和可维护性,是实现代码的第二个目标的一种方法。
单元测试(Unit Testing),是指对软件中的最小可测试单元进行检查和验证。在结构化编程的时代,单元测试中单元指的就是函数。beeshell 组件库全面使用单元测试,由组件的开发者完成。研究成果代表,不管何时做出修改都须要进行完整的回归测试,对于提供基础功能的组件来讲更是如此,在生命周期中尽早地对软件产品进行测试将使效率和质量都获得最好的保证。Bug 发现的越晚,修改它所需的成本就越高,单元测试是一个在早期抓住 Bug 的机会。
单元测试的优势有如下几点:
beeshell 组件库使用 Jest 作为单元测试的工具,自带断言、测试覆盖率工具,实现开箱即用。
测试用例的核心是输入数据,咱们会选择具备表明性的数据做为输入数据,主要有三种:正常输入,边界输入,非法输入,下面以组件库中提供的 isLeapYear
工具函数来举例说明,代码以下:
Jest 使用 test
函数来描述一个测试用例,其中的 toBe
边是一句断言。
函数使用了外部数据,正常输入确定会有,这里的 2000
和 '2000'
都是正常输入;边界输入和非法输入并非全部的函数都有,这里为了说明使用了有这两种输入的例子,边界输入是有效输入的极限值,这里 0
和 Infinity
是边界输入;非法输入是正常取值范围之外的数据, 'xx'
和 false
则是非法输入。通常状况下,考虑以上三种输入能够找出函数的基本功能点,单元测试与代码编写是“一体两面”的关系,编码时对上述三种输入都是应该考虑的,不然代码的健壮性就会出现问题。
上文所说的测试是针对程序的功能来设计的,就是所谓的“黑盒测试”。单元测试还须要从另外一个角度来设计测试数据,即针对程序的逻辑结构来设计测试用例,就是所谓的“白盒测试”。
仍是以 isLeapYear
函数来进行说明,其代码以下:
这里有一个 if else
语句,若是咱们只提供一个 2000
的输入,只会测试到 if
语句,而不会测试 else
语句。虽然,在黑盒测试足够充分的状况下,白盒测试没有必要,惋惜“足够充分”只是一种理想状态,难于衡量测试的完整性是黑盒测试的主要缺陷。而白盒测试偏偏具备易于衡量测试完整性的优势,二者之间具备极好的互补性,例如:完成功能测试后统计语句覆盖率,若是语句覆盖未完成,极可能是未覆盖的语句所对应的功能点未测试。
白盒测试也是比较常见的需求,Jest 内置了测试覆盖率工具,能够直接在命令中添加 --coverage
参数即可以输出单元测试覆盖率的报告,结果以下:
能够看到代码的每一行都覆盖到了 Coverage 为 100%,在很大程度上保证了功能的稳定性。
想要确保组件库的 UI 不会意外被更改,快照测试(Snapshot Testing)是很是有用的工具。一个典型的移动 App 快照测试案例过程是,先渲染 UI 组件,而后截图,最后和独立于测试存储的参考图像进行比较。使用 Jest 进行在快照测试,在 beeshell 中第一次对某个组件进行测试时,会在测试目录下建立一个 snapshots 文件夹,并将快照结果存放在该文件夹中。快照结果文件以 <组件名>.js.snap 命名,其内容为某个状态下的 UI 组件树。
下面以 Button 组件快照测试为例来讲明:
运行命令后获得快照结果:
常常与单元测试联系起来的开发活动还有静态分析(Static analysis)。静态分析就是对软件的源代码进行研读,查找错误或收集一些度量数据,并不须要对代码进行编译和执行。
静态分析效果较好并且快速,能够发现 30%~70% 的代码问题,能够在几分钟内检查一遍,成本低、收益高。beeshell 使用 SonarQube 进行静态代码检查。
SonarQube 是一个开源的代码质量管理系统,支持 25+ 种语言,能够经过使用插件机制与 Eclipse、VSCode 等工具集成,实现对代码的质量的全面自动化分析和管理。
SonarQube 经过对 Reliability(可靠性)、Security(安全性)、Maintainability(可维护性)、Coverage(测试覆盖率)、Duplications(重复)几个维度,对代码进行全方位的分析,经过设置 Quality Gates 保证代码质量。
beeshell 组件库的分析结果概况如图:
可靠性达到 A 级别,是最高等级,表示无 Bug:
安全性达到 A 级别,是最高等级,表示无漏洞:
测试覆盖率平均达到 70% 以上
beeshell 组件库使用 npm 包的形式下载使用,下载成功后会放置在项目根目录的 node_modules 目录,而后在项目中经过引入模块的方式,引入 beeshell 的组件来使用。
那咱们如何开发组件库?如何保证组件库的开发与使用的体验一致性?
首先,咱们须要一个 demo 项目,这个项目是 beeshell 组件库的开发环境,是一个 React Native 应用。而后,咱们把 beeshell 作为 demo 项目的依赖,在 demo 项目中下载安装。
如今,咱们的问题就变成了 node_modules 目录中的 beeshell 如何和本地的 beeshell 源码进行同步。
咱们知道可使用 npm link 来开发 npm 包,原理以下:
本质是就是使用 Symbol link,可是咱们创建好软连接后,运行打包命令却报错了,错误信息为 Expected path '/xxx/xxx/index.js' to be relative to one of project roots
咱们前端开发一般会用 Webpack 作为打包工具,而 React Native 应用使用的是 Metro,咱们须要分析 Metro 来定位问题。
通过 Metro 的源码分析,咱们发现 Metro 的打包方案与 Webpack 有较大差别,Webpack 是根据入口文件,即配置中的 entry 属性,递归解析依赖,构建依赖关系图而 Metro 是爬取特定路径下的全部文件来构建依赖关系图。
分析发现 Metro 的特定路径默认是运行打包命里的路径,以及 node_modules 下第一层目录,这样咱们就定位到了问题的所在:
Metro 在爬取文件的时候,经过软连接找到了全局的 beeshell 可是并无继续判断全局的 beeshell 是否有软连接,因此没法爬取 beeshell 源码部分。
经过 ln -s 命令,直接创建 demo 项目 node_modules 下 beeshell 包 与 beeshell 源码的软连接:
这种方式同时支持 Native 部分 iOS、Android 的源码开发,注意 Android 部分的须要在 setting.gradle 中调用 getCanonicalPath 方法获取创建软连接后的路径。
经过试验、发现问题、分析源码、定位问题、解决问题、方案完善这几个步骤,完整的实现了 beeshell 组件库的开发与使用的体验一致性,同时提高了组件库的开发效率。
咱们的目标是把 beeshell 建设成为一个大而全的组件库,不只会不断丰富 JS 组件,并且会不断增强复合组件去支持更多的底层功能。由于咱们支持所有引入和按需引入两种方式,用户不须要担忧会引入过多无用组件而使得包体积过大,影响开发和使用效率。
beeshell 目前提供了 20+ 组件以及基础工具,基于良好的架构设计、开发体验,为咱们不断地丰富组件库提供了良好的基础。同时在开发 React Native 应用的几年时间中,咱们已经积累了 50+ 基础以及业务组件,咱们后续会把积累的组件进行梳理与调整,所有迁移到 beeshell 中。由于咱们的组件主要来源于咱们的业务需求,可是业务场景有限,可能会使得 beeshell 的发展受到限制,因此咱们将其开源。但愿借助社区的力量不断丰富组件库的功能,尽最大努力覆盖到移动应用方方面面的功能,欢迎你们献计献策,多多支持。
咱们为组件库发展规划了三个阶段: