微店的Flutter混合開發組件化與工程化架構

1、簡述

對於構建Flutter類型應用,因其開發語言Dart、虛擬機、構建工具與平時咱們開發Native應用不一樣且平臺虛擬機也不支持,因此須要Flutter SDK來支持,如構建Android應用須要Android SDK同樣,下載Flutter SDK一般有兩種方式:android

  1. 在官網下載構建好的zip包,裏面包含完整的Flutter基礎Api,Dart VM,Dart SDK等
  2. 手動構建,Clone Flutter源碼後,運行flutter --packages get或其它具備檢測類型的命令如builddoctor,這時會自動構建和下載Dart SDK以及Flutter引擎產物

在咱們微店App團隊的多人協做開發下,這種依賴每一個開發本地下載Flutter SDK的方式,不能保證Flutter SDK的版本一致性與自動化管理,在開發時若是Flutter SDK版本不一致,每每會出現Dart層Api兼容性或Flutter虛擬機不一致等問題,由於每一個版本的Flutter都有各自對應的Flutter虛擬機,構建產物中會包含對應構建版本的虛擬機。Flutter工程的構建須要Flutter標準的工程結構目錄和依賴於本地的Flutter環境,每一個對應Flutter工程都有對應的Flutter SDK路徑,Android在local.properties中,IOS在Generated.xcconfig中,這個路徑會在Native工程本地依賴Flutter工程構建時讀取,並從中獲取引擎、資源和編譯構建Flutter工程,而調用flutter命令時構建Flutter工程則會獲取當前flutter命令所在的Flutter SDK路徑,並從中獲取引擎、資源和編譯構建Flutter工程,因此flutter命令構建環境與Flutter工程中平臺子工程的環境變量必定得保持一致,且這個環境變量是隨flutter執行動態改變的,團隊多人協做下這個得保證,在打包Flutter工程的正式版每一個版本也應該有一個對應的Flutter構建版本,無論是本地打包仍是在打包平臺打包。ios

咱們知道Flutter應用的工程結構都與Native應用工程結構不同,不一致地方主要是Native工程是做爲Flutter工程子工程,外層經過Pub進行依賴管理,這樣經過依賴下來的Flutter Plugin/Package代碼便可與多平臺共享,在打包時Native子工程只打包工程代碼與Pub所依賴庫的平臺代碼,Flutter工程則經過flutter_tools打包lib目錄下以及Pub所依賴庫的Dart代碼。回到正題,因工程結構的差別,若是基於現有的Native工程想使用Flutter來開發其中一個功能模塊,通常來講混合開發至少得保證以下特色:git

  1. 對Native工程無侵入
  2. 對Native工程零耦合
  3. 不影響Native工程的開發流程與打包流程
  4. 易本地調試

顯然改變工程結構的方案能夠直接忽略,官方也提供了一種Flutter本地依賴到現有Native的方案,不過這種方案不加改變優化而直接依賴的話,則會直接影響了其它無Flutter環境的開發同窗的開發,影響開發流程,且打包平臺也不支持這種依賴方式的打包。github

再講講Flutter SDK,平時進行Flutter開發過程當中,不免避免不了因Flutter SDK的Bug亦或是須要改Flutter SDK中平臺連接的腳本代碼致使直接改動或者定製Flutter SDK,這種方式雖然能夠解決問題或定製化,不過極其不推薦,這種方式對後續Flutter SDK的平滑升級極不友好,且帶來更多的後期維護成本。api

接下來,本文主要是介紹如何對上述問題解決與實現:xcode

  1. Flutter SDK版本一致性與自動化管理
  2. 無侵入Flutter SDK源碼進行BugFix或定製化
  3. Flutter混合開發組件化架構
  4. Flutter混合開發工程化架構

2、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層的實現,每每定義一些公共Widgetruby

3、Flutter工程Pub依賴管理

Flutter工程之間的依賴管理是經過Pub來管理的,依賴的產物是直接源碼依賴,這種依賴方式和IOS中的Pod有點像,均可以進行依賴庫版本號的區間限定與Git遠程依賴等,其中具體聲明依賴是在pubspec.yaml文件中,其中的依賴編寫是基於YAML語法,YAML是一個專門用來編寫文件配置的語言,下面是一個經過Git地址遠程依賴示例:bash

dependencies:
  uuid:
    git:
      url: git://github.com/Daegalus/dart-uuid.git
      ref: master
複製代碼

聲明依賴後,經過運行flutter packages get命名,會從遠程或本地拉取對應的依賴,同時會生成pubspec.lock文件,這個文件和IOS中的Podfile.lock極其類似,會在本地鎖定當前依賴的庫以及對應版本號,只有當執行flutter packages upgrade時,這時纔會更新,一樣pubspec.lock文件也須要做爲版本管理文件提交到Git中,而不該gitignore。架構

1. Pub依賴衝突處理

對於PubPod這種依賴管理工具對於發生衝突時處理衝突的能力與Android的Gradle依賴管理相比差了一大截,因此當同一個庫發生版本衝突時,只能咱們本身手動進行處理,並且隨着開發規模的擴大,確定會出現傳遞依賴的庫之間的衝突。app

Pub依賴衝突主要有兩種:

  1. 當前依賴庫的版本與當前的Dart SDK環境版本衝突
  2. 傳遞依賴時出現一個庫版本不一致衝突

第一種會在flutter packages get時報錯並提示爲什麼出現衝突且最低須要的版本是多少,以下:

The current Dart SDK version is 2.1.0-dev.5.0.flutter-a2eb050044.

Because flutter_app depends on xml >=0.1.0 <3.0.1 which requires SDK version <2.0.0, version solving failed.                        
pub get failed (1)
複製代碼

這個能夠直接根據提示進行依賴庫的版本升級解決

而第二種則比較複雜點,假若有A、B、C三個庫,A和B都依賴C庫,若是A的某個版本依賴的C和B版本依賴的C版本不一致,則會發生衝突,而如何解決這種衝突呢?有兩種方式

一、首先把A和B庫的版本都設爲any任意版本,以下:

dependencies:
	A: any
	B: any
複製代碼

此時再經過flutter packages get時,則不會提示有版本衝突報錯,由於Pub已經自動選取了讓C庫版本一致的A、B庫的版本號,此時打開同級目錄下的pubspec.lock文件,搜索A、B兩個庫,則會有對應無衝突的版本號,最後再把這兩個版本號分別替換掉any版本,這個版本衝突就解決了

二、經過版本覆蓋進行解決

2. Pub依賴版本覆蓋

Pub依賴管理中,既然支持傳遞依賴,一樣也提供了一種版本覆蓋的方式,意爲強制指定一個版本,這和Android中Gradleforce有點類似,一樣版本覆蓋方式也能夠用於解決衝突,若是知道某一個版本確定不會衝突,則可直接經過版本覆蓋方式解決:

dependency_overrides:
  A: 2.0.0
複製代碼

4、Flutter連接到Native工程原理

官方提供了一種本地依賴到現有的Native工程方式,具體可看官方wiki:Flutter本地依賴,這種方式太依賴於本地環境和侵入Native工程會影響其它開發同窗,且打包平臺不支持這種方式的打包,因此確定得基於這種方式進行優化改造,這個後面再說,先說說Native兩端本地依賴的原理

1. Android

在Android中本地依賴方式爲:

  1. settings.gradle中注入include_flutter.groovy腳本
  2. 在須要依賴的module中build.gradle添加project(':flutter')依賴

對於Android的本地依賴,主要是由include_flutter.groovyflutter.gradle這兩個腳本負責Flutter的本地依賴和產物構建

1. include_flutter.groovy

settings.gradle中注入時,分別綁定了當前執行Gradle的上下文環境與執行include_flutter.groovy腳本,該腳本只作了下面三件事:

  1. include FlutterModule中的.android/Flutter工程
  2. include FlutterModule中的.flutter-plugins文件中包含的Flutter工程路徑下的android module
  3. 配置全部工程的build.gradle配置執行階段都依賴於:flutter工程,也即它最早執行配置階段

其中.flutter-plugins文件,是根據當前依賴自動生成的,裏面包含了當前Flutter工程所依賴(直接依賴和傳遞依賴)的Flutter子工程與絕對路徑的K-V關係,子工程多是一個Flutter Plugin或者是一個Flutter Package,下面是.flutter-plugins中的一段內容示例: .flutter-plugins:

url_launcher=/Users/Sunzxyong/.pub-cache/hosted/pub.flutter-io.cn/url_launcher-4.0.2/
複製代碼

2. flutter.gradle

該腳本位於Flutter SDK中,內容看起來很長,其實主要作了下面三件事:

  1. 選擇符合對應架構的Flutter引擎(flutter.so
  2. 解析上述.flutter-plugins文件,把對應的android module添加到Native工程的依賴中(上述的include其實爲這步作準備)
  3. Hook mergeAssets/processResources Task,預先執行FlutterTask,調用flutter命令編譯Dart層代碼構建出flutter_assets產物,並拷貝到assets目錄下

有了上述三步,則直接在Native工程中運行構建便可自動構建Flutter工程中的代碼並自動拷貝產物到Native中

2. IOS

在IOS中本地依賴方式爲:

  1. 在Podfile中經過eval binding特性注入podhelper.rb腳本,在pod install/update時會執行它
  2. 在IOS構建階段Build Phases中注入構建時須要執行的xcode_backend.sh腳本

對於IOS的本地依賴,主要是由podhelper.rbxcode_backend.sh這兩個腳本負責Flutter的Pod本地依賴和產物構建

1. podhelper.rb

因Podfile是經過ruby語言寫的,因此該腳本也是ruby腳本,該腳本在pod install/update時主要作了三件事:

  1. Pod本地依賴Flutter引擎(Flutter.framework)與Flutter插件註冊表(FlutterPluginRegistrant)
  2. Pod本地源碼依賴.flutter-plugins文件中包含的Flutter工程路徑下的ios工程
  3. 在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)
  2. 把產物(*.framework、flutter_assets)拷貝到對應XCode構建產物中,對應產物目錄爲:$HOME/Library/Developer/Xcode/DerivedData/${AppName}

上述兩個靜態庫*.framework是拷貝到${BUILT_PRODUCTS_DIR}"/"${PRODUCT_NAME}".app/Frameworks"目錄下

flutter_assets拷貝到${BUILT_PRODUCTS_DIR}"/"${PRODUCT_NAME}".app"目錄下

在XCode工程中,對應的是在${AppName}/Products/${AppName}.app

5、Flutter與Native通訊

Flutter與Native通訊有三種方式,這裏只簡單介紹下:

  1. MethodChannel:方法調用
  2. EventChannel:事件監聽
  3. BasicMessageChannel:消息傳遞

Flutter與Native通訊都是雙向通道,能夠互相調用和消息傳遞

接下來是本文的重點內容,上述主要是普及下Flutter工程上比較重要的內容以及爲下面要講作準備,固然還有打包模式、構建流程等就不放這裏了,後面能夠單獨開一篇講

6、Flutter版本一致性與自動化管理

在團隊多人協做開發模式下,Flutter SDK的版本一致性與自動化管理,這是個必須解決的問題,經過這個問題,咱們回看Android中Gradle的版本管理模式:

Gradle的版本管理是經過包裝器模式,每一個Gradle項目都會對應一個Gradle構建版本,對應的Gradle版本在gradle-wrapper.properties配置文件中進行配置,若是執行構建時本地沒有當前工程中對應的Gradle版本,則會自動下載所需的Gradle版本,而執行構建則是經過./gradlew包裝器模式進行執行,這樣本地配置的全局Gradle環境與工程環境便可隔離開,對應的項目始終保持同一個Gradle版本的構建

這種包裝器模式的版本管理方式,可與每臺機器中全局配置的環境保持隔離,在團隊多人協做下,也可保持同一個項目工程保持同一個構建版本

因此,咱們沿用Gradle版本管理思想,在每一個Flutter工程(包含上述說的四種工程)的根目錄加入三個文件:

wrapper/flutter-wrapper.properties
flutterw
flutterw.bat
複製代碼

加入後的項目結構則多了三個文件,以下:

上述flutter-wrapper.properties爲當前工程Flutter SDK版本配置文件,內容爲:

distributionUrl=https://github.com/flutter/flutter
flutterVersion=1.0.0
複製代碼

固然有須要能夠再增長一些配置,目前這兩個配置已經足夠了,指定了Flutter的遠程地址以及版本號,若是Clone Github上項目比較慢,也能夠改成私有維護的鏡像地址

flutterw爲一個Shell腳本,內部對版本管理主要作的事情爲:

  1. 讀取配置的版本號,校驗Flutter SDK版本,不存在則觸發下載
  2. 更新Android中local.properties和IOS中Generated.xcconfig文件中Flutter SDK地址
  3. 最後把命令行傳來的參數連接到Flutter SDK中的flutter進行執行

以後構建Flutter工程則用flutterw命令:

./flutterw build bundle
複製代碼

而不用本地全局配置的flutter命令,避免每一個開發同窗版本不一致問題,且這種方式對於新加入Flutter開發的同窗來講,徹底不須要本身手動下載Flutter SDK,只需執行一下flutterw任何命令,如./flutterw --version,便可自動觸發對應Flutter SDK的下載與安裝,實現優雅的自動化管理,這種方式對打包平臺來講也爲支持Flutter工程的打包提供基礎

7、Flutter混合開發組件化架構

上述說的若是咱們要利用Flutter來開發咱們現有Native工程中的一個模塊或功能,確定得不能改變Native的工程結構以及不影響現有的開發流程,那麼,以何種方式進行混合開發呢? 前面說到Flutter的四種工程模型,Flutter App咱們能夠直接忽略,由於這是一個開發全新的Flutter App工程,對於Flutter Module,官方提供的本地依賴即是使用Flutter Module依賴到Native App的,而對於Flutter工程來講,構建Flutter工程必須得有個main.dart主入口,剛好Flutter Module中也有主入口

因而,咱們進行組件劃分,經過Flutter Module做爲全部經過Flutter實現的模塊或功能的聚合入口,經過它進行Flutter層到Native層的雙向關聯。而Flutter開發代碼寫在哪裏呢?固然能夠直接寫在Flutter Module中,這沒問題,而若是後續開發了多個模塊、組件,咱們的Dart代碼總不可能所有寫在Flutter Module中lib/吧,若是在lib/目錄下再創建子目錄進行模塊區分,這不失爲一種最簡單的方式,不過這會帶來一些問題,全部模塊共用一個遠程Git地址,首先在組件開發隔離上徹底耦合了,其次各個模塊組件沒有單獨的版本號或Tag,且後續模塊組件的增多,帶來更多的測試迴歸成本

正確的組件化方式爲一個組件有一個獨立的遠程Git地址管理,這樣各個組件在發正式版時都有一個版本號和Tag,且在各個組件開發上徹底隔離,後續組件的增多不影響其它組件,某個組件新增需求而不需迴歸其它組件,帶來更低的測試成本

前面提到Flutter Plugin能夠有對應Dart層代碼與平臺層的實現,因此能夠這樣設計,一個組件對應一個Flutter Plugin,一個Flutter Plugin爲一個完整的Flutter工程,有獨立的Git地址,而這些組件之間不能互相依賴,保持零耦合,因此這些組件都在業務層,能夠叫作業務組件,這些業務組件之間的通訊和公共服務能夠再劃分一層基礎層,能夠叫作基礎組件,全部業務組件依賴基礎層,而Flutter Module做爲聚合層依賴於全部Flutter組件,這些Flutter工程之間的依賴正是經過Pub依賴進行管理的

因此,綜合上述,總體的組件化架構能夠設計爲:

業務組件與基礎組件的定位

對於上面的基礎組件好比還能夠進行更細粒度的劃分,不過不建議劃分太多,對於與Native平臺層的通訊,每一個業務組件對應一個Channel,固然內部還能夠進行更細粒度的Channel進行劃分,這個Channel主要是負責Native層服務的提供,讓Flutter層消費。而對於Native層調用Flutter層的Api,應該儘量少,須要調也只有出現一些值回調時

由於Flutter的出現最本質的就是一次開發兩端運行,而若是有太多這種依賴於平臺層的實現,反而出現違背了,最後只是UI寫了一份而已。對於平臺層的實現也要儘可能保持一個原則,即:

儘可能讓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';
複製代碼

8、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調試 對於Android中配置文件爲本地的local.properties,IOS中爲本地新建的local.xcconfig,兩個平臺的配置屬性保持一致:

FLUTTER_MODULE_LINK_ENABLE=true
FLUTTER_MODULE_LOCAL_LINK=/Users/Sunzxyong/FlutterProject/flutter_module
複製代碼

2. 遠程依賴 遠程依賴是把Flutter Module的構成產物發佈到遠程,而後在Native工程中遠程依賴,這種依賴方式是默認的依賴方式,這樣對其它開發同窗來講是透明的,不影響開發流程和打包平臺

上述說到的兩種依賴方式,接下來主要說怎麼進行這兩種依賴方式的工程化管理和定製化

1. 無侵入Flutter SDK源碼進行BugFix和定製化

Flutter SDK在使用時,難免會遇到一些Flutter SDK的問題或Bug,但這些問題一般是在各平臺層的連接腳本中出現坑,而若是咱們要兼容現有工程和擴展定製化功能,每每會直接修改Flutter SDK源碼,這種侵入性的方式極不推薦,這對後續SDK的平滑升級會帶來更多的成本

一般出現Bug或須要定製化的腳本每每是和平臺連接時相關的,固然排除須要修改dart層Api代碼的狀況下,這種只能更改源碼了,不過這種出bug的概率仍是比較小的,比較涉及到SDK的Api層面了。而大機率出現問題須要兼容或進行定製化的幾個地方一般爲下面幾處:

  1. $FLUTTER_SDK/packages/flutter_tools/gradle/flutter.gradle
  2. $FLUTTER_SDK/bin/cache/artifacts/engine/android-arch/flutter.jar
  3. $FLUTTER_MODULE/.android/build.gradle、.android/settings.gradle
  4. $FLUTTER_MODULE/.android/Flutter/build.gradle
  5. $FLUTTER_MODULE/.ios/Flutter/Generated.xcconfig
  6. $FLUTTER_MODULE/.ios/Flutter/podhelper.rb
  7. $FLUTTER_MODULE/.ios/Podfile
  8. $FLUTTER_SDK/packages/flutter_tools/bin/xcode_backend.sh

而咱們須要兼容的Flutter SDK的問題和定製化的點有下面幾項:

  1. Android:Flutter SDK中的Flutter引擎不支持armeabi架構
  2. Android:Flutter SDK中的flutter.gradle連接腳本不支持非app名稱的Application工程
  3. Android:Flutter SDK中的flutter.gradle連接腳本本地依賴存在flutter_shared資源文件不拷貝Bug
  4. Android:解決上述幾項須要代理build.gradle構建腳本,以及在build.gradle構建腳本中定製化咱們的構建產物收集Task
  5. IOS:Flutter Module中自動生成的.ios中的podhelper.rbruby腳本使用了Pod中的post_install方法,致使Native工程不能使用或使用了的發生衝突,間接侵入了Native工程與耦合,限制性太強
  6. IOS:Flutter Module中自動生成的Podfile文件,須要添加咱們本身私有的Specs倉庫進行定製化
  7. IOS:解決post_install問題後,Flutter SDK中的xcode_backend.sh連接腳本環境變量的讀取問題

爲了實現無侵入Flutter SDK,對於上述的這些問題的解決,咱們使用代理方式進行Bug的修改和定製化,下面是針對兩個平臺分別的實現策略

1. Android

在Android平臺上述問題和定製化的解決策略,對於armeabi架構的支持,咱們能夠經過腳本進行自動化,上面講到flutterw的版本自動化管理,一樣,咱們在裏面加段armeabi架構的支持腳本,這樣作得好處是後續不須要支持了能夠直接移除,經過調用./flutterw armeabi便可自動添加armeabi架構的引擎

對於Flutter SDK中的flutter.gradle連接腳本的問題兼容,不會直接在源碼中進行更改,而是把它拷貝出來,命名爲flutter_proxy.gradle,而後在代理腳本中進行問題的修復,主要修復點爲flutter_shared的支持與app硬編碼名稱的兼容,以下:

Task copySharedFlutterAssetsTask = project.tasks.create(name: "copySharedFlutterAssets${variant.name.capitalize()}", type: Copy) {
			from(project.zipTree(chosenFlutterJar))
			include 'assets/flutter_shared/*'
			into "src/${variant.name}"
		}
複製代碼

再讓copyFlutterAssetsTask任務依賴於它,而app硬編碼名稱的兼容,則更簡單了,經過在Native工程中local.properties配置Module名,再在flutter_proxy.gradle腳本中加入讀取該屬性代碼:

String appName = loadRootProjectProperty(project, "FLUTTER_APP_NAME", "app")
		Task mergeAssets = project.tasks.findByPath(":${appName}:merge${variant.name.capitalize()}Assets")
複製代碼

而對於build.gradle構建腳本的代理,咱們能夠經過在執行Gradle構建時,經過-c命令進行settings.gradle的代理,進而代理掉build.gradle和指定Module中的build.gradle腳本,以下:

cd .android
./gradlew assembleDebug -c ../script/proxy/settings.gradle
複製代碼

而經過代理的settings.gradle文件再進行build.gradle的代理:

getRootProject().buildFileName = 'build_proxy.gradle'
project(":flutter").buildFileName = "build_proxy.gradle"
複製代碼

其中代理的Flutter/build.gradle中的腳本apply會改成修復的Flutter SDK中的腳本代理:

apply from: "${project.projectDir.parentFile.parentFile.absolutePath}/script/proxy/flutter_proxy.gradle"
複製代碼

這樣.android工程在構建時期能夠徹底由咱們自主控制,包括加入一些產物收集插件、產物發佈到遠程插件等定製功能

不過這種方式須要執行構建命令時手動指定代理腳本,對於本地依賴時Native自動構建來講,是不會指定的,全部基於這種方式,咱們再優化一下,由於Flutter Module.android.ios工程是經過Flutter SDK內部模版自動生成的,只要執行build|packages get等命令都會自動生成,首先想到是更改Flutter SDK內部工程模版,在Flutter SDK的packages/flutter_tools/templates目錄下,不過這與咱們無侵入Flutter SDK違背了,因此不能選取這種方式

回想咱們的Flutter SDK版本一致性管理是經過flutterw腳本進行自動化的,而最終會連接調用到原生Flutter SDK中的命令,因此,咱們能夠在flutterw中加入腳本,用於在.android.ios工程生成後,進行內部腳本文件的替換,把build.gradlesettings.gradle腳本內容直接替換爲咱們的代理腳本的內容,這樣既不侵入Flutter SDK,在後續維護起來也方便,後續不須要這個功能了,只須要把這段腳本代碼註釋就行了,隨即又恢復原生的構建腳本了,flutterw腳本執行過程以下:

function main() {
		# ...
		link_flutter "$@"
    	inject_proxy_build_script
    	# ...
}
複製代碼

inject_proxy_build_script這個Shell函數會把對應腳本進行咱們的腳本替換掉,當前函數內部也有對應判斷,由於flutterw主要用於Flutter SDK版本一致性管理,這裏僅對Flutter Module工程生效。因此這種方式無論是在本地依賴構建下仍是經過命令行構建均可以完美支持

2. IOS

在IOS平臺上述問題和定製化的解決策略,對於IOS主要是對Podfilepodhelper.rb腳本進行支持,而對Podfile的支持,這個比較簡單,在Podfile頭部經過腳本注入咱們本身私有的Specs倉庫便可:

source 'https://***/XXSpecs.git'
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
...
複製代碼

這個工做一樣在flutterw執行後進行兼容,後續不須要了能夠直接註釋,這個自動注入腳本也僅對Flutter Module工程生效

podhelper.rb腳本的兼容,主要是在進行本地依賴時,內部已經用了post_install函數,該函是在pod install後執行,這會與Native已經使用了該函數的發生衝突並報錯,因此咱們經過flutterw腳本的執行後默認註釋掉該腳本中的post_install使用處,可是確定不能無緣無故註釋掉,咱們要了解這段的做用,其實就是設置環境變量,爲後續xcode_backend.sh腳本的構建執行作準備,而註釋掉怎麼用另一種方式恢復環境變量的設置這個後面再講,註釋後podhelper.rb腳本代碼片斷爲:

# post_install do |installer|
# installer.pods_project.targets.each do |target|
# target.build_configurations.each do |config|
# config.build_settings['ENABLE_BITCODE'] = 'NO'
# xcconfig_path = config.base_configuration_reference.real_path
# File.open(xcconfig_path, 'a+') do |file|
# file.puts "#include \"#{File.realpath(File.join(framework_dir, 'Generated.xcconfig'))}\""
# end
# end
# end
# end
複製代碼

最終在flutterw自動支持上述處理腳本執行流程爲:

function main() {
		# ...
		link_flutter "$@"
    	# ...
    	podfile_support
    	podhelper_support
    	collect_ios_product "$@"
}
複製代碼

