beeshell 是一个 React Native 应用的基础组件库,基于 0.53.3 版本,提供一整套开箱即用的高质量组件,包含 JavaScript(以下简称 JS)组件和复合组件(包含 Native 代码),涉及前端(FE)、iOS、Android 三端技术,兼顾通用性和定制化,支持自定义主题,用于开发和服务企业级移动应用。现在已经在 GitHub 上开源,地址为:https://github.com/meituan/beeshell。
截止目前,beeshell 中的组件已经在美团外卖移动端应用蜜蜂 App 中广泛应用,而且已经持续了一年多时间,通过了各种业务场景、操作系统、机型的实战考验,具备很好的稳定性、安全性和易用性,所以我们将其开源,以期发挥出更大的应用价值。
在开源之前,我们对业界已经开源的组件库进行了调研,这里主要对比了 beeshell 与其他组件库的优势与劣势,为大家选择组件库提供参考意见。目前,业界开源的组件库比较多,我们在这里仅选取 Github Star 数 5000 以上的组件库,并从组件数量、通用性、定制化、是否包含原生功能、文档完善程度五个维度来进行对比分析:
通过对比可以看出,beeshell 只在组件数量上稍有劣势,在其他方面都一致或者优于其他项目。因为 beeshell 具备了良好的系统架构,所以丰富组件数量只时间问题,而且我们团队也已经有了详细的规划来完善数量上的不足。
系统设计是将一个实际问题转换成相应解决方案的主动过程,是解决办法的描述。在通用的软件工程模型中,需求分析完成后的第一步就是系统设计。一个项目最终的稳定性、易用性在很大程度上也取决于系统设计这一步。
beeshell 组件库是为了更加快速的搭建移动端应用,为业务开发提供基础技术支持,大幅提升开发人效。然而,面对不同的业务方、不同的功能需求、不同的 UI 规范与交互方式,如何有效的兼顾所有的需求?这对系统设计提出了更高的要求,下面以抽象层次逐层降低的方式来详细介绍 beeshell 的系统设计。
这些年,React Native 的出现为移动端开发提供了一种新的选择。React Native 相比原生开发有着更高的开发效率,同时比 HTML5、Hybrid 的性能更好,所以能够脱颖而出,这也使得越来越多的开发者开始学习和使用 React Native。
beeshell 组件库基于 React Native,向下通过 React Native 与 iOS、Android 平台进行系统层面的交互,向上提供开发者友好的统一接口,抹平平台差异,为用户开发业务功能提供服务支持。beeshell 扮演了一个中间者的角色,从而保证了移动端应用基础功能的稳定性、易用性。
框架设计确定了 beeshell 的系统边界,指明了包含的功能与不包含的功能之间的界限。明确了系统边界,我们才能继续进行下面的分析、设计等工作。
在进行组件库的详细设计之前,我们提出了几个设计原则:
整体上使用 JS 作为统一入口,多层封装隐藏实现细节,抹平 JS 与 Native、iOS 平台与 Android 平台的差异,开箱即用,降低了用户的学习和使用成本。局部上基于 React Native 的技术特点,分成 JS 组件部分和复合组件部分,两部分推行“松耦合”的开发模式,使得 Native 部分拥有替换变更的能力,提升组件库的灵活性。
复合组件部分可以直接暴露 JS 接口,如果有需要,也可以在 JS 组件部分进行定制化封装。我们尽量保证 Native 部分功能的原子性、简洁性,有任何定制化需求都使用 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 的发展受到限制,所以我们将其开源。希望借助社区的力量不断丰富组件库的功能,尽最大努力覆盖到移动应用方方面面的功能,欢迎大家献计献策,多多支持。
我们为组件库发展规划了三个阶段:
扫码加入美团beeshell技术交流群,跟作者零距离交流。如群已满,请加美美同学的微信(微信号:MTDPtech01),回复:beeshell,美美会自动拉你进群。
原网址: 访问
创建于: 2022-01-18 14:16:00
目录: default
标签: 无
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
最新评论