查看原文
其他

开源|Magpie:Magpie在安居客有料业务的落地实践

王海君 58技术 2022-03-15





开源项目专题系列(八)
1.开源项目名称magpie2.github地址:

https://github.com/wuba/magpie

https://github.com/wuba/magpie_sdk

https://github.com/wuba/magpie_fly

https://github.com/wuba/magpie_log


3.简介:本文以安居客App“有料”模块Flutter化为例,介绍怎么利用Magpie四个开源项目来解决原生工程在初次使用Flutter技术落地一个产品需求时需要考虑的问题,Magpie项目帮助我们大大提升了整个开发效率。




live58技术沙龙活动第五期Flutter在58的应用实践系列话题本周日晚7:00准时开幕点击图片即可报名抢座

全文大纲如下:
  • 背景
  • 简介
    • 原有问题
    • 解决过程
    • Flutter化“有料”体验版本下载

    背景

    Flutter作为Google新推出的跨平台解决方案,它提供了一整套从底层渲染到上层开发语言的全方案支持。视图渲染完全闭环在自带的框架内部,不依赖于底层系统提供的UI组件,从根本上保证了它渲染的UI在Android和iOS平台的高度一致,这也是它跟React Native的核心区别。在获得跨平台渲染一致性和高效性的同时,Hot Reload功能大大提升了移动端同学在调试UI视觉时的效率,以往数分钟的编译安装过程可以在几秒钟内完成。此外它将会成为Google公司开发的下一个操作系统(Fchusia)的主要UI开发框架,会成为我们大前端同学必备的技能。

    原有问题

    Flutter从语言到开发框架都是全新的,国内投入调研的公司还较少,整个Flutter的生态还不是

    需要更多的团队投入和实践,才能让Flutter真正在项目中发挥预期的效果。2019年底 我们发起了Magpie Flutter项目,项目组选取了安居客App中的“有料”模块作为Flutter化实践的第一块实验田。在这个过程中,我们遇到了一些问题,整理如下:

    1、混合工程采用什么样的方式搭建;
    2、Flutter模块怎么按照现有的模块化方式集成到原生工程,Flutter模块输入和输出怎么能像原生模块一样实现无差异的通信;
    3、当开始实现界面视觉元素的时候,我们怎样把已有的视觉规范导入到Flutter,从而让Flutter Widget也能像原生UI标准组件一样展示;
    4、面对一个复杂的UI组件我们是需要从0开始开发吗?有没有已经封装好的UI组件帮助我们快速的搭建界面;
    5、完成界面开发后,用户在Flutter界面的操作日志我们怎么传到自己的服务器,有没有什么无痕埋点的方法呢?
    6、开发完了需求,我们回头看界面质量怎么样,得到高性能的同时我们有哪些损失呢?

    解决过程

    1.  搭建混合工程 与 HelloWorld界面显示
    Magpie Workflow 安装可以看Magpie:平台工具链开发实践介绍,在我们安装完Flutter和 Magpie Workflow环境之后,我们就可以通过magpie create -n ** 创建一个三端(Dart、Android、iOS)一体化的Flutter工程,我们后续就可以在这里开发和调试我们的业务侧需求。此时Flutter工程架构是这样的:




    图一
    此时我们用Android Studio打开和运行工程就可以看到一个Flutter HelloWorld界面。但是现在的Flutter工程还没有跟我们已有的原生工程产生关联,我们接下来需要把这个HelloWorld界面集成到安居客App,并在一个特定的地方显示出来。

    有两种方案可以完成集成。方案一:把已有的Android、iOS工程,按照一体化的工程规范,作为SubModule集成到Flutter工程中。方案二:把Flutter实现的业务模块当作原生的SubModule集成进来。基于安居客App现有的面向服务的成熟框架,我们选择了方案二,维持原来的开发和工程管理方式不变,集成成本相对小,进而在安居客App中形成3端分离的组织形式。此时的三端依赖关系如下:





    图二
    当我们决定采用这种组织模式之后,我们就可以参考Magpie:混合开发工程化框架介绍的内容,分别把Magpie SDK依赖和Flutter SDK集成进各自的Android和iOS工程中。这里的Flutter SDK是我们通过flutter build aar/ios-framework生成的业务SDK。我们先通过手动的方式拷贝到各自工程并像普通aar和framework一样引入项目中。为了简化本文的图例,我们将以Android端做重点介绍,iOS端思想需要保持一致。
    按照统一的Magpie SDK readme集成完SDK后,接下来我们就需要把Flutter侧开发的界面在App指定的位置显示出来了。这就需要根据业务需求封装各自的Flutter界面载体。以安居客App “有料”Flutter为例,需要把FlutterView包装成Fragment在首页Tab中显示。效果图如下:




    图三

    这里顺便介绍一下“有料”模块,这里承载了房产行业经纪人、置业顾问、自媒体KOL等各种角色的内容和形态,是App中UI组件最丰富、业务迭代比较快的模块。而且,App首页打开时就需要加载该模块。基于这些业务特性,值得我们思考,是否需要引入Hot Reload和跨平台技术,相比较存在一定渲染效率问题的RN技术,Flutter较优的渲染性能对于我们的跨端技术实战有更大的可行性。下面是获得Flutter侧指定界面的代码:

    FlutterFragment flutterFragment = new FlutterFragment.NewEngineFragmentBuilder().url(“youLiaoPage”).build();
    利用Magpie SDK提供的接口,让我们可以便捷的访问Flutter内容的指定界面,”youLiaoPage”是我们在Flutter侧的路由命名。在我们把Flutter对应的界面封装到FlutterFragment载体后就可以像普通Fragment一样添加到项目任何位置。经过上面的步骤,原来在Flutter一体化工程现实的HelloWorld界面就搬迁到安居客App端显示了。


    2.  Flutter模块与安居客模块化工程结合





    图四
    以上是安居客App目前简化版的架构。安居客App是由提供基础能力壳工程(或者叫宿主工程)和各可复用的业务模块SDK组成的。基础层的Router和EventBus提供了各模块之间路由注册、路由访问和数据通信的能力,他们共同搭建了面向服务的架构就像后端SOA架构一样,他俩组合完成了企业服务总线的工作,然后各个业务模块只要遵循架构协议来开发,通过总线就可以对其他模块界面和能力进行访问,并获得期望的返回数据结果。

    那我们需要集成来的Flutter业务SDK就需要去按安居客原有协议封装,并向总线注册和发布自己的能力,这里面我们只需要把FlutterFragment封装到Activity,再加上Router的注解协议,就完成了对外提供界面能力的封装,当Flutter侧处理完成一个业务逻辑需要把结果返回给调用者的时候,就需要用MethodChannel去对接公用Response Case再通过 EventBus把返回数据发送给监听者。Response 内容我们可以像后端一样采用Json数据的格式统一封装,监听者再经过反序列数据,从而获得Flutter返回的结果对象。


    3. Workflow工具链的使用

    当我们在原生App里面可以正常跳转Flutter界面并能获得Flutter侧返回结果后,我们就可以体验Hot Reload的能力了,比如把原有Hello World改成Hello。(注意这边我们只能使用Flutter Debug版本才可以体验Hot Reload)我们在命令行就需要输入flutter attach、并不停的在命令行输入r或者R来热重载或者热重起整个业务观看修改后的效果。当然不支持热重载的场景有Widget状态不兼容、全局变量和静态变量的更新、main和initState方法的修改、枚举值修改等场景。当界面修改完成,我们又需要输入flutter build aar/ios-framework打包,再把打包产物拷贝到原生工程中。大家可以看到这整个调试和集成过程都是固定的,这就给了Magpie Workflow发挥提效的场景。Magpie Workflow把固定命令转变成了固定的可视按钮。如下图所示:




    图五
    Magpie Workflow把固定的SDK包迁移变成了往maven仓库直接发布版本的模式。原生App可以在Flutter业务Release正式发布后,简单修改版本号就可以完成新功能的集成更新。如下图所示:




    图六
    我们可以看到Magpie Workflow很好的通过可视化和自动化的方式提升了整个Flutter开发过程中的体验和效率。

    4. 混合界面管理

    随着Flutter页面和模块的增多,怎么管理这些界面的交互就成了我们急需要处理的问题。例如点击一条Push消息想进入到flutter显示的界面,我们改怎么处理呢?




    图七
    以上是安居客App中处理混合界面调用处理的示意图。示例图中有A、B 两个Flutter实现的模块,C为原生实现的模块。我们从图下方的Router Center开始看,在项目中当一个界面需要跳转时我们会统一发送到Router Center中处理。Router Center中我们存在了两类注册表,一个是Flutter模块入口界面注册表,一个是原生模块入口界面注册表。注册表中我们不需要把所有界面注册进来,只需要把模块输入的入口界面注册进来就好。一个界面跳转我们会在Flutter注册表里面查找,如果查找不到我们才转到原生Router处理。如下图八所示:




    图八
    Dart侧代码中会把对原生提供能力的路由通过Magpie PageBuilder注册起来。重点提一下Dart侧我们把路由分成两类,一类是对外路由一类是内部命令路由。内部路由通过下图routes内进行注册。




    图九
    安居客Dart部分的路由访问是采用命令路由的方式完成,可访问的界面都需要写到上面的routes里面,然后通过Navigator.pushNamed(context,”detailPage”)来跳转目标界面。命令路由通过名字标示的方式来做内部界面的区分,可以很好的完成各个界面的解偶,而不用在当前界面import对应的目标界面。但是这种方式又给我们带来了一个新问题,当我们访问的目标界面不存在需要怎么怎么办?这个时候我们就需要增加一个类似404的界面,我们可以在onUnknownRoute中提供统一预警的404界面,当Dart业务侧想跳转到原生界面怎么办呢?在Dart侧代码中,我们通过调用Magpie.singleton.open(原生目标界面的路由协议)来完成原生模块界面的跳转。如果是另外一个Flutter模块页面,Router Center就会重新启动新的Flutter载体来完成目标模块的加载和界面的跳转。

    5. 主题界面管理

    前面的介绍,让我们已经可以完成界面间跳转能力的搭建,粒度是大界面级别的,当我们开始往界面里面添加视觉元素的时候,第一个想到的是我们原生的视觉规范怎么在Flutter模块中继续使用,要不然做出的东西UED验收就比较艰难。
    我们在第二节简单介绍了安居客App的架构思想,安居客App经过模块化改造后,各个高内聚低耦合的模块都已经可以单独编译、开发、集成,关键在2019年经过“木星计划”之后,各可复用的业务模块都支持在58同城App和安居客App中独立集成使用,”有料“模块就是其中一个模块。所以新的Flutter化的“有料”界面就需要可以在58同城和安居客App中做跨应用的主题切换,这就让我们必须做好主题颜色和平台图片可动态切换的设计,此外还需要考虑暗黑模式的支持。我们在第一版中暂时先不考虑两套主题和图片都存在一个包中带来的App整体体积增大的问题。我们在针对跨App和暗黑模式支持设计在Dart侧采用了如下的设计:
    • 主题工厂的思想,根据项目的名字和暗黑场景来创建Flutter业务当前使用的主题





    图十

    • 重新封装图片展示接口,根据项目区分引入图片资源





    图十一
    • 后期优化

    1. 原生注入主题,Dart只保留一份默认主题值。跟原生一起共享编译一套主题。

    2. 不同项目加上暗黑支持,一个图片多的会需要4个形状一致只是颜色有差异的图片,我们考虑内置像换色算法,支持一张图片不同场景的变换


    6.  界面快速搭建与Magpie Fly
    我们在获得与原生一致的主题参数之后,我们就可以开始动手搭建整个界面了。一个界面我们平常会分成小的的UI组件开发再去组合成一个完整的布局。每一个UI组件都需要自己从零开始开发吗?显然不是。我们通常的做事流程是看自己项目里面有没有可复用的,如果没有在看公司级的产品有没有可复用的,如果再没有就要看外部的github、论坛有没有介绍类似的UI组件,如果都没有那就悲剧了,需要自己从零开始做了。我们前端的大部分时间都在不停的跟UI打交道,如果UED设计的组件我们找到可复用的对象,那就大大提升了我们的开发效率,所以Magpie Fly开源项目的宗旨就是希望我们把更多的可复用的组件能开源出来,方便前端开发者最大程度的复用。
    回首上面的图三“有料”的界面,我们可以从Magpie Fly项目中找到下面的组件,经过简单的配置组合就完成了界面的大体的框架。

    SearchInput





    图十二

    HorizontalScrollView





    图十三

    BannerWidget





    图十四

    RefreshHeader





    图十五
    通过了上面的可复用UI组件,我们再集合在自己项目的主题参数就可以快速搭建UED需要的效果。在一个项目中,乃至在一个模块中都会存在可复用的UI组件,这也给我们提供了往Magpie Fly反哺的机会。如下图是“有料”列表内的一个组合的卡片,在我们没有把一个复杂的卡片切割成更小的UI组件时,一点卡片的小修改都会造成新增一种卡片类型,最多的时候内置了六七十这种卡片。后来我们通过切割标准化小组件的模式,通过后端下发json来动态配置卡片的展示,虽然小组件多了但是小组件的复用率提高了好多倍,而且产品后续自己可以配置想要的卡片组合效果,大大减少了前端的开发时间。




    图十六
    上图中的房产经纪人或者顾问展示,中间的视频播放,我们就可以抽取出来提交到Magpie Fly项目维护,形成开源的标准化UI组件。Magpie Fly 提交规范请见关注后续文章《让组件飞:Magpie-Fly详解》 。

    7. 埋点与Magpie Log

    当我们完成一个需求的交互和视觉开发后,不可避免的还需要把产品需要的埋点需求导入到Dart侧代码中。我们把埋点需求分成三大类:页面浏览、卡片曝光、点击。我们怎么在Dart代码中把这些类型的埋点需求完成封装以及把采集的数据怎么对接到原生已有的日志系统中呢?这个时候我们的 Magpie Log项就应需而生了。Magpie Log使用关键方法拦截和结合Redux状态管理框架做到了无痕埋点和圈选埋点的效果。
    Magpie Log 初始化最好放到入口界面build方法中配置,主要是使用Navigator的Context,否则圈选功能会出现异常:




    图十七
    由于我们选择的是使用原生日志管理系统,为了避免频繁的跟原始产生信息交互,我们默认使用定时的方式往原生传递数据集合。Magpie Log为我们封装了原生监听方法,我们只需要如下图设置本地日志监听,把数据传入自家的日志系统即可:




    图十八
    接下来我们再看一下Magpie Log怎么辅助我们解决产品提出的三个维度的埋点需求

    页面浏览埋点

    首先我们需要在Dart侧的代码中,配置全局navigator跳转监听。




    图十九
    接着我们就可以通过我们的圈选界面,配置我们采集的界面ID,名字等信息,这些信息会一起打包到日志中去。如下图两个界面设置:




    图二十

    卡片曝光

    卡片是我们List里面的一个Item或者叫Cell,映射到Dart侧,我们需要针对不同的卡片实现类型(StatelessWidget和StatefulWidget)进行埋点,不管哪种我们都可以依赖Redux框架来处理。先按照下图的代码配置全局状态管理Store。(详细的代码实例,可以参考 https://github.com/wuba/magpie_log 地址Magpie Log项目Demo)。




    图二十一
    • StatelessWidget 类型

    一般只有纯信息展示和点击跳转,我们的埋点数据可以在build方法里面,使用抛出Action的方式,在reducer内统一处理埋点信息。如下图发送一个埋点Action:




    图二十二
    • StatefulWidget 类型

    Magpie Log给我们提供了自动采集的方式,我们需要做的只是按照Redux规范编写代码即可。此外如果觉得有些数据不需要采集,可以在调试界面中(例如图十九)通过勾选需要埋点状态参数,做到控制需要关心的数据。

    点击

    我们也有两种处理方式,各位同学可以根据自己的需要来处理:
    • 彻底把的Widget与绑定的业务处理分离,通过Redux Action的方式继续借助Redux框架来统一处理。发送一个Action,如曝光埋点中的图二十二Demo。

    • 手动埋点

      Magpie Log 考虑到有些场景自动化的日志采集不能胜任的情况,所以同时设计了开发者手动埋点的接口,代码如下:

    MagpieStatisticsHandler.instance.writeData({'data': '手动埋点数据示例'});

    程序异常捕获

    除了产品关心的日志,我们开发还需要关心代码的质量,Dart代码的异常信息也需要及时上传到各自的日志系统的,以便开发能及时处理问题,进而改善用户的使用体验。
    Dart 采用事件循环的机制来运行任务,所以各个任务的运行状态是互相独立的。也就是说,即便某个任务出现了异常我们没有捕获它,Dart 程序也不会退出,只会导致当前任务后续的代码不会被执行,用户仍可以继续使用其他功能。Dart 异常,根据来源又可以细分为 App 异常 和 Framework 异常。Flutter 为这两种异常提供了不同的捕获方式,接下来我们就一起看看吧。
    • App 异常的捕获

    App异常就是应用代码的异常,通常由未处理应用层其他模块所抛出的异常引起。根据异常代码的执行时序,App 异常可以分为两类,即同步异常和异步异常:同步异常可以通过 try-catch 机制捕获,异步异常则需要采用 Future 提供的 catchError 语句捕获。同步的 try-catch 和异步的 catchError,为我们提供了直接捕获特定异常的能力,而如果我们想集中管理代码中的所有异常,Flutter 也提供了 Zone.runZoned 方法。我们可以给代码执行对象指定一个 Zone,在 Dart 中,Zone 表示一个代码执行的环境范围,其概念类似沙盒,不同沙盒之间是互相隔离的。如果我们想要观察沙盒中代码执行出现的异常,沙盒提供了 onError 回调函数,拦截那些在代码执行对象中的未捕获异常。在下面的代码中,我们将可能抛出异常的语句放置在了 Zone 里。可以看到,在没有使用 try-catch 和 catchError 的情况下,无论是同步异常还是异步异常,都可以通过 Zone 直接捕获到,然后统一转接到Magpie Log手动埋点接口传入Magpie Log系统,统一处理代码如下图:




    图二十三
    • Framework 异常的捕获

    如果 Flutter 框架引发的异常,通常是由应用代码触发了 Flutter 框架底层的异常判断引起的。比如,当布局不合规范时,Flutter 就会自动弹出一个触目惊心的红色错误界面。为了集中处理框架异常,Flutter 提供了 FlutterError 类,这个类的 onError 属性会在接收到框架异常时执行相应的回调。因此,要实现自定义捕获逻辑,我们只要为它提供一个自定义的错误处理回调即可。我们只需要在main方法里面添加如下代码:
    FlutterError.onError = (FlutterErrorDetails details) async { //Magpie log手动埋点接口};
    然后同样转接到Magpie Log系统。
    这样我们代码异常捕获和日志上传就搭建好了,我们可以在自家日志平台上像处理原生异常一样,去处理Dart侧代码异常。

    前面简单介绍了Flutter在安居客有料频道的实践,从实际需求出发,完成了业务全链路闭环的落地实施,也充分验证了我们对Flutter的理解和思考。通过这次全面的实操,也让团队拥有了新的能力,在实际业务场景中,除了原生开发、RN,又多了一个技术抓手。后续,我们会结合业务实际场景,持续推进应用实践。

    在实际落地Flutter后,我们还需要综合考虑另外几个维度。例如包大小,内存回收,原生数据和Dart侧数据的转换效率等等。这里面又给我们提供了对Flutter框架和设计优化的空间,相信更多的团队加入Flutter阵营才能使它更好的满足项目需要。


    Flutter化“有料”体验版本下载






    作者介绍

    王海君 房产技术部 Android架构师,在Magpie项目中承担Workflow 脚手架模块开发 和 Magpie四个开源项目在安居客App“有料”模块中落地联调和开发。


    参考文献

    1、陈航老师的《Flutter核心技术与实战》
    2、小徳老师的《Flutter完全手册》

    END

    阅读推荐
    Go服务在容器内CPU使用率异常问题排查手记
    开源|Magpie:平台工具链开发实践
    开源|Magpie:58 跨平台技术应用及 Flutter 实践概览
    开源|WPaxos:一致性算法Paxos的生产级高性能Java实现
    开源|dl_inference:通用深度学习推理服务

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存