6

Flutter 混编工程的模块化架构设计实践

 3 years ago
source link: https://my.oschina.net/SwiftOldDriver/blog/5163767
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

老司机技术周报与淘系技术联合主办了今年的淘系技术.T 沙龙杭州专场。本次沙龙邀请了 4 位国内嘉宾,特邀了 2 位国外嘉宾。Jackie 受邀为大家分享【Flutter 混编工程的模块化架构设计实践】,老峰基于这次分享视频为大家整理此文,辛苦二位!阅读原文,获取 PPT!

讲师简介:Jackie, iOS 老司机,苹果发烧友,喜欢科技新事物,有一定设计洁癖,在大前端方向不断探索,目前在有赞负责移动技术团队。

编辑简介:老峰,老司机技术周报编辑,《iOSTips》作者。

业务模块化

f7f82d64-eaaf-4fda-9621-9c943838a6f0.png

大家好,我是 Jackie,今天给大家分享下 Flutter 混编工程的模块化架构设计在有赞团队中的实践。业务模块化并不是一个新事物,稍微有一定复杂度的应用都会采用模块化的架构,首先我们回顾下 iOS 原生业务是如何做模块化架构的。

ea22816e-ea54-4f2b-94bf-a19b5a6cdd02.png
我们今天聊的主要是业务模块化如订单模块、会员模块、商品模块,并非功能模块如网络库、图片库。业务模块化主要解决的是混乱的模块间依赖关系以及模块解耦后的通讯问题并规范模块架构设计。 目前业界模块化常见方案有蘑菇街, CTMediator, BeeHive 等,有赞基于自身业务需求及特点完成了自己的模块化框架 Bifrost(雷神里的彩虹桥),以外观设计模块架构,拆分 ModuleService 和 ServiceImpl,各业务模块依赖 ModuleService,ModuleService 包含服务接口,模型接口,路由声明,消息通知等,各模块通过 ServiceImpl 实现自身对外提供的服务。

19ac3807-2ad1-4ba5-8327-e73b5e7e23f6.png

Flutter => 新问题

原生模块化方案比较成熟,一直持续支撑有赞各业务有序演进,直到项目中引入 Flutter 后,开始暴露出了新问题。首先第一个问题 Flutter 侧未遵循模块化设计,它属于架构设计上的退步,问题原因可能在使用新技术的时候基建不够成熟,整体架构思考不够,导致 Flutter 模块之间的通讯把代码下沉到 common;路由注册等业务代码直接写到 App 壳工程 main.dart 文件中等明显的架构问题。其次 Flutter 调用原生模块的服务随意写 channel,散乱,重复。

17fd6c04-487b-4de3-ba06-f6bc53615023.png

跨栈场景下的服务暴露与调用

那么问题来了, Flutter 侧模块化架构设计如何优化?其实也很简单,原生部分已经有一套相对成熟的模块化方案了,只需要在 Flutter 侧扩展镜像一下,在 Flutter 侧增加 Mediator 层,Mediator 层负责相关模块间的跨模块通信,包括 Flutter 与 Native 两侧 Mediator 的 channl 通信。
98f6b09b-dea9-4c0e-871a-8c10eca3d4ff.png
单从 Flutter 侧模块化架构优化来讲,那么到这一步问题已经解决了。但是对于业务方的同学来说如何保证 Flutter 及原生侧不同业务线间跨端通讯的 channel 代码可以做到同步更新,如下图所示业务方会同时进行 N 个项目,而且业务方在专注于自己的业务开发时需要分散精力去更新 channel 代码。
044a3cf8-be6e-4d26-9511-b04e620f6328.png

基于这样的背景,业务侧同学提出是否可通过自动化生成 channel 代码?

可是实现自动化很多挑战:

源码解析:语言多,语法特性多

多端差异:iOS、Android、Dart有较多语法差异和设计差异

规范落地:新的设计规范的落地问题

当然自动化带来的价值也很大:

聚焦:原生项目的同学聚焦原生部分,不需要分散精力额外去实现 Flutter 侧服务。