函數內部判斷僅針對Flutter Module工程生效,畢竟其它Flutter Plugin工程不須要這種處理

2. 本地依賴無侵入流程

咱們要作到只經過一個屬性配置文件,在配置文件中經過配置開發來打開或關閉本地的Flutter Module連接依賴,只按官方的依賴方式確定是不行的,無論是Android仍是IOS,都會直接侵入Native工程,影響其它無Flutter環境同窗的開發且影響打包平臺上的打包。因此,確定得作優化,咱們在官方這種依賴方式中加一層,做爲代理層,而代理層主要作的工做是判斷本地是否有對應的屬性配置文件且屬性值是否符合本地依賴Flutter Module的條件,若是是則進行本地Flutter Module的依賴,若是不是則Return掉,默認不作任何處理

因此經過這種代理方式即不影響Native工程原先的開發流程,對其它業務開發同窗和打包平臺也是透明的

對於代理層的實現,Android與IOS平臺各不同

1. Android

Android是經過一個Gradle腳本進行自動管理的,這個Gradle腳本主要在settings.gradlebuild.gradle中作local.properties配置文件的屬性值校驗,決定是否開啓本地Flutter Module連接的

2. IOS

IOS則較爲複雜一些,由於涉及到Podfile中的ruby執行腳本代理與Build Phases時期的Shell腳本代理,因此得寫兩種類型的代理腳本:Ruby和Shell,代理腳本的最終執行仍是會調用被代理的腳本,只是在調用前作一層包裝邏輯判斷。而IOS中自己沒有本地配置文件,因此咱們新建一個IOS的本地配置文件爲local.xcconfig,這個配置文件不隨版本進行管理,會gitignore掉,因而,在IOS中Podfile最終調用的腳本是:

eval(File.read(File.join('./', 'FlutterSupport', 'podhelper_proxy.rb')), binding)
複製代碼

而在Build Phases調用的是:

chmod +x "${SRCROOT}/FlutterSupport/xcode_backend_proxy.sh"
"${SRCROOT}/FlutterSupport/xcode_backend_proxy.sh" flutterBuild
複製代碼

而剛剛上面說到的podhelper.rb腳本中post_install函數被註釋掉後怎麼用另外一種方式進行替換,咱們知道這段函數主要就是提供在IOS構建階段時執行xcode_backend.sh的環境變量的,好比會獲取FLUTTER_ROOT等屬性值,這些環境變量由Flutter Module中Generated.xcconfig來提供,而若是咱們把這個文件的內容經過腳本拷貝到IOS工程下對應構建配置的xcconfig中,如debug.xcconfigrelease.xcconfig,這種方式可行,不過會侵入Native工程,致使Native工程中多了這些變量,並且不優雅,咱們要作到的是保證無侵入性

既然咱們已經經過代理腳本進行代理,那麼這些環境變量咱們徹底能夠獲取出來,經過Shell腳本的特性,子Shell會繼承於父Shell中export的環境變量值,因此,在代理Shell腳本中再加段下面代碼:

function export_xcconfig() {
	export ENABLE_BITCODE=NO
	if [[ $# != 0 ]]; then
		local g_xcconfig=$1/.ios/Flutter/Generated.xcconfig
		if [[ -f "$g_xcconfig" ]]; then
			# no piping.
			while read -r line
			do
  				if [[ ! "$line" =~ ^// ]]; then
					export "$line"
				fi
			done < $g_xcconfig
		fi
	fi
}
複製代碼

其中注意不能使用管道,管道會在另一個Shell進程

3. 遠程依賴產物打包流程

Flutter的遠程產物依賴,Android是經過Aar依賴,IOS是經過.a.framework靜態庫進行依賴,要進行這些遠程依賴很簡單,關鍵是如何打包獲取這些依賴的產物以及上傳到遠程,由於按照現有組件化的打包,除了聚合層Flutter Module中有對應的flutter-debug.aarApp.frameworkflutter_assets等產物的生成,其中業務組件和基礎組件中,也有對應的打包產物,這些打包產物會對應各自平臺打包不一樣類型產物,Android仍是aar,而IOS則是.a靜態庫了,下面就分別講下Android與IOS的打包流程

1. Android

Android的打包比較簡單,經過在Flutter Module中的.android子工程下執行./gradlew assembleRelease,則會在對應Flutter中Android子工程的build目錄下輸出對應aar產物,而重點是怎麼獲取依賴的各組件(Flutter Plugin)中的產物,則是經過.flutter-plugins文件,該文件是在packages get時自動生成的,裏面包含了該Flutter工程經過Pub所依賴的庫,咱們能夠解析這個文件,來獲取對應依賴庫的產物

2. IOS

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註冊表的靜態庫以及頭文件

3. 產物收集與傳遞依賴

對於經過Flutter Module聚合層構建出來的產物,咱們進行收集後再聚合到單獨的產物輸出目錄下,固然這一切都是經過腳本自動作掉的

在Android上,經過Gradle插件Hook assembleTask

collectAarTask.dependsOn assembleTask
		assembleTask.finalizedBy collectAarTask
複製代碼

這樣當執行完./gradlew assemble${variant}命令後則會自動進行產物收集

在IOS上,經過flutterw腳本,在構建完後判斷構建命令是不是IOS構建命令,進而自動收集構建後的產物:

function collect_ios_product() {
	if [[ $# != 0 && $# > 2 ]]; then
		if [[ "$1" = "build" && "$2" = "ios" ]]; then
			# do collect...
		fi
	fi	
}		
複製代碼

對應.a靜態庫和頭文件的收集關鍵腳本代碼以下:

while read -r line
		do
			if [[ ! "$line" =~ ^// && ! "$line" =~ ^# ]]; then
				array=(${line//=/ })
				local library=$product_dir/${array[0]}/lib${array[0]}.a
				if [[ -f "$library" ]]; then
					local plugin=$dest_dir/plugins/${array[0]}
					rm -rf $plugin
					mkdir -p $plugin
					cp -f $library $plugin
					local classes=${array[1]}ios/Classes
					for header in `find "$classes" -name *.h`; do
						cp -f $header $plugin
					done
			else
				echo "The static library $library do not exist!"
				fi
			fi
		done < $flutter_plugins
複製代碼

以下是Android與IOS的打包後產物收集後的目錄結構以下:

對於傳遞依賴的支持,咱們知道單獨的aar文件以及經過podspec聲明這些靜態庫產物,是會丟失傳遞依賴的,丟失傳遞依賴可能致使咱們Native工程中沒有使用到的一些三方庫,而Flutter工程中引用了,而後App運行Crash,而保證傳遞依賴的方式,則是Android發佈到遠程Maven,最後經過遠程依賴,上述產物只是本地依賴,IOS則是解析全部Flutter插件中的podspec文件,把它還原爲JSON格式,經過解析dependencies對象,獲取對應的依賴庫命名以及版本號,最後在IOS遠程產物的podspec配置文件中添加這些依賴

對於IOS的遠程依賴,咱們知道單獨建一個獨立的Git倉庫就能夠解決,經過配置好podspec,便可在IOS Native端進行遠程依賴,可是像Flutter.frameworkApp.framework這種大文件,若是直接上傳到Git倉庫中有些不太友好,好比能夠上傳到CDN中,而後經過podspecspec.prepare_command特性,在pod庫安裝時候預先執行一段腳本把這兩個產物拉下來,對於目前來講,能夠先傳到Git中,這樣比較直觀與可控,便於版本的管理

4. Flutter混合開發工程化總體流程

9、後序

對於現有工程使用Flutter進行混合開發,坑點仍是有的,好比性能、頁面棧管理等方面,只是目前還未踩到,加上目前Flutter上一些基礎庫不成熟,對於項目內的重要頁面以及動態化強度比較高的頁面,目前仍是不建議使用Flutter進行開發,若是要使用也須作好降級方案,相反可使用稍微輕量級點的頁面,且在設計時對於Flutter與Native層的通訊,應該讓Flutter做爲消費層消費Native層提供的服務,Native端應作儘可能少的改動,最好僅增長一處頁面路由的攔截器代碼,在攔截器中經過Native與Flutter頁面的映射關係,把Native的頁面路由跳轉替換爲Flutter頁面路由,這樣能夠保證Native與Flutter的零耦合

做者簡介

zhengxiaoyong,@WeiDian,2016年加入微店,目前主要負責微店App的基礎支撐開發工做。

歡迎關注微店App技術團隊官方公衆號

微店App技術團隊
相關文章
相關標籤/搜索