1.4 快速完成混合工程搭建
Flutter 的主要开发模式分成两种,一种是独立App 的模式,以Flutter为主,原生工程会被包含在Flutter 工程下;另一种是让Flutter 以模块(Flutter 模块)的形式存在,分别集成在已有的iOS 和Android 原生应用下,原生工程可以在任何的目录结构下,和Flutter 工程地址不产生关联,并需要在原生工程结构中声明Flutter 工程的本地地址。在Flutter 能够以模块形式存在之前,闲鱼进行了很长时间的混合App 架构的探索,对原生工程进行了比较多的改动。在Flutter 官方推出Flutter 模块模式后,我们进行了大量调研,最终推出了一套开箱即用的混合工程脚手架flutter-boot,有助于快速搭建混合工程。
1.4.1 flutter-boot 简介
flutter-boot 主要解决了混合开发模式下的两个问题:Flutter 混合开发的工程化设计和混合栈。那么flutter-boot 是如何解决的呢?
首先在工程化设计的问题上,flutter-boot 建立了一套标准的工程创建流程和友好的交互命令。当流程执行完成后,即拥有了混合开发的标准工程结构。这一套工程结构能够帮助开发者同时拥有Flutter 开发和原生开发两种开发视角,本地Flutter 开发和云端Flutter 构建两种Flutter 集成模式,其效果如图1-19 所示。
另外,在混合栈方面,flutter-boot 能自动注入混合栈依赖,同时将核心的混合栈接入代码封装并注入原生工程内。用户按提示插入简单的几行模板代码后,即可看到混合栈的效果。使用flutter-boot 搭建的混合工程开箱即可使用,接下来介绍flutter-boot 解决这些问题的详细过程。
图1-19
1.4.2 工程化设计
1.了解官方的Add Flutter to existing apps 项目
在了解flutter-boot 的工程化设计细节之前,我们需要对Google 官方提供的Add Flutter to existing apps 方案有一个初步的了解。Add Flutter to existing apps 项目会引导开发者以模块的形式创建Flutter,模块形态的Flutter 的工程结构如下所示。
some/path/
my_flutter/
lib/main.dart
.ios/
.android/
在官方的工程结构下,.ios 和.android 是模板工程,当在Flutter 工程目录下运行时,即通过这两个工程来启动应用。我们如何让原生工程和Flutter 产生关联呢? 这里的关联会分成三个部分, 分别是Flutter 的Framework、业务代码和插件库。其中,Flutter 插件库分成 Flutter Plugin Native(即插件原生代码)和Flutter Plugin Dart(即插件的Dart 代码)两个部分。这四部分的差异如表1-1 所示。
表1-1
Flutter Framework 只需要在依赖管理中声明即可,Flutter Plugin Native可以直接以源码的方式集成,Flutter Plugin Dart 只有在被业务代码引用时才有效。和业务代码一样,需要支持Dart 代码的调试模式和发布模式。Dart 代码的关联会侵入App 的构建环节,根据App 构建的模式来决定Dart代码的构建模式。对于具体的实现,以iOS 系统为例,在podfile 文件中增加一个自定义的Ruby 脚本podfilehelper 的调用,podfilehelper 会声明Flutter Framework 的依赖、Flutter Plugin Native 的源码引用和业务代码的路径。接下来会介入构建流程,在Xcode 的build phase 内加入shell 脚本xcodebackend 的调用,xcodebackend 会根据当前构建模式产出Dart 构建产物。
2.flutter-boot 的补充
对于官方的混合工程项目,在体验过程中发现有如下问题:
- 文件或配置的添加为手动添加,流程较长。
- 不支持在Flutter 仓库下运行原生工程。
- 不支持Flutter 以独立代码仓库部署时的远端机器构建。
因此,在flutter-boot 脚手架中,为了解决这些问题,把混合工程的部署分为create、link、remotelink 和update 四个过程。
(1)create
create 过程在于搭建一个Flutter 模块,包括Flutter 模块的创建和Git仓库的部署。在调用Flutter 模块创建命令前,通过基础的检查,让工程位置和命名的规范满足官方条件。在部署Git 仓库时,会在gitignore 中忽略部分文件,同时对仓库的状态进行检查。当仓库为空时,直接添加文件;当仓库非空时,会优先清理仓库。
(2)link
link 过程在于关联本地的原生工程和Flutter 工程。在关联的过程中,会先请求获取Flutter 工程的地址和原生工程的地址,然后将需要手动集成的部分通过脚本的方式自动集成。为了获得Flutter 开发视角(即Flutter工程下运行原生工程),将原生工程进行了软链接,链接到Flutter 工程的iOS 目录和Android 目录。Flutter 在运行前会找到工程下的iOS 或Android目录然后运行。在Flutter 工程下运行iOS 工程会存在一个限制,即iOS 工程的target 需要指定为runner。为了解决这个问题,将原生工程的主target进行了复制,命名为runner 的target。同时,为了支持远程构建的模式,将Flutter 仓库本地路径的声明根据构建模式进行了区分,封装在自定义的依赖脚本中。例如在iOS 工程内,会添加fbpodhelper.rb 脚本文件,然后将Flutter 仓库本地路径添加到配置文件fbConfig.local.json 中。
(3)remotelink && update
在远端构建模式下,通过remotelink 能够获取Flutter 仓库的代码,并在远端机器上进行构建。在远端构建模式下,我们会侵入依赖管理的过程,在获取依赖时,拉取Flutter 仓库的代码,将代码放置在原生工程的.fbflutter目录下,并将该目录声明为Flutter 仓库本地路径。对于拉取Flutter 代码并进行本地部署的过程,我们称为update 过程。在远端构建时,就能和本地构建如出一辙。那么如何区分远端模式和本地模式呢?我们将远端的Flutter 仓库信息记录在fbConfig.json 中, 同时在gitignore 中忽略fbConfig.local.json 文件,这样只需要负责初始化混合工程的工程师运行一次remotelink,其他的开发协同者将不用关注远端构建的配置流程。
(4)init
为了便于快速搭建,我们提供了一个命令集合,命名为init,将必备的环节以命令行交互的模式集成在了init 命令中。
1.4.3 混合栈
混合栈是闲鱼开源的一套用于Flutter 混合工程下协调原生页面与Flutter 页面交互的框架,目前是混合开发模式下的主流框架。在混合栈开源后,有大量开发者在集成混合栈时会遇到因各种环境配置或代码添加导致的集成问题,为此这里提供一套快速集成的方案。
1.集成问题
要做到快速集成,面临两个问题:Flutter 和混合栈的版本兼容,混合栈Demo 代码封装及插入。
(1)版本兼容问题
目前支持的混合栈版本为0.1.52,支持Flutter 1.5.4。当Flutter 升级时,混合栈势必要进行适配,即集成的混合栈版本也需要变更。因此,将混合栈的版本配置通过文件进行维护,记录当前Flutter 所需要的混合栈版本。在初版的flutter-boot 中,我们限定了混合栈的版本号,在发布新版本混合栈时,将开放版本选择的功能。
(2)代码封装及插入问题
在调研了混合栈的使用过程后,将混合栈需要的Demo 代码分成了四个部分:Flutter 引擎的托管,页面路由的配置,Demo 形式的Dart 页面,原生的测试跳转入口。
2.解决方案
① Flutter 引擎的托管
对于引擎的托管,依赖于应用的初始化。由于初始化过程随着应用的复杂而复杂,因此目前提供了一行代码作为接口,使用者在初始化应用时加入这一行代码即可完成托管。
② 页面路由的配置 &&Demo 形式的Dart 页面
路由配置指路由到某个标识符时,Flutter 页面或原生页面需要识别并跳转到相应页面。路由的配置需要在原生页面和Flutter 页面两侧进行部署。在原生侧,将混合栈的Demo 路由代码进行了精简,然后将其添加到原生工程的固定目录下。由于iOS 仅添加代码文件是不会被纳入构建范围的,因此封装了一套iOS 侧的代码添加工具来实现文件的插入。在Flutter侧,对main.dart 文件进行了覆盖,将带有路由逻辑的main.dart 集成进来,同时提供了Demo 形成的Dart 页面的创建逻辑。
③ 原生的测试跳转入口
为了方便使用者快速看到混合工程的跳转模式,在iOS 和Android 双端封装了一个入口按钮和按钮的添加过程,使用者在测试页面手动加入一行代码,即可看到跳转Flutter 的入口。
3.最终效果
在使用flutter-boot 前,开发者可能要花费数天来进行混合工程搭建。现在,开发者只需要调用一个命令,加入两行代码即可完成混合工程的搭建,大大降低了开发成本。但flutter-boot 的使命还未达成,我们期望开发者能更加流畅地进行Flutter 开发,未来会优化多人协同的开发流程,完善持续集成环境的搭建,让开发者拥有更佳的开发体验。