质量:避免某一侧有变动,另一侧没有及时同步引发线上问题

效率:跑在业务前面,需要用时有现成的

面对挑战与价值,团队内部进行多次讨论,最终有赞决定做有价值且存在一定挑战的事,接下来就是将复杂问题拆解为源码解析、多端差异、规范落地,然后具体看一个个问题如何解决。

服务自动同步-源码解析

首先第一步是源码解析,如果不熟悉源码解析的话,可能会觉得这是一个很复杂的问题,但是如果有相关经验或者学习过编译原理的科班同学对这块内容并不会陌生,这里抛出一个概念 AST(抽象语法树,Abstract Syntax Code),它主要用于编译原理中,它可以把各种编程语言源码通过词法分析生成 AST 。

AST:在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

在如下的例子中可以将下面三行代码转为下图中的语法树,可见使用语法树可以方便的分析不同语言的代码差异。

72b52bbe-d3b6-4be7-a943-f039dfd66e11.png

e814296e-07fc-4a9a-8ab7-9e81a9a6147e.png

通过 AST 工具将 iOS&Android 源码生成 各自的 AST 数据,通过对臃肿庞大文件做一些必要精简优化处理生成数据文件,采用 diff 和 merge 算法将各自的数据文件合并为最终的数据文件,然后基于数据文件分别生成 iOS 、Android、Flutter channel 代码,基于 AST 的 channel 代码解析流程如下图:

e152fe5a-d968-4234-8b3a-f9e4f80c5d97.png
上文中提到的 AST 常用工具有很多如:ClangAST,Lex+Yacc,Antlr 等。iOS 同学可能对 Clang 比较熟悉,熟悉编译器的同学对 Lex+Yacc 也不陌生,我们今天要介绍的主角是 Antlr,Antlr 是我们源码解析最终采用的方案,主要是 Clang 对 Android 支持不是太友好,Lex+Yacc 文档又少,而 Antlr 全语法支持,也方便扩展自定义语法描述文件,文档齐全,例子众多,同时可方便的自定义语法描述文件。基于 Antlr AST 工具在实现服务自动同步-源码解析过程中发现其实比预期快很多,难度也没那么高。关于 AST 这块由于时间关系,实践也没太多坑点,这里不展开做更具体的探讨,对 Antlr 感兴趣的同学可以下来深入研究,用起来真香!!!

服务自动同步-多端差异

接下来第二步是多端差异,这块涉及的坑比较多。首先很明显的问题是  Swift 、Java、Kotlin、Dart、Obj-C各语言基础类型不一致,需要通过表格梳理,在生成源码时特殊处理。
c0a161aa-b37a-4c93-8009-28f7b044eb22.png
第二个相对复杂的问题是,复合类型处理,如模块间通信涉及模型的传递,这里基于安全稳定性来说肯定不适合传字典,传对应实体对象对上层业务方来说也更友好,比如如下图的例子:

fd063d39-1287-4b6a-9ee2-043e8d297da3.png

用户模块的接口 getUserById 它返回的是用户模型的实体,在 Flutter 侧如何用呢?我们肯定希望直接通过 getUserById 可以获取 User 模型,但这里有个问题 User 是 protocol 动态的,无法确定他的实现实体,所以无法为模型接口实现 channel 方法。

ff67fde2-478d-4212-9b45-2a1dc7ff3da0.png

那么如何解决这个问题呢?我们思考一下,通常我们手写 channel 时,会把原生做一个序列化,然后传到 Dart 侧,那么这里自动化方案也类似模仿手写过程,将原生侧自动化做序列化,然后传递数据,在 Dart 侧创建相应的模型类,自动反序列化,特别的这里的模型只提供 get 方法。 接下来第三个涉及差异且相对复杂的问题是特殊类型处理,比如 iOS 侧有 Block,对应的 Dart 侧是怎样的实现方式?我们首先看下面的例子:
60447f35-9930-4dd7-9b7d-60d269801f65.png

