天鹅到家自2014年成立以来,开发了包括天鹅到家,阿姨一点通,家政用户端,到家商家端,家政员工端等在内的诸多App,在移动研发领域有深厚的技术积累,移动研发团队也沉淀了一套到家App通用开发框架(以下简称通用框架),在这篇文章里,我们一起来看看她的全貌吧。
PS:因为笔者主要从事Android开发,所以视角更多的会放在Android平台。
熟悉到家业务的都知道,到家App按用户群体划分,主要分成三类:用户端,代表App是天鹅到家和家政用户端;商家端,代表App是阿姨一点通;员工端,到家员工端(下图皆为测试数据)。
上面是四个代表App的首页截图。我们仔细观察就会发现一些共同功能,比如定位、消息红点、扫一扫、tab导航等等。如果是放眼整个App呢,相同功能会更多。比如:登录注册、分享、版本更新、地址选择、视频面试、push消息、日志记录等等。我们当然不可能每个App将这些功能都分别实现一遍,那如何实现功能重用是我们面临的问题。到家给出的答案是:组件化。
经过多年的积累,到家将许多基础功能和通用业务功能都抽取成了组件,下图对我们主要的组件进行了列举。
罗马不是一日建成的,组件化建设也不例外。在长时间的积累过程中,到家经历了多次组织架构调整。我们的组件也有着不同的开发部门,开发人员和开发时期。所以也产生了诸多问题。比如
•技术栈不统一:组件开发语言有java也有kotlin,java语言支持的版本有的是1.7,有的是1.8•依赖库不统一:support库的版本五花八门;网络库选型不一致;消息总线方案不一致等等•开发规范不统一:命名规范、compile和target sdk的版本、运行环境定义等都没有统一•组件关系不清晰:以上三点其实都可以归属为规范不统一,无伤大雅。但是因为缺少前期的统一规划和协调,组件化比较致命的问题是组件之间的关系不清晰,造成组件间的直接依赖。比如IM、视频通话、直播这些组件都依赖于IM的用户登录数据,如果IM组件出了问题,会造成另外两个组件也不可用;这些组件也都有自己的BaseActivity等基础类的封装,没有统一的基础组件。
那怎么解决上述问题呢?我们给出的答案是:组件开发遵循基本法
首先我们将组件开发规范化。
•统一技术栈:考虑到存量组件较多都是java语言开发的,我们统一组件开发都使用java,支持1.8•统一依赖库:统一support库版本,网络库统一使用Retrofit+okhttp,图片加载使用Glide等•统一业务定义:生产环境、沙盒环境和测试环境的定义保持一致;相同业务字段统一命名等•其他规范:命名规范、MVP代码规范等
除了规范定义,我们对于组件如何封装也有一套标准。以定位功能为例,到家有自己的一套城市体系,在定位成功之后,请求后端拿到到家的cityId,再将id数据提供给其他业务功能。如果我们基于百度地图来做简单封装,可以创建一个单例的定位类,对外提供一个定位API,参数则只需要传入百度地图sdk的定位监听器。在API内,我们创建百度的定位客户端,做一些简单配置,传入监听器启动定位,在监听器中接收到定位数据请求后端拿到cityId。这样做有什么问题呢?上层应用将不可避免的需要引用到百度的监听器类和实体类,如果以后需要将百度sdk替换成高德sdk,会有一定的切换成本。如果是更复杂的功能,比如直播、IM等,则切换成本会很高。那我们可以采用怎样的方式封装呢?看下图:
我们在第三方之上增加到家的适配层,不将实体类、监听器和客户端直接暴露给App,通过到家客户端代理类持有实际的厂商定位客户端对象(还可以采用策略模式等方式动态切换厂商),提供到家自己的位置实体类和监听器类,在代理类中封装定位方法,只需传入到家的监听器,在厂商位置回调后,代理类将位置转化成到家位置信息通过到家监听器回调出去。实现层我们实现了一个默认监听器,获取了到家位置信息之后,在这里处理了到家特性业务,比如获取城市的cityId、持久化存储位置信息等,同时封装好默认的定位API,外层未传入监听器时使用默认的监听器实现。
通过上述封装之后,组件化也具备了以下特点:
•业务解耦合:从APP中抽取组件本身就是一次解耦过程•业务重用:通用的业务功能独立成组件之后,可以被多个APP复用•业务可扩展:组件内保持高度的可扩展性。比如上例中我们需要接入多厂商,让APP选择厂商,则可以在组件层面完成功能快速扩展。APP可以维持原有接入,也选择接入自选厂商的新功能。•适配到家业务:组件分为基础组件和业务组件,对于业务组件则具备到家的业务属性,在组件内我们集成到家的公共业务,比如上例中我们cityId的获取•屏蔽第三方灵活切换:上层APP对底层供应商是完全无感知的,当底层因为商务或技术选型切换供应商时,接入方只需更新版本即可,接入方式没有任何变化。
通过组件封装的基本法,我们规范了单个组件开发,但并没有解决组件关系不清晰的问题。我们先看下面两个问题。
•如何防止组件之间不合理的依赖呢?我们需要组件间解耦的工具,方案也有很多,比如消息总线,路由等等。•所有组件都不可相互依赖吗?前文提到,组件有不同类型:基础功能、通用业务等。显然业务组件是可以依赖基础组件的,反之则不允许。所以我们很容易想到通过组件分层来约束组件之间的依赖关系。如下图:
我们将组件划分为三类:
•基础功能组件:我们将业务组件中都有的页面封装、网络访问、工具类等从组件内抽离,形成一个个基础组件构成了这一层。这一层都是最基础的功能,不包含任何业务逻辑,直接与平台Framework打交道。•通用业务组件:这一层都是独立的业务功能组件,不可相互依赖,但都依赖于基础功能组件。•纵向支撑组件:这部分组件主要用于组件之间的解耦,以及用于APP业务监控的日志组件、系统特性的授权组件等等。这些组件不像基础组件是业务组件必须依赖的,但是可以在业务组件需要的时候被业务组件引入。•依赖法则:上层依赖下层,左侧依赖右侧。
组件化使通用业务功能得以复用,节省了单个App的开发成本,更利于团队协作开发。但是组件化也存在一些问题。
•接入有成本:组件一般有一个初始化方法,需要宿主App传入一些配置信息;部分组件比如分享,还需要在宿主App中创建指定包名类名的Activity文件•不是所有通用功能都适合组件化,这部分功能仍需要在App开发时同步。
那怎么解决这些问题呢?有没有一劳永逸的办法?三国演义开篇说到:天下大势,合久必分,分久必合。我们从App中分离抽取组件的过程,就是合而后分;那我们是不是可以再将组件聚拢,用一个容器将组件集中引入,集中管理?这便是分而后合。因此,到家想到用一个壳App将这些东西都聚合起来,这也是接下来要介绍的:到家App通用开发框架。
既然称之为框架,那自然要进行架构设计。结合到家的业务特点和App常见的架构方式,我们采用以下架构设计思路:
•分层:顺着组件化的分层思路,通用框架大方向上延续了分层架构•内聚:通用业务功能组件化、业务功能模块化、MVP模式聚合同类型业务•解耦:消息总线和路由跳转实现组件和模块间的解耦,部分场合也可以使用调停者模式来实现组件间的时间注册和监听•微内核:业务功能可通过插件或采用Hybrid模式动态插拔,主App作为一个容器,提供基础能力 最终我们确定了以下通用框架,如图:
•底层:最底层是三方库和系统Framework依赖,这一层也确定了我们的常用技术选型•通用基础层:绿色虚线内是我们的组件库,既包括基础组件,也包括业务组件;同时包括常用的未组件化的功能模块,比如数据存储、页面管理;还有对组件的二次封装,比如选图组件的上传流程、web组件的通用交互库等等•App壳层:这一层是通用的壳App应用层,App可以在这一层开发个性化业务,同时这一层也集成了应用和组件的初始化、应用配置、开屏和用户引导等常用功能,最大化的提升开发效率•纵向支撑:和组件化分层中的定位一致,增加了全局异常监控、依赖注入和AOP等•Hybrid:在通用层提供基础Web容器,在App壳层增加容器定制,提供H5业务的支撑
今年到家启动了一个新项目:家政云。家政云是为千万家政公司提供公司管理、客户管理、家政员(劳动者)管理,以及线上签单、保险购买、信用查询、业务看板等核心业务功能的一套系统。而家政云App是到家提供给公司老板和员工使用的工具App。
我们通过首页的UI稿来比较直观的分析一下家政云的需求,如图:
•个性业务:从上图可以看出,家政云有许多个性业务。首页聚合、客户管理、家政云管理、公司管理、保险、电子合同、搜索等等•通用业务:也有许多通用业务,比如登录注册、消息推送、分享、WEB容器等•基础功能:除了以上比较直观的功能,还有一些隐含的基础功能,比如基础页面的封装、数据存储、网络模块、常用工具等等
通过上述分析,我们可以得出结论:家政云=通用框架+个性业务
通用业务和基础功能部分当然是原生来提供支撑,但是个性业务则由前端和原生两个团队负责,如何分工呢?我们先看一下具体有哪些个性业务功能:
我们将功能模块的类型,大致分成三类:
•聚合页面类:这一类页面是由许多业务模块的数据聚合而成的,最具代表性的是首页•信息管理类:这些模块侧重信息的创建和创建后的数据维护管理•业务功能类:这些模块侧重某一项具体功能,能提供给信息模块使用
厘清了功能模块及其特点,那么哪些适合用原生开发,哪些适合用H5开发呢?我们主要基于以下三点:
•功能特性:业务多变的一般采用H5开发,比如信息管理;业务稳定和依赖系统底层的一般使用Native,比如个人中心、消息中心列表等•交互特征:有复杂动画对性能要求较高或者离线时也能快速访问的一般用Native开发,比如设置,含有退出登录的入口,需要在离线时也能访问•项目管理:功能特性和交互特征两点都有一定的弹性空间,回归到分工的本质,这是一个项目管理的问题。我们需要综合考虑当前人力的投入和项目的工期等因素,如果前端投入人力较少,则功能模块可以适当往原生倾斜,反之亦然。
根据这些原则我们选择了下图中的紫色背景部分采用原生开发
选定好原生开发的模块,在我们排期开发时发现,如果按照传统的Android和iOS独立开发的方式,根本无法在deadline之前完成开发,因此原生除了基于通用框架开发,还需要其他方式提效。我想很多朋友都能想到采用现在比较主流的方式:原生跨平台。我们调研了Flutter、RN等主流的跨平台方案,最终选定了Flutter作为到家的跨平台方案,因为本篇不是Flutter专题,因此也不对我们选型过程和Flutter做过多的介绍。
最终我们选定了下图中的红色背景部分采用Flutter开发
经过需求分析,开发分工,提效方式的确立,我们在通用框架的基础上衍生出了家政云项目的业务架构图,如下:
上图和通用框架最大的差异是,增加了一个UI层。用于家政云通用UI的管理。
实际项目开发中我们又遇到很多问题,比如
•Flutter的网络请求原生桥接 or DIO?•搜索如何支持动态新增搜索类型•首页如何满足配置化等
这些并非此文重点,不在此赘述了。
我们的框架经过家政云项目的实践,确立了Flutter作为到家跨平台开发的方案,也引入到了通用框架中,最终通过项目推动了框架的演进。
未来,我们将持续推进通用框架的发展和完善。比如推动Flutter组件化的建设,研究Flutter动态化的方案;强化组件管理:组件自动化注册(初始化)、组件规范检查自动化工具等;通用框架壳工程一键生成等等。
(全文完)