0、引言
在接入Flutter技术时,通常都会遇到原生工程和Flutter工程混合开发的问题,即便是新项目从一开始就采用Flutter开发,可能会由于部分功能Flutter无法实现、效果不好等原因,也会需要考虑混合开发的问题。
本文给大家介绍一套混合开发实现方式,包括工程集成打包、开发调试,Flutter基础组件选型和技术架构方式,支持原生、H5、Flutter(或其他容器)的跨容器互通的页面跳转和事件路由,使用Provider实现一套类似于原生开发的MVVM业务框架,以及Flutter侧的组件化实现。我们团队从2019年8月开始接入Flutter,经历了1.9.1、1.12.13、1.17.5 Flutter版本升级,今年6月底已经实现80%的页面使用Flutter替代,目前在做Flutter侧组件化拆分和复用,实现一套代码开发Shein、Romwe两个APP(Android+iOS)4个端的能力。
1、集成引入
集成方式
混合工程避不开Flutter和原生工程集成的问题,在1.12之前的Flutter版本中官方只正式提供源码集成的方式,Flutter编译产物(Android的AAR,iOS的Framework)的集成方式还处于预览版,有一些bug需要自己解决,在1.12及之后的版本中,官方提供了比较完善的源码集成和编译产物集成方式:https://flutter.cn/docs/development/add-to-app 。
源码集成方式适合flutter和原生代码互相调用的开发,方便开发调试插件,尤其是Android端可以在一个Android Studio IDE窗口下编辑原生和Flutter代码,非常方便,但如果依赖的插件较多,每个插件都包含一个Android Module工程,工程结构会较为复杂,也会一定程度影响编译速度。
Flutter编译产物有3种编译模式:debug、profile、release,debug模式编译产物适合纯Flutter侧代码的开发、调试,profile的用来做性能分析和测试,release的用于打包发布。这和原生工程的编译配置可以一一对应,在开发、测试、发布时方便实现切换。
集成问题
官方提供的Flutter编译产物集成方式是在本地集成,而混合开发时,原生侧的开发是不希望依赖Flutter环境的,纯Flutter侧的开发也希望能有一个编译好的debug版本的APP,然后使用Attach命令连上开发机即可进行开发(支持hot reload),测试和发布阶段也希望有一个统一的持续集成环境,需要考虑原生工程依赖Flutter编译产物的版本问题,比如提交了Flutter代码到Git仓库,想打包一个新版本APP包含刚才提交的Flutter代码,是否能不用手动修改依赖的Flutter产物版本号即可方便的打包。另外,还有多版本并行的问题,Git分支如何支持等等。
Git分支
要解决这些问题,首先要考虑的是Git分支模式的选择,分支模式会影响到整个开发和集成发布流程,主流的Git分支模式如Git-Flow、GitHub-Flow、GitLab-Flow等,这些模式如Git-Flow对于APP版本迭代来说过于复杂,GitHub-Flow则不能很好的支持APP多版本并行等特点,而GitLab-Flow对于大型APP团队较适合,对于中小型APP研发团队2-3周一个版本的迭代来说,有一定的成本和复杂性,具体选择需要根据团队规模、工程结构、业务特点来决定。我们团队使用的是类似于GitHub-Flow的版本分支模式,只不过把feature branch当成了版本分支来用(如v1.2.3),master只作为版本记录用。版本分支可以有多个并行,当一个版本开始时,从master上拉取最新的代码创建版本分支,版本开发、修改bug、发布都在版本分支上完成,发布后再合并到master分支和其他正在进行的版本分支,如下图:
混合工程至少会有两个工程,一个原生工程、一个Flutter工程,所以两个工程都使用相同的版本分支,一一对应,这样做的好处是本地源码集成或远端Flutter产物集成时,切换版本都会比较简单方便。本地源码集成切换版本只需要同时切换两个工程的Git分支,而远端Flutter产物集成可以根据APP的版本参数,依赖不同版本的Flutter产物文件夹目录。
集成打包
在开发同学提交代码到Git仓库时,可以由Git服务推送通知Jenkins打包服务开始打包,也可以由Jenkins打包服务手动或定时触发打包,首先需要拉取原生工程和Flutter工程相同版本分支的代码,可以使用Jenkins的Multiple SCMs plugin,这个插件支持拉取多个Git仓库源。更新到对应版本分支的最新代码后,就可以开始打包了,打包过程主要有两个阶段,编译Flutter产物阶段和编译APP安装包阶段。
编译Flutter产物阶段大致步骤如下:
- 清理Flutter工程编译输出目录
- 执行flutter pub upgrade更新依赖的flutter组件
- 执行flutter build aar(Android) 或 flutter build ios-framework(iOS)编译Flutter产物
- 上传Flutter产物到FTP服务器(或任意支持http下载服务)
- 更新原生工程依赖的Flutter产物版本号(这里依赖的是第四步上传到FTP服务上的产物)
第五步iOS端:
需要在构建脚本中更新原生工程Podfile依赖的Flutter产物版本号,并提交更改到Git仓库。
pod 'flutter_module', ‘1.2.3.1001’
可采用4段式的版本号命名,如“1.2.3”是正式的版本号(也是git分支名),第四段“1001”是build号,每次构建+1。
第五步Android端:
可以使用changing标记实现sync/clean时自动拉取最新aar,而不必每次修改版本号,如下图是上传到FTP服务器的AAR产物文件夹目录(按照版本划分):
1、依赖远端AAR:
repositories {
maven {
// 工程的versionName和版本分支名称一致,和Flutter产物版本文件夹名称也一致,ps:1.2.3
url 'http:///flutter_module_android/' + rootProject.ext.versionName + '/repo/'
// 依赖本地工程编译产物,方便开发调试
// url '../../flutter_module/build/host/outputs/repo'
}
maven {
url "https://storage.googleapis.com/download.flutter.io"
}
}
2、依赖的flutter aar 标记 changing = true:
dependencies {
debugImplementation ('com.example.flutter_module:flutter_debug:1.0’) { changing = true }
profileImplementation ('com.example.flutter_module:flutter_profile:1.0’) { changing = true }
releaseImplementation ('com.example.flutter_module:flutter_release:1.0’) { changing = true }
}
3、修改aar缓存策略:
configurations.all {
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}
将Flutter编译产物上传到FTP服务,就可以开始原生工程的编译打包过程,介绍原生打包的文章比较多,这里不再赘述,原生打包可以根据需要实现debug、profile、release等不同编译模式的包。Flutter侧开发可以直接使用Jenkins打出的debug包进行开发调试,原生侧开发可以依赖FTP服务上的Flutter编译产物开发,不需要Flutter开发环境。多版本并行时,Jenkins也可以支持不同版本的打包和发布。
开发环境
为了保持一致的开发环境,方便团队开发,可以把flutter sdk以Git submodule形式集成在Flutter Module工程内,可以使用Flutter Wrapper(https://github.com/passsy/flutter_wrapper)工具方便的管理sdk版本,这样做的好处是sdk版本和工程代码是关联的,当升级sdk时,意味着这个工程依赖的sdk升级,并且通过Git同步给整个团队。
2、基础组件
UI组件:Material vs Cupertino
众所周知,Flutter提供了Material和Cupertino两套UI风格组件,而国内很多APP设计风格是偏向于Cupertino风格的,似乎我们要使用Cupertino组件来构建页面UI,但如果这么做,很快就会发现Cupertino组件相比Material组件实在太少了,而且一些基础Widgets组件在CupertinoApp下的默认样式很难看,需要做额外的处理。
那么,是否可以使用Material组件,并统一修改风格为我们需要的样式?答案是可以的。
风格差异较大的地方主要是这几点:
- 水波纹效果
- AppBar、TabBar等组件的高度
- 加载指示器
- Navigator跳转页面过渡动画
第一点:水波纹效果,可以通过如下代码去除
MaterialApp(
theme: ThemeData(
// 禁用水波纹效果
highlightColor: Colors.transparent,
splashFactory: const NoSplashFactory(),
primaryColor: Colors.white,
),
)
class NoSplashFactory extends InteractiveInkFeatureFactory {
const NoSplashFactory();
InteractiveInkFeature create({
@required MaterialInkController controller,
@required RenderBox referenceBox,
@required Offset position,
@required Color color,
TextDirection textDirection,
bool containedInkWell: false,
RectCallback rectCallback,
BorderRadius borderRadius,
ShapeBorder customBorder,
double radius,
VoidCallback onRemoved,
}) {
return new NoSplash(
controller: controller,
referenceBox: referenceBox,
color: color,
onRemoved: onRemoved,
);
}
}
class NoSplash extends InteractiveInkFeature {
NoSplash({
@required MaterialInkController controller,
@required RenderBox referenceBox,
Color color,
VoidCallback onRemoved,
}) : assert(controller != null),
assert(referenceBox != null),
super(
controller: controller,
referenceBox: referenceBox,
onRemoved: onRemoved) {
controller.addInkFeature(this);
}
@override
void paintFeature(Canvas canvas, Matrix4 transform) {}
}
第二点:AppBar、TabBar等组件的高度,可以通过继承AppBar、TabBar,覆盖preferredSize解决
第三点:加载指示器,可自行实现自定义效果的LoadingWidget
第四点:Navigator跳转页面过渡动画,可以通过继承PageRoute自定义实现
而AppBar的返回按钮Icon、标题居左(Android) / 居中(iOS) 的差异,可能是我们需要的。若要统一,也可以自定义实现。
混合页面栈
混合开发的APP,页面堆栈的管理一直是比较麻烦的问题,由于APP的主体仍然是原生框架,原生和跨平台页面(H5/RN/Flutter)会出现交叉堆叠的场景,所以需要解决如页面跳转、返回动画,页面生命周期一致性等问题。比较成熟的混合技术如原生+H5混合页面,每打开一个H5页面都是用一个单独的WebView容器承载,优点是实现简单,缺点是H5页面之间不能直接访问数据,需要原生桥接,多个WebView实例消耗内存也较多。
Flutter官方最初给出的解决方案与原生+H5混合是类似的,把FlutterView当成一个容器视图,并且优化了一些场景,当连续打开两个Flutter页面时,第二个Flutter页面可以在当前FlutterView中打开。由于刚开始使用Flutter技术,原生和跨平台页面出现交叉堆叠的场景较多,能够使用优化的场景就比较少了。Flutter 1.12版本中实现了复用引擎的方案,实际使用需要处理很多细节问题,较为繁琐。
闲鱼团队开源的FlutterBoost(https://github.com/alibaba/flutter_boost)帮我们解决了这个问题,FlutterBoost将Flutter页面一一映射到原生页面,从原生角度来看,就是原生页面之间的跳转,如Android端的跳转:NativeActivity —> FlutterActivity(FlutterView) —> FlutterActivity(FlutterView),页面栈的管理就交给原生框架了。而所有FlutterView所关联的FlutterEngine复用同一个,可以减少内存使用,也能正常实现页面/模块之间的数据通信。
跨容器路由:页面路由 & 事件路由
原生侧的页面跳转已有较多成熟的页面路由组件可以实现,通过给每一个页面定义一个URI来实现导航。但从原生的视角来看,WebView/Flutter等技术实现的跨平台页面,相当于一个独立的容器空间,实际场景中会存在大量原生和跨平台页面之间相互跳转,甚至是相互发送广播事件,所以我们需要实现的是一个跨原生、Webview、Flutter空间的页面路由和事件路由组件。
由于FlutterBoost将每个Flutter页面都映射为一个独立打开的FlutterActivity(Android)/FlutterViewController(iOS),这和所有的H5页面通常都由一个公共的WebViewActivity/WebViewController打开类似,需要将所有由Flutter实现的页面URI都指向FlutterActivity/FlutterViewController,并传递原始URI和参数到Flutter容器中,再由FlutterBoost管理的页面URI路由映射表创建具体的Flutter Widgets页面。
实际Flutter开发场景可能是将已经存在的原生页面用Flutter技术重写并替换,考虑到可能的风险,需要实现灰度发布和降级回原生页面的能力。这显然不太可能由每个跳转的业务代码各自去判断,那么,由路由组件实现拦截并动态改变跳转的目标页面则会是比较好的实现方式。
原生页面开发中会存在一些使用事件总线(如EventBus)的场景,部分原生页面转化为Flutter页面后,也会需要接收到原生的事件,这可以通过在Flutter中实现EventBus代理类,并桥接原生的EventBus实现。但这样只是桥接了原生和Flutter的事件总线,还不是事件路由。
页面路由通过定义一个特定URI(如:/module/abc),可以导航页面并传递参数。如果我们把页面路由的概念推广一下,一个事件也定义一个特定的URI(如:/event/login),那么是否也可以使用路由投递事件并传递参数?答案当然是可以的,并且还是和页面路由使用完全一致的API。这样做有什么好处呢?如果路由只是在APP内部使用,确实没有什么用处,但如果路由是APP、H5、服务端一起使用时,则可以实现巨大的灵活性。试想一下,如果所有支持页面路由的地方,还可以无缝支持事件路由,那么原本可能需要APP修改并发版本的功能,现在只需要H5或服务端修改一下路由链接,即可实现H5或服务端向APP发送事件!!(前提是APP中已经实现接收并处理该事件的代码)
事件路由具体的实现方式是,在路由组件中拦截所有路由,判断URI是否是“/event/“开头,如果是,则将当前事件使用EventBus发送出去,并终止路由跳转即可。
图片加载
APP中显示的图片有如下这几个来源:
- 内置在安装包中的图片
- 网络加载的图片
- 手机本地存储的图片(如照片、图片缓存)
这3种来源的图片Flutter官方已经帮我们实现了加载到内存并显示,但还有一些细节问题没有解决:
- 内置的图片,原生和Flutter不能共用同一套,需要分别放在各自工程目录下
- 网络加载的图片,没有本地磁盘缓存,一些第三方图片加载库(如cached_network_image)虽然有缓存,但不能和原生图片加载库共用同一个图片缓存,需要分别下载
- 同一张图片如果原生侧和Flutter侧都加载到内存,需要双倍的内存
Flutter社区对于这些问题涌现了各种解决方案,但也还存在一些瑕疵,我们目前也还在探索中,就目前而言,也有一些方案可以暂时使用。
- 内置的图片,原生工程和Flutter工程可以分别都放置一份,因为安装包是经过压缩的,所以并不会增加安装包的体积,只会增加安装到手机后的存储空间(需要解压),目前手机的存储空间都比较大,这个影响还可以接受。官方虽然提供了原生侧读取Flutter图片的方法(https://flutter.dev/docs/development/ui/assets-and-images#sharing-assets-with-the-underlying-platform),但这种共用方式需要改变原生加载图片的方式,不太方便。有第三方插件(https://flutter.dev/docs/development/ui/assets-and-images#loading-ios-images-in-flutter)可以支持Flutter读取原生资源,只支持iOS端,Android理论上也可以实现,但需要考虑Android和iOS资源一致性的问题,需要的话可以研究一下。
- 网络图片缓存问题,可以通过自定义ImageProvider,将图片加载桥接给原生的图片加载库,加载完成后返回本地缓存路径给Flutter侧,再通过Image.file(File file, ...) 加载到内存中显示。
- 双倍的内存的问题,闲鱼团队分享了一个巧妙的方案(https://mp.weixin.qq.com/s/98BxqW5QDHXLKMwHX_E7EQ),通过外接纹理解决图片内存复用问题,这个方案目前没有开源,似乎还存在一些问题。在我们实际业务中,分析发现,出现重复图片的场景主要是列表页Item的图片和商品详情头图,以及点击查看大图,但列表页Item的图片是较小的图片,和商品详情的头部大图并不是同一个图片文件,所以真正相同图片的场景并不多。而且图片在内存中真正需要显示的也不多,大部分都是内存缓存。所以,如果将图片缓存大小控制在一定范围,并且,对于出现重复图片的场景,要么将这两个页面全都迁移到Flutter,要么就都保持原生页面,则可以暂时规避这个问题。
另外还有一个问题是,Flutter工程内置的图片要放置多套图片的问题,如放置1x图在工程目录的images/cat.png路径,并在images/2x/cat.png和images/3x/cat.png路径分别放置2x和3x图片,使用时可以不指定密度,直接使用:Image.asset("images/cat.png")。虽然使用方便,但这样会增加安装包体积,实际我们可能只会放置两套图,甚至只放一套3x图,但如果省略掉1x的默认图,图片无法显示,而如果把3x图放在1x图的默认路径下,显示出来的图片会非常大,需要手动指定图片大小。
官方文档(https://api.flutter.dev/flutter/widgets/Image/Image.asset.html)中对于省略部分密度的图片有说明,虽然也有一些繁琐,不是很完美
The images/cat.png image can be omitted from disk (though it must still be present in the manifest). If it is omitted, then on a device with a 1.0 device pixel ratio, the images/2x/cat.png image would be used instead.
意思是可以省略images/cat.png图片文件,但是必须在pubspec.yaml的assets中配置完整的图片路径(不能只配置“images”目录),并且每一张图片都需要在pubspec.yaml中声明图片路径
assets:
- images/cat.png
Flutter编译打包时,似乎只会自动识别1x的默认图和pubspec.yaml中明确配置的图片路径,对于2x、3x等其他变体资源不会自动识别。每个图片资源都需要配置会比较繁琐,不过,可以通过实现一个插件自动配置图片路径到pubspec.yaml中。
网络、埋点、数据
混合开发的APP,原生侧通常已经有一套完善的网络、埋点、数据存储等基础组件了,而且这些组件中还会包含一些公共的业务逻辑处理,这些基础组件如果使用Dart语言重新编写一遍,工作量会很大,稳定性也不高,需要时间慢慢完善,后续还需要维护多套代码(Android、iOS、Flutter)等问题。
通过桥接原生侧来实现这些非UI层的基础组件,可能是一个比较好的选择。网络和埋点组件桥接起来比较简单,做好数据的解析和映射就可以了,但数据桥接会比较麻烦。原生侧的持久化数据一般会有数据库存储、序列化存储、键值对存储等持久化存储方式,这些持久化数据通常有统一的访问接口,桥接不难,但可能存在多线程并发读写的情况,也会需要在某一侧更新了数据后,通知另一侧更新数据或刷新页面的场景,而且,还有很多只会临时存储在内存中的业务数据,这些数据分散在各个业务模块中,各自负责管理和更新。
对于数据的桥接,持久化存储的数据,如果有统一的访问接口,并且不存在并发场景时,可以统一桥接底层持久化存储API。如果有并发场景或只会临时存储在内存中的数据,则可以在各个业务模块中各自实现一个单例模式的Manager,原生侧的Manager负责真正的管理和维护数据,Flutter侧的Manager只是一个代理类,通过Flutter MethodChannel桥接实现双向通讯,Flutter侧可以主动调用方法获取数据,原生侧数据有变化也可以主动通知到Flutter侧。
3、架构
MVVM
Flutter社区中,大家似乎不怎么谈论MVVM框架,而是喜欢说状态管理,而状态管理的框架又特别多,如Scoped Model、Provide、Provider、MobX、Redux、Fish Redux等等。这些框架无一例外都基于Flutter内建的InheritedWidget组件提供的数据共享和传递机制,也就是模型数据的变化,会自动刷新UI,再加上Flutter声明式UI的写法,就可以很轻松的构建MVVM中由数据到UI的绑定,而UI接收用户触摸事件到模型的绑定就简单了,只需要在UI触摸回调事件中获取到Model,并调用对应方法处理即可,这样就构建好了MVVM业务层开发模式。相比原生的MVC、MVP等模式要简单不少,而原生开发即便使用MVVM模式,也需要额外实现模型和UI的双向绑定,像Android端虽然有官方提供的DataBinding自动生成绑定代码,但也不如声明式UI这样简洁。
但这些状态管理框架,要么比较重型,使用较为复杂,要么虽然简单(如Scoped Model、Provider),但似乎只适合小型应用开发,其提供的Demo(https://github.com/2d-inc/developer_quest)中只有一个顶层的单一数据源,对于大型应用来说,几百个数据源都写在顶层,维护起来可能是个灾难。
页面级数据源
官方推荐的Provider框架,虽然Demo中是顶层单一数据源,但为何一定要这样写呢?我们在原生开发中不是每个页面、模块独立管理各自的数据吗?在Flutter中也完全可以实现以页面/模块为单位来管理和维护数据,我们只需要Provider提供的数据刷新机制就可以了。
对于不需要跨页面共享的数据,其数据对象实例是定义在Model中的一个个字段,并在页面的根Widget下配置数据源。对于需要跨页面共享的数据,原生开发通常是将其存储在具有长生命周期的单例类或服务中,Flutter也可以使用相同的方法,在所有需要使用共享数据的页面,其Model通过单例类或服务获取数据,并在页面的根Widget下配置数据源即可。
不配置在顶层的原因是,共享数据有时候难以界定,是只要有两个页面共用同一个数据就算共享数据呢,还是需要多个不同业务都使用的数据才算共享数据,具体又如何界定?可能在大型团队里面,每个人都有不同的看法,难以统一。即便统一或团队都认可的界定方式,实际上可能还是有很多共享数据,让维护变得困难。
熟悉Android开发的同学应该看出来了,这和Android官方的应用架构(https://developer.android.google.cn/jetpack/docs/guide)几乎一致,只是在数据层桥接了原生的数据存储、网络组件,实际开发中也可以根据业务情况决定是否需要Repository层来提供统一的数据获取接口。
组件化
原生开发中,当业务规模和团队规模越来越大时,为了更好的多团队协作开发,就有必要做组件化拆分,Flutter开发也一样。对于我们团队还有一个更重要的原因是,我们需要维护Shein和Romwe两个APP,业务逻辑非常相似,但UI风格不同,为了减少维护工作,提高开发效率,我们希望能将每个功能模块拆分成组件,集成时打包不同的资源,实现复用一套代码支持两个APP开发的能力。但如果再加上跨平台技术,就可以实现一套代码支持4个端的能力。
组件集成
Flutter的pub集成工具提供了本地代码集成和远程Git库集成,具备了组件化改造的基础,在工程的pubspec.yaml文件中配置依赖:
#远端集成
xxx_module:
git:
url: https://github.com/xxx/xxx_module
ref: 1.2.3 #版本分支集成,若有更新需要执行[flutter pub upgrade]拉取最新更新
#本地集成
#xxx_module:
# path: ../xxx_module
默认使用远端集成,当需要修改某个组件时,将远端集成改为本地集成即可。另外,也可以使用Git submodule的方式集成。
资源问题
Flutter提供的构建工具没有Android Gradle那么强大的功能,无法实现像Gradle的productFlavors提供的构建变体,所以我们只能自己在打包脚本中实现打包时选择不同的资源。具体实现是,在每个业务工程中,除了有默认的 res/ 资源目录,再创建一个变体资源目录(如 res_romwe/ ),目录结构和文件名称同默认资源保持一致。相同的资源只需要放置一份在res目录中,不同的资源可以再放一份在变体资源目录中,在打包时,只需要将变体资源目录复制并覆盖默认资源目录,再执行正常的编译流程。
图片资源拆分到业务组件工程后,再使用 Image.asset('images/cat.png’) 方式显示内置图片时,发现图片无法显示。这是因为资源和代码一样,都有包隔离的特性,否则,不同的库中资源名相同就会出现冲突或覆盖了(Android采用的是优先级覆盖)。当我们需要调用Flutter SDK或第三方库中的代码时,需要import相应代码文件(如 import 'package:flutter/material.dart’),同理,加载子工程中的资源也需要指定package:Image.asset("images/cat.png", package: "module_name”),即便是在子工程中的代码加载自己工程下的资源也是一样。具体可以参考Flutter文档:https://api.flutter.dev/flutter/painting/AssetImage-class.html。
组件通信
Flutter组件间通信方式,除了页面路由和事件总线,还需要有调用其他组件服务的能力,实现方式和原生组件类似:
业务工程可拆分为Business module和Lib module,应先下沉业务组件,这些组件可以提供给其他业务工程复用,也可以提供接口给其他业务调用,接口具体实现在Business module中。Lib module即能实现模块间通讯,又提供了业务工程和Basic工程的中间层,避免Basic工程的过度膨胀。接口通信优点是实现简单,适合实现一组复杂的数据访问接口,表达强依赖关系,缺点是需要定义和拆分较多的module工程,有一定成本。
组件架构
由于混合开发需要一个Module类型的Flutter工程来实现Flutter部分的编译构建,Flutter侧组件化拆分后,两个APP集成各自需要的Flutter业务组件,需要两个Module类型的Flutter工程,如shein_flutter_module和romwe_flutter_module。另外,Flutter侧有很多桥接原生侧的基础组件,为了避免两个APP各自桥接,就需要原生侧能剥离出统一的Basic工程,实际上就是原生侧的组件化,最终的组件架构如下:
4、总结
与纯原生开发或纯Flutter开发相比,混合开发由于需要打通原生和Flutter的数据和服务,需要有大量桥接实现,各个模块互相协作也需要考虑各种异常或降级的情况,因此需要从整体架构上设计一套完善的跨平台混合开发框架,我们团队还在继续探索和实践。本文分享的一些实践方法欢迎各位同学讨论和交流,谢谢!
作者:李伟