77e71358-41f7-49a4-a4d7-61a80ac9916b.png

0b594c24-10ca-4586-bfb0-5e71272c8439.png
iOS 原生侧登录服务,他涉及登录成功失败的一个回调,Android 会涉及 Observable 这样的实现,原生侧通过一定的规范约定还是比较容易对齐,iOS 侧 channel 的代码实现:
a88b77f1-8969-4996-8de7-52d63cf948c7.png
但这里有个问题一个方法包含多个 block 怎么办?当然通过一定的封装可以解决,那还有一个问题原生侧 block 没有被执行,await不就卡死了吗?所以基于以上场景最终在 Flutter 侧使用 Function 实现异步。
08f28124-8fa7-4925-ac34-6e331ab0ae47.png

多端差异还有一些其他的细节问题处理比如接口注释信息,AST拿不到,通过方法关键字去源码文件中反查;原生侧特殊对象,如 UIView, UIButton 等,在设计初期 case by case 特殊处理等。

服务(接口)同步问题-规范落地

老代码怎么办?
在业务开发中 channel 同步涉及的老代码怎么办?比如业务侧 iOS 涉及很多老代码改动需要花费较大人力。 方法名问题:提供方法名注解,可以指定 Flutter 侧方法名,原生侧方法名不需调整。 设计实现问题:比如 iOS 实现方法为一个,Android 侧实现为多个方法,这种涉及方案调整的通过提供 Ignore 注解,暂不自动同步,允许业务方按节奏推进。 新代码怎么办? 业务方的同学可能会担心方法设计方式上是不是受到很大制约?的确在实践中过程中也看到了,很多端上的特性,Dart 侧是不支持的,那么只能通过不断迭代优化 + case by case 解决,但我们的接口都只涉及对外提供的服务,本身量不是那么多,整体收益上来说这块增加的还是可以接受。 另外怎么确保 iOS 和 Android 在方法设计上的一致?各端都各自实现出现差异谁来改呢?这块主要通过参考过往跨栈技术方案实施,如路由以及后端接口等通过文档规范,在技术方案中约定好。

自动化同步任务

解决了源码解析,多端差异问题后也就到最后一步了,接下来就是自动化同步任务实现,目前支持多种触发形式,暂时没加入 CI 环节。自动同步任务还涉及异常处理如差异异常处理,自动化测试,告警通知等,整体流程如下图:

b23fde18-d293-4592-aa82-1ac3eb980cf6.png

模块间通讯之外

分享开始我们聊了模块间通信的问题,模块化框架解决的其实不仅仅是模块间通信的问题,还要提供一个标准的模块设计规范,比如很多时候会把自己业务代码写到别的模块,这里我们对 Flutter 侧也提出了明确的模块设计规范 。

自己的业务代码写自己模块,禁止写在 main.dart/app.dart 中

模块注册:基于 App 的 pubspec.yaml 中的依赖解析,生成 Module.dart 文件

路由注册 :通过注解进行注册, 通过 Mediator 对外提供。

Bifrost 也会提供 App 关键生命周期信息的监听方法。

b9e88048-1b64-45aa-b3de-d9f6d3d68e45.png

总结与展望

到这里我们 Flutter 混编工程的模块化架构设计一期工程也就结束了,其实呢整个方案还有一些优化点,比如目前我们只实现了原生到 Flutter 侧的单向同步,还未实现  Flutter 到原生的同步,相信未来随着 Flutter 侧业务演进,也会有越来越多的 Flutter 到原生的同步,所以接下来我们也会实现 Native 和 Flutter 的双向同步;第二点会提供更多的语法和类型支持;最后调用性能优化,目前基于 channel 通信,未来也会探索其他性能较好的其他通信方式。最后整个方案落地后团队内还是有明显的提效,主要体现在开发和测试的双提效。

对这次分享感兴趣的朋友可以加入讲师所在的开发团队,一起推进  Flutter 基建 的落地~

bb9b4127-dda2-4509-91bf-373b4f14903d.jpg


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK