![]()
1.1 接入 Fair
对于 Fair 的接入方式,推荐大家使用 源码依赖 的方式,下载地址如下:
git clone https://github.com/wuba/fair.git
在 pubspec.yaml 文件里添加依赖:
假如 Fair 源码和你的项目代码在同一个目录下
![]()
另外需要特别注意的是,Fair 内部通过 fair_version 库来做版本控制,所以大家需要根据自己本机的 Flutter SDK 版本来决定 fair_version 的版本。
以我的电脑为例子,我本地使用的 Flutter SDK 版本为 2.5.0,所以我强制指定 fair_version 的版本为 flutter_2_5_0:
![]()
1.2 将 Widget 改造为可动态下发的 bundle
将一个 Widget 改造为可以动态下发的 bundle 需要三步。
第一步:改造 main 函数
需要去掉默认的 runApp(MyApp()),然后手动调用一下 WidgetsFlutterBinding.ensureInitialized(),最后用 FairApp 来包裹项目的根 Widget。
![]()
第二步:添加 @FairPatch() 注解
在需要改造的 Widget 上添加 @FairPatch() 注解。
![]()
第三步:执行 build_runner 命令
执行了 build_runner 即可生成 bundle 资源
flutter pub run build_runner build
1.3 bundle 资源生成位置和组成
bundle 资源的位置,位于:project -> build -> fair 下。
![]()
一个 bundle 资源包含了两个文件:一个是 js 文件,一个是 json 文件。
js 文件里面包含的是动态页面的逻辑部分
json 文件包含的是动态页面的布局 DSL
1.4 使用 FairWidget 加载 bundle 资源
bundle 资源生成好以后,我们可以使用 FairWidget 组件对其进行加载。
在本地测试时,我们可以将 bundle 资源拷贝到 assets 目录下,然后将 assets 资源的地址通过 path 参数传递给 FairWidget:
![]()
运行效果如下:
![]()
本地测试通过以后,可以将 bundle 资源托管到自己的服务器上,然后将 path 换成一个 bundle 资源的服务器下载地址即可:
![]()
使用小结
到这里简单小结一下 Fair 的使用步骤:
![]()
对于一个简单的页面可以通过以上步骤进行改造,但是实际的项目中,情况往往比较复杂,所以,我们下面继续介绍在一些复杂的业务场景下,如何使用 Fair。
1.5 动态页面之间的参数传递
Fair 提供了两个注解,可以实现动态页面之间的参数传递:
这两个注解的区别是:
来看看示例代码。
@FairWell 的使用:
![]()
@FairProps 的使用:
![]()
1.6 动态页面之间的跳转
推荐使用 命名路由 的方式进行跳转。
首先注册路由表:
![]()
然后使用 Navigator.pushNamed 进行跳转:
![]()
1.7 动态页面引用本地 Widget 组件:@FairBinding
对于一个本地的 Widget,如果直接在动态页面里面引用的话,Fair 无法完成 DSL 的生成,需要使用 @FairBinding 注解来对本地 Widget 组件进行标记。
![]()
标记完成后,可以运行 flutter pub run build_runner build 命令,运行成功后,会在项目的 src 目录下,生成本地 Widget 的组件映射表:AppGeneratedModule。
我们需要对 AppGeneratedModule 进行注册:
![]()
然后就可以在动态页面里使用了:
![]()
1.8 动态页面引用三方 sdk 的 Widget 组件:@FairBinding(package)
那很多人可能会有疑问了,假如我引用的是一个第三方 SDK 里的 Widget 呢?
我们依然使用的是 @FairBinding 注解,不过需要我们将三方 SDK 的 Widget 的 package 路径传递给 FairBinding:
![]()
1.9 逻辑模版使用:FairDelegate
对于页面中一些固定的逻辑方法,我们可以将其抽离出来,放到 FairDelegate 里面,来降低 Dart 与 JS 的频繁通信,达到提升性能的目标。
甚至一些页面,只需要 UI 动态化,而不需要逻辑动态化,那么完全可以使用 FairDelegate 来实现。
使用 FairDelegate 方式很简单,首先自定义一个类,让它继承 FairDelegate:
![]()
然后,需要在 FairApp 里注册模板:
![]()
1.10 使用 IFairPlugin 桥接常用第三方 SDK 的能力
我们在动态页面中,经常会用到一些第三方 SDK,比如网络请求、权限申请、拍照功能等等。
此时,我们需要使用 IFairPlugin 来桥接第三方 SDK 的能力,才能在动态页面中正常使用。
我们以权限申请库为例子。
首先,自定义一个类,继承 IFairPlugin:
![]()
重写 getRegisterMethods(),在里面注册需要暴露给 JS 侧的方法。
同样的,定义好 Plugin 后,需要在 FairApp 里进行注册:
![]()
然后就可以正常使用了。
Fair 上线后,我们也是在 58 拍客里进行了接入和使用。
我们完成了三个页面的改造:发布页面、视频列表页面、视频详情页面。
并且自己设计了热更新流程,下面,我们就来详细介绍一下。
2.1 58 拍客接入 Fair 前后 UI 对比
接入 Fair 前的页面:
![]()
接入 Fair 后的页面:
![]()
Fair 能做到像素级别的还原,因此接入前后,页面几乎看不出任何差异。
2.2 更新方案设计
因为目前 Fair 的热更新平台正在开发中,还没有上线,因此,我们自己设计了一套热更新方案:
方案1:同步更新
方案2:静默更新
方案3:预加载+同步更新
方案4:预加载+静默更新
2.2.1 同步更新方案
采用同步更新方案时,每次进入页面会先请求版本号,如果有更新,则下载 bundle 资源(如已有缓存则不下载),最后展示。
流程图如下:
![]()
2.2.2 静默更新方案
采用静默更新方案,每次进入页面时,如有缓存则直接展示,并异步更新 bundle 资源。如无缓存,则使用同步更新。
所以,静默更新有 3 个特点:
![]()
2.2.3 预加载
对于标记为预加载的资源,我们会在 APP 启动的时候就下载好 bundle 资源。
比如,我们改造 58 拍客的视频列表页,它位于首页的第二个 tab,对于这样的 tab 页,我们当然是希望它在 APP 启动时就进行预加载。
![]()
预加载的方案一般不会单独使用,会选择与同步更新和静默更新一起使用。
总结
![]()
2.3 如何保证 bundle 资源安全性?
如果我们在下载 bundle 资源的过程中,因为一些未知原因导致资源未下载完整,或者说下载了一个被恶意替换的资源,此时,去加载这样一个不合法资源的话,会发生一些未知的错误,增加 APP 的风险。
![]()
那么如何解决呢?
我们的解决方案是采用验证 校验和 的方式。
我们将 bundle 资源上传到服务器时,会通过 MD5 或 SHA-256 计算出 bundle 资源的 校验和(Checksum),并由热更新接口下发。
![]()
客户端完成 bundle 下载后,重新计算一遍得出校验和,如果与服务端下发一致,则为完整且合法的,可以执行加载。
特别注意:上图中展示的校验和是明文的形式,实际开发中,需要大家对其进行加密后再下发。这样做是为了进一步提升数据的安全性。
比如拍客里面,是对其进行了非对称加密后下发,客户端解密后再使用。
2.4 兜底策略(防止未知错误)
在拍客里,我们加入了一个兜底的策略,即:由 Server 端控制加载 bundle 资源还是 Flutter 原生的 Widget。
伪代码如下:
![]()
这样做的目的是,当线上发生一些未知的错误而无法纠正时,能够及时控制。
2.5 Fair 落地过程中遇到的问题
第一个问题:图片模糊
![]()
如上图所示,我们在完成改造后,加载页面发现图片变得非常模糊。
之所以会有这个问题是因为,Fair 将 DSL 转化为 Widget 时,默认取的是 assets 目录下的 1x 图,所以导致了在一些高分辨率手机下出现模糊。
解决方案是,1x 图不要使用低分辨率图片,且 Image 需要设置 width、height 和 fit 属性。
第二个问题是:colors 属性缺失
我们在代码里设置了一个颜色值:Colors.red[50]:
![]()
但是生成 bundle 资源后,发现 DSL 里并没有对于的 color 属性:
![]()
出现这个问题的原因是,Fair 暂时不能支持解析 Colors.xx[xx] 这种设置颜色的深度的语法。
推荐使用十六进制颜色值 Color(0xAARRGGBB)的写法。
首先同步一下开发环境和测试环境:
开发环境:
测试设备:
HUAWEI nova 7,Android 11
iOS iPhoneXSMax,iOS13.3
测试方式:
测试页面:
![]()
3.1 页面流畅度
通过模拟 12 个操作事件,如滑动、点击、跳转来进行测试。
![]()
![]()
3.2 内存表现(增量数据)
接入 Fair 后,Android 端的内存占用 增加了 22MB,iOS 端增加了 17.9MB。
![]()
3.3 启动时间(增量数据)
接入 Fair 后,Android 端的启动时间 增加了 0.03 秒,iOS 端增加了 0.1 秒。
![]()
我们在 58 拍客里接入和使用 Fair 后,对 Fair 有 3 个比较大的感受:
![]()
第一个感受是:保留Flutter原生开发习惯
使用 Fair 不需要学习新的技术栈,也不用适应新的语法习惯,只需要按照 Flutter 原生的写法开发即可。
第二个感受是:改造简单
将 Widget 改造为动态页面,只需要加上几个注解即可,方便、实用。
参考数据:58 拍客接入 Fair + 3 个页面改造排期:2 天
第三个感受是:bundle 资源大小控制合理
58 拍客改造了 3 个页面,这 3 个页面总的 bundle 资源大小才 8KB。
最后,欢迎大家使用 Fair,也欢迎大家为我们点亮 Star
git clone https://github.com/wuba/fair.git
如果想要加入 Fair 的交流群,可以先添加小秘书微信
![]()
作者简介:
陈有余:58 同城,Android 高级开发工程师