Flutter混合开发组件化与工程化架构

一、简述

对于构建Flutter类型应用,因其开发语言Dart、虚拟机、构建工具与平时我们开发Native应用不同且平台虚拟机也不支持,所以需要Flutter SDK来支持,如构建Android应用需要Android SDK一样,下载Flutter SDK通常有两种方式:

1.在官网下载构建好的zip包,里面包含完整的Flutter基础Api,Dart VM,Dart SDK等
2.手动构建,Clone Flutter源码后,运行flutter --packages get或其它具有检测类型的命令如build、doctor,这时会自动构建和下载Dart SDK以及Flutter引擎产物

在团队多人协作开发下,Flutter SDK可能遇到的问题

在团队多人协作开发下,这种依赖每个开发本地下载Flutter SDK的方式,不能保证Flutter SDK的版本一致性与自动化管理,在开发时如果Flutter SDK版本不一致,往往会出现Dart层Api兼容性或Flutter虚拟机不一致等问题,因为每个版本的Flutter都有各自对应的Flutter虚拟机,构建产物中会包含对应构建版本的虚拟机。

Flutter工程的构建需要Flutter标准的工程结构目录和依赖于本地的Flutter环境,
每个对应Flutter工程都有对应的Flutter SDK路径,Android在local.properties中,IOS在Generated.xcconfig中。

// This is a generated file; do not edit or check into version control.
FLUTTER_ROOT=/Users/bd/Documents/development/flutter
FLUTTER_APPLICATION_PATH=/Users/bd/flutter/bcc/cc/biz/flutter_biz/example
FLUTTER_TARGET=/Users/bd/flutter/baidu/bcc/cc/flutter_biz/example/lib/main.dart
FLUTTER_BUILD_DIR=build
SYMROOT=${SOURCE_ROOT}/../build/ios
FLUTTER_FRAMEWORK_DIR=/Users/bd/Documents/development/flutter/bin/cache/artifacts/engine/ios
FLUTTER_BUILD_NAME=1.0.0
FLUTTER_BUILD_NUMBER=1
TRACK_WIDGET_CREATION=true

这个路径会在Native工程本地依赖Flutter工程构建时读取,并从中获取引擎、资源和编译构建Flutter工程,而调用flutter命令时构建Flutter工程则会获取当前flutter命令所在的Flutter SDK路径,并从中获取引擎、资源和编译构建Flutter工程,所以flutter命令构建环境与Flutter工程中平台子工程的环境变量一定得保持一致,且这个环境变量是随flutter执行动态改变的,团队多人协作下这个得保证,在打包Flutter工程的正式版每个版本也应该有一个对应的Flutter构建版本,不管是本地打包还是在打包平台打包

我们知道Flutter应用的工程结构都与Native应用工程结构不一样,不一致地方主要是Native工程是作为Flutter工程子工程,外层通过Pub进行依赖管理,

这样通过依赖下来的Flutter Plugin/Package代码即可与多平台共享,(插件共享)

在打包时Native子工程只打包工程代码与Pub所依赖库的平台代码,

Flutter工程则通过flutter_tools打包lib目录下以及Pub所依赖库的Dart代码。
打包产物为Flutter.framework和App.framework

回到正题,因工程结构的差异,如果基于现有的Native工程想使用Flutter来开发其中一个功能模块,一般来说混合开发至少得保证如下特点:

  1. 对Native工程无侵入
  2. 对Native工程零耦合
  3. 不影响Native工程的开发流程与打包流程
  4. 易本地调试

显然改变工程结构的方案可以直接忽略,官方也提供了一种Flutter本地依赖到现有Native的方案,不过这种方案不加改变优化而直接依赖的话,则会直接影响了其它无Flutter环境的开发同学的开发,影响开发流程,且打包平台也不支持这种依赖方式的打包

二、Flutter四种工程类型

Flutter工程中,通常有以下几种工程类型,下面分别简单概述下:

1. Flutter Application

标准的Flutter App工程,包含标准的Dart层与Native平台层

2. Flutter Module

Flutter组件工程,仅包含Dart层实现,Native平台层子工程为通过Flutter自动生成的隐藏工程

3. Flutter Plugin

Flutter平台插件工程,包含Dart层与Native平台层的实现

4. Flutter Package

Flutter纯Dart插件工程,仅包含Dart层的实现,往往定义一些公共Widget

三、Flutter工程Pub依赖管理

Flutter工程之间的依赖管理是通过Pub来管理的,依赖的产物是直接源码依赖,这种依赖方式和IOS中的Pod有点像,都可以进行依赖库版本号的区间限定与Git远程依赖等,其中具体声明依赖是在pubspec.yaml文件中,其中的依赖编写是基于YAML语法,YAML是一个专门用来编写文件配置的语言

声明依赖后,通过运行flutter packages get命名,会从远程或本地拉取对应的依赖,同时会生成pubspec.lock文件,这个文件和IOS中的Podfile.lock极其相似,会在本地锁定当前依赖的库以及对应版本号,只有当执行flutter packages upgrade时,这时才会更新,同样pubspec.lock文件也需要作为版本管理文件提交到Git中,而不应gitignore

四、Flutter链接到Native工程原理

官方提供了一种本地依赖到现有的Native工程方式,具体可看官方wiki:Flutter本地依赖,这种方式太依赖于本地环境和侵入Native工程会影响其它开发同学,且打包平台不支持这种方式的打包,所以肯定得基于这种方式进行优化改造,这个后面再说,先说说Native两端本地依赖的原理

1. iOS

在iOS中本地依赖方式为:

  1. 在Podfile中通过eval binding特性注入podhelper.rb脚本,在pod install/update时会执行它
  2. 在iOS构建阶段Build Phases中注入构建时需要执行的xcode_backend.sh脚本

对于iOS的本地依赖,主要是由podhelper.rb和xcode_backend.sh这两个脚本负责Flutter的Pod本地依赖和产物构建

1. podhelper.rb

因Podfile是通过ruby语言写的,所以该脚本也是ruby脚本,该脚本在pod install/update时主要做了三件事:

  1. Pod本地依赖Flutter引擎(Flutter.framework)与Flutter插件注册表(FlutterPluginRegistrant)
  1. Pod本地源码依赖.flutter-plugins文件中包含的Flutter工程路径下的ios工程
  1. 在pod install执行完后post_install中,获取当前target工程对象,导入Generated.xcconfig配置,这些配置都为环境变量配置,主要为构建阶段xcode_backend.sh脚本执行做准备

上述事情即可保证Flutter工程以及传递依赖的都通过pod本地依赖进Native工程了,接下来就是构建了

2. xcode_backend.sh

该Shell脚本位于Flutter SDK中,该脚本主要就做了两件事:

  1. 调用flutter命令编译构建出产物(App.framework、flutter_assets)
  1. 把产物(*.framework、flutter_assets)拷贝到对应XCode构建产物中

五、Flutter与Native通信

Flutter与Native通信有三种方式,这里只简单介绍下:

  1. MethodChannel:方法调用
  2. EventChannel:事件监听
  3. BasicMessageChannel:消息传递

Flutter与Native通信都是双向通道,可以互相调用和消息传递

六、Flutter版本一致性与自动化管理

七、Flutter混合开发组件化架构

上述说的如果我们要利用Flutter来开发我们现有Native工程中的一个模块或功能,肯定得不能改变Native的工程结构以及不影响现有的开发流程,那么,以何种方式进行混合开发呢?

前面说到Flutter的四种工程模型:

1.Flutter App我们可以直接忽略,因为这是一个开发全新的Flutter App工程。

2.对于Flutter Module,官方提供的本地依赖便是使用Flutter Module依赖到Native App的,而对于Flutter工程来说,构建Flutter工程必须得有个main.dart主入口,恰好Flutter Module中也有主入口。

Flutter Module模式产生的问题:

于是,我们进行组件划分,通过Flutter Module作为所有通过Flutter实现的模块或功能的聚合入口,通过它进行Flutter层到Native层的双向关联。而Flutter开发代码写在哪里呢?当然可以直接写在Flutter Module中,这没问题,而如果后续开发了多个模块、组件,我们的Dart代码总不可能全部写在Flutter Module中lib/吧,如果在lib/目录下再建立子目录进行模块区分,这不失为一种最简单的方式,不过这会带来一些问题,所有模块共用一个远程Git地址,首先在组件开发隔离上完全耦合了,其次各个模块组件没有单独的版本号或Tag,且后续模块组件的增多,带来更多的测试回归成本

正确的组件化方式为一个组件有一个独立的远程Git地址管理,这样各个组件在发正式版时都有一个版本号和Tag,且在各个组件开发上完全隔离,后续组件的增多不影响其它组件,某个组件新增需求而不需回归其它组件,带来更低的测试成本

  1. Flutter Plugin模式

前面提到Flutter Plugin可以有对应Dart层代码与平台层的实现,所以可以这样设计,一个组件对应一个Flutter Plugin,一个Flutter Plugin为一个完整的Flutter工程,有独立的Git地址,而这些组件之间不能互相依赖,保持零耦合,所以这些组件都在业务层,可以叫做业务组件,这些业务组件之间的通信和公共服务可以再划分一层基础层,可以叫做基础组件,所有业务组件依赖基础层,而Flutter Module作为聚合层依赖于所有Flutter组件,这些Flutter工程之间的依赖正是通过Pub依赖进行管理的

所以,综合上述,整体的组件化架构可以设计为:

image.png

尽量让Native平台层成为服务层,让Flutter层成为消费层调用Native层的服务,即Dart调用Native的Api,这样当两端开发人员编写好一致基础的服务接口后,Flutter的开发人员即可平滑使用和开发

而对于基础组件中的公共服务组件Dart Api层的设计,因为公共服务主要调用Native层的服务,在Flutter中提供公共的Dart Api,作为Native到Flutter的一个桥梁,对于Native的服务,会有很有多种,而对应Api的设计为一个dart文件对应一个种类的服务,整个公共服务组件提供一个统一个对外暴露的Dart,内部的细粒度的Dart实现通过export导入,这种设计思想正是Flutter官方Api的设计,即统一对外暴露的Dart为common_service.dart:

library common_service;

export 'network_plugin.dart';
export 'messager_plugin.dart';
...

而上层业务组件调用Api只需要import一个dart即可,这样对上层业务组件开发人员是透明的,上层不需要了解有哪些Api可用:

import 'package:common_service/common_service.dart';

八、Flutter混合开发工程化架构

基本组件化的架构我们搭建好了,接下来是如何让Flutter混合开发进行完整的工程化管理,我们都知道,对于官方的本地依赖这种方式,我们不能直接用,因为这会直接影响Native工程、开发流程与打包流程,所以我们得基于官方这种依赖方式进行优化改造,于是我们衍生出两种Flutter链接到Native工程的方式:

  1. 本地依赖(源码依赖)
  2. 远程依赖(产物依赖)

为什么要有这两种方式,首先本地依赖对于打包平台不支持,现有打包平台的环境,只能支持标准的Gradle工程结构进行打包,且本地依赖对于无需开发Flutter相关业务的同学来说是灾难性的,所以便有了远程依赖,远程依赖直接依赖于打包好的Flutter产物,Android通过Gradle依赖,IOS通过Pod远程依赖,这样对其它业务开发同学来说是透明的,他们无需关心Flutter也不需要知道Flutter是否存在

对于这两种依赖模式的使用环境也各不一样

  1. 本地依赖
    本地依赖主要用于需要进行Flutter开发的同学,通过在对应Native工程中配置文件配置是否打开本地Flutter Module依赖,以及配置链接的本地Flutter Module地址,这样Native工程即可自动依赖到本地的Flutter工程,整个过程是无缝的,同时本地依赖是通过源码进行依赖的,也可以很方便的进行Debug调试
  1. 远程依赖
    远程依赖是把Flutter Module的构成产物发布到远程,然后在Native工程中远程依赖,这种依赖方式是默认的依赖方式,这样对其它开发同学来说是透明的,不影响开发流程和打包平台

上述说到的两种依赖方式,接下来主要说怎么进行这两种依赖方式的工程化管理和定制化

  1. 远程依赖产物打包流程

iOS上的打包相比Android来说更复杂一些,我们借助.ios/Runner来打包出静态库等产物,所以还需要设置签名,通过在Flutter Module中直接执行./flutterw build ios --release,该命令会自动执行pod install,所以我们不必再单独执行它,IOS中构建出的产物获取也相对繁琐些,除了获取Flutter的相关产物,还需要获取所依赖的各组件的静态库以及头文件,需要获取的产物如下:

Flutter.framework
App.framework
FlutterPluginRegistrant
flutter_assets
所有依赖的Plugin的.a静态库以及头文件

其中Flutter.framework为Flutter引擎,类似Android中的flutter.so,而App.framework则是Flutter中Dart编译后的产物(Debug模式下它仅为一个空壳,具体Dart代码在flutter_assets中,Release模式下为编译后的机器指令),FlutterPluginRegistrant是所有插件Channel的注册表,也是自动生成的,flutter_assets含字体等资源,剩下一些.a静态库则是各组件在IOS平台层的实现了

而收集IOS产物除了在.ios/Flutter目录下收集*.framework静态库和flutter_assets外,剩下的就是收集.a静态库以及对应的头文件了,而这些产物则是在构建Runner工程后,在Flutter Module下的

build/ios/$variant-iphoneos

目录下,variant对应所构建变体名,我们还是通过解析.flutter-plugins文件,来获取对应所依赖Flutter插件的名称,进而在上述的输出目录下找到对应的.a静态库,但是对应的头文件而不在对应.a静态库目录下,所以对于头文件单独获取,因为解析了.flutter-plugins获取到了KV键值对,对应的V则是该Flutter插件工程地址,所以头文件我们从里面获取

最后还需要获取FlutterPluginRegistrant注册表的静态库以及头文件

参考:
http://zhengxiaoyong.com/2018/12/16/Flutter%E6%B7%B7%E5%90%88%E5%BC%80%E5%8F%91%E7%BB%84%E4%BB%B6%E5%8C%96%E4%B8%8E%E5%B7%A5%E7%A8%8B%E5%8C%96%E6%9E%B6%E6%9E%84/

你可能感兴趣的:(Flutter混合开发组件化与工程化架构)