今天,我們來聊一聊 Flutter 應用的交付這個話題。
軟件項目的交付是一個復雜的過程,任何原因都有可能導致交付過程失敗。中小型研發團隊經常遇到的一個現象是,App 在開發測試時沒有任何異常,但一到最后的打包構建交付時就問題頻出。所以,每到新版本發布時,大家不僅要等候打包結果,還經常需要加班修復臨時出現的問題。如果沒有很好地線上應急策略,即使打包成功,交付完成后還是非常緊張。
可以看到,產品交付不僅是一個令工程師頭疼的過程,還是一個高風險動作。其實,失敗并不可怕,可怕的是每次失敗的原因都不一樣。所以,為了保障可靠交付,我們需要關注從源代碼到發布的整個流程,提供一種可靠的發布支撐,確保 App 是以一種可重復的、自動化的方式構建出來的。同時,我們還應該將打包過程提前,將構建頻率加快,因為這樣不僅可以盡早發現問題,修復成本也會更低,并且能更好地保證代碼變更能夠順利發布上線。
其實,這正是持續交付的思路。
所謂持續交付,指的是建立一套自動監測源代碼變更,并自動實施構建、測試、打包和相關操作的流程鏈機制,以保證軟件可以持續、穩定地保持在隨時可以發布的狀態。 持續交付可以讓軟件的構建、測試與發布變得更快、更頻繁,更早地暴露問題和風險,降低軟件開發的成本。
你可能會覺得,大型軟件工程里才會用到持續交付。其實不然,通過運用一些免費的工具和平臺,中小型項目也能夠享受到開發任務自動化的便利。而 Travis CI 就是這類工具之中,市場份額最大的一個。所以接下來,我就以 Travis CI 為例,與你分享如何為 Flutter 工程引入持續交付的能力。
## Travis CI
Travis CI 是在線托管的持續交付服務,用 Travis 來進行持續交付,不需要自己搭服務器,在網頁上點幾下就好,非常方便。
Travis 和 GitHub 是一對配合默契的工作伙伴,只要你在 Travis 上綁定了 GitHub 上的項目,后續任何代碼的變更都會被 Travis 自動抓取。然后,Travis 會提供一個運行環境,執行我們預先在配置文件中定義好的測試和構建步驟,并最終把這次變更產生的構建產物歸檔到 GitHub Release 上,如下所示:
:-: 
圖 1 Travis CI 持續交付流程示意圖
可以看到,通過 Travis 提供的持續構建交付能力,我們可以直接看到每次代碼的更新的變更結果,而不需要累積到發布前再做打包構建。這樣不僅可以更早地發現錯誤,定位問題也會更容易。
要想為項目提供持續交付的能力,我們首先需要在 Travis 上綁定 GitHub。我們打開[Travis 官網](https://travis-ci.com/),使用自己的 GitHub 賬號授權登陸就可以了。登錄完成后頁面中會出現一個“Activate”按鈕,點擊按鈕會跳回到 GitHub 中進行項目訪問權限設置。我們保留默認的設置,點擊“Approve&Install”即可。
:-: 
圖 2 激活 Github 集成
:-: 
圖 3 授權 Travis 讀取項目變更記錄
完成授權之后,頁面會跳轉到 Travis。Travis 主頁上會列出 GitHub 上你的所有倉庫,以及你所屬于的組織,如下圖所示:
:-: 
圖 4 完成 Github 項目綁定
完成項目綁定后,接下來就是**為項目增加 Travis 配置文件**了。配置的方法也很簡單,只要在項目的根目錄下放一個名為.travis.yaml 的文件就可以了。
.travis.yaml 是 Travis 的配置文件,指定了 Travis 應該如何應對代碼變更。代碼 commit 上去之后,一旦 Travis 檢測到新的變更,Travis 就會去查找這個文件,根據項目類型(language)確定執行環節,然后按照依賴安裝(install)、構建命令(script)和發布(deploy)這三大步驟,依次執行里面的命令。一個 Travis 構建任務流程如下所示:
:-: 
圖 5 Travis 工作流
可以看到,為了更精細地控制持續構建過程,Travis 還為 install、script 和 deploy 提供了對應的鉤子(before\_install、before\_script、after\_failure、after\_success、before\_deploy、after\_deploy、after\_script),可以前置或后置地執行一些特殊操作。
如果你的項目比較簡單,沒有其他的第三方依賴,也不需要發布到 GitHub Release 上,只是想看看構建會不會失敗,那么你可以省略配置文件中的 install 和 deploy。
## 如何為項目引入 Travis?
可以看到,一個最簡單的配置文件只需要提供兩個字段,即 language 和 script,就可以讓 Travis 幫你自動構建了。下面的例子演示了如何為一個 Dart 命令行項目引入 Travis。在下面的配置文件中,我們將 language 字段設置為 Dart,并在 script 字段中,將 dart\_sample.dart 定義為程序入口啟動運行:
~~~
#.travis.yaml
language: dart
script:
- dart dart_sample.dart
~~~
將這個文件提交至項目中,我們就完成了 Travis 的配置工作。
Travis 會在每次代碼提交時自動運行配置文件中的命令,如果所有命令都返回 0,就表示驗證通過,完全沒有問題,你的提交記錄就會被標記上一個綠色的對勾。反之,如果命令運行過程中出現了異常,則表示驗證失敗,你的提交記錄就會被標記上一個紅色的叉,這時我們就要點擊紅勾進入 Travis 構建詳情,去查看失敗原因并盡快修復問題了。
:-: 
圖 6 代碼變更驗證
可以看到,為一個工程引入自動化任務的能力,只需要提煉出能夠讓工程自動化運行需要的命令就可以了。
在[第 38 篇文章](https://time.geekbang.org/column/article/140079)中,我與你介紹了 Flutter 工程運行自動化測試用例的命令,即 flutter test,所以如果我們要為一個 Flutter 工程配置自動化測試任務,直接把這個命令放置在 script 字段就可以了。
但需要注意的是,Travis 并沒有內置 Flutter 運行環境,所以我們還需要在 install 字段中,為自動化任務安裝 Flutter SDK。下面的例子演示了**如何為一個 Flutter 工程配置自動化測試能力**。在下面的配置文件中,我們將 os 字段設置為 osx,在 install 字段中 clone 了 Flutter SDK,并將 Flutter 命令設置為環境變量。最后,我們在 script 字段中加上 flutter test 命令,就完成了配置工作:
~~~
os:
- osx
install:
- git clone https://github.com/flutter/flutter.git
- export PATH="$PATH:`pwd`/flutter/bin"
script:
- flutter doctor && flutter test
~~~
其實,為 Flutter 工程的代碼變更引入自動化測試能力相對比較容易,但考慮到 Flutter 的跨平臺特性,**要想在不同平臺上驗證工程自動化構建的能力(即 iOS 平臺構建出 ipa 包、Android 平臺構建出 apk 包)又該如何處理呢**?
我們都知道 Flutter 打包構建的命令是 flutter build,所以同樣的,我們只需要把構建 iOS 的命令和構建 Android 的命令放到 script 字段里就可以了。但考慮到這兩條構建命令執行時間相對較長,所以我們可以利用 Travis 提供的并發任務選項 matrix,來把 iOS 和 Android 的構建拆開,分別部署在獨立的機器上執行。
下面的例子演示了如何使用 matrix 分拆構建任務。在下面的代碼中,我們定義了兩個并發任務,即運行在 Linux 上的 Android 構建任務執行 flutter build apk,和運行在 OS X 上的 iOS 構建任務 flutter build ios。
考慮到不同平臺的構建任務需要提前準備運行環境,比如 Android 構建任務需要設置 JDK、安裝 Android SDK 和構建工具、接受相應的開發者協議,而 iOS 構建任務則需要設置 Xcode 版本,因此我們分別在這兩個并發任務中提供對應的配置選項。
最后需要注意的是,由于這兩個任務都需要依賴 Flutter 環境,所以 install 字段并不需要拆到各自任務中進行重復設置:
~~~
matrix:
include:
# 聲明 Android 運行環境
- os: linux
language: android
dist: trusty
licenses:
- 'android-sdk-preview-license-.+'
- 'android-sdk-license-.+'
- 'google-gdk-license-.+'
# 聲明需要安裝的 Android 組件
android:
components:
- tools
- platform-tools
- build-tools-28.0.3
- android-28
- sys-img-armeabi-v7a-google_apis-28
- extra-android-m2repository
- extra-google-m2repository
- extra-google-android-support
jdk: oraclejdk8
sudo: false
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- libstdc++6
- fonts-droid
# 確保 sdkmanager 是最新的
before_script:
- yes | sdkmanager --update
script:
- yes | flutter doctor --android-licenses
- flutter doctor && flutter -v build apk
# 聲明 iOS 的運行環境
- os: osx
language: objective-c
osx_image: xcode10.2
script:
- flutter doctor && flutter -v build ios --no-codesign
install:
- git clone https://github.com/flutter/flutter.git
- export PATH="$PATH:`pwd`/flutter/bin"
~~~
## 如何將打包好的二進制文件自動發布出來?
在這個案例中,我們構建任務的命令是打包,那打包好的二進制文件可以自動發布出來嗎?
答案是肯定的。我們只需要為這兩個構建任務增加 deploy 字段,設置 skip\_cleanup 字段告訴 Travis 在構建完成后不要清除編譯產物,然后通過 file 字段把要發布的文件指定出來,最后就可以通過 GitHub 提供的 API token 上傳到項目主頁了。
下面的示例演示了 deploy 字段的具體用法,在下面的代碼中,我們獲取到了 script 字段構建出的 app-release.apk,并通過 file 字段將其指定為待發布的文件。考慮到并不是每次構建都需要自動發布,所以我們在下面的配置中,增加了 on 選項,告訴 Travis 僅在對應的代碼更新有關聯 tag 時,才自動發布一個 release 版本:
~~~
...
# 聲明構建需要執行的命令
script:
- yes | flutter doctor --android-licenses
- flutter doctor && flutter -v build apk
# 聲明部署的策略,即上傳 apk 至 github release
deploy:
provider: releases
api_key: xxxxx
file:
- build/app/outputs/apk/release/app-release.apk
skip_cleanup: true
on:
tags: true
...
~~~
需要注意的是,由于我們的項目是開源庫,因此 GitHub 的 API token 不能明文放到配置文件中,需要在 Travis 上配置一個 API token 的環境變量,然后把這個環境變量設置到配置文件中。
我們先打開 GitHub,點擊頁面右上角的個人頭像進入 Settings,隨后點擊 Developer Settings 進入開發者設置。
:-: 
圖 7 進入開發者設置
在開發者設置頁面中,我們點擊左下角的 Personal access tokens 選項,生成訪問 token。token 設置頁面提供了比較豐富的訪問權限控制,比如倉庫限制、用戶限制、讀寫限制等,這里我們選擇只訪問公共的倉庫,填好 token 名稱 cd\_demo,點擊確認之后,GitHub 會將 token 的內容展示在頁面上。
:-: 
圖 8 生成訪問 token
需要注意的是,這個 token 你只會在 GitHub 上看到一次,頁面關了就再也找不到了,所以我們先把這個 token 復制下來。
:-: 
圖 9 訪問 token 界面
接下來,我們打開 Travis 主頁,找到我們希望配置自動發布的項目,然后點擊右上角的 More options 選擇 Settings 打開項目配置頁面。
:-: 
圖 10 打開 Travis 項目設置
在 Environment Variable 里,把剛剛復制的 token 改名為 GITHUB\_TOKEN,加到環境變量即可。
:-: 
圖 11 加入 Travis 環境變量
最后,我們只要把配置文件中的 api\_key 替換成 ${GITHUB\_TOKEN}就可以了。
~~~
...
deploy:
api_key: ${GITHUB_TOKEN}
...
~~~
這個案例介紹的是 Android 的構建產物 apk 發布。而對于 iOS 而言,我們還需要對其構建產物 app 稍作加工,讓其變成更通用的 ipa 格式之后才能發布。這里我們就需要用到 deploy 的鉤子 before\_deploy 字段了,這個字段能夠在正式發布前,執行一些特定的產物加工工作。
下面的例子演示了**如何通過 before\_deploy 字段加工構建產物**。由于 ipa 格式是在 app 格式之上做的一層包裝,所以我們把 app 文件拷貝到 Payload 后再做壓縮,就完成了發布前的準備工作,接下來就可以在 deploy 階段指定要發布的文件,正式進入發布環節了:
~~~
...
# 對發布前的構建產物進行預處理,打包成 ipa
before_deploy:
- mkdir app && mkdir app/Payload
- cp -r build/ios/iphoneos/Runner.app app/Payload
- pushd app && zip -r -m app.ipa Payload && popd
# 將 ipa 上傳至 github release
deploy:
provider: releases
api_key: ${GITHUB_TOKEN}
file:
- app/app.ipa
skip_cleanup: true
on:
tags: true
...
~~~
將更新后的配置文件提交至 GitHub,隨后打一個 tag。等待 Travis 構建完畢后可以看到,我們的工程已經具備自動發布構建產物的能力了。
:-: 
圖 12 Flutter App 發布構建產物
## 如何為 Flutter Module 工程引入自動發布能力?
這個例子介紹的是傳統的 Flutter App 工程(即純 Flutter 工程),**如果我們想為 Flutter Module 工程(即混合開發的 Flutter 工程)引入自動發布能力又該如何設置呢?**
其實也并不復雜。Module 工程的 Android 構建產物是 aar,iOS 構建產物是 Framework。Android 產物的自動發布比較簡單,我們直接復用 apk 的發布,把 file 文件指定為 aar 文件即可;iOS 的產物自動發布稍繁瑣一些,需要將 Framework 做一些簡單的加工,將它們轉換成 Pod 格式。
下面的例子演示了 Flutter Module 的 iOS 產物是如何實現自動發布的。由于 Pod 格式本身只是在 App.Framework 和 Flutter.Framework 這兩個文件的基礎上做的封裝,所以我們只需要把它們拷貝到統一的目錄 FlutterEngine 下,并將聲明了組件定義的 FlutterEngine.podspec 文件放置在最外層,最后統一壓縮成 zip 格式即可。
~~~
...
# 對構建產物進行預處理,壓縮成 zip 格式的組件
before_deploy:
- mkdir .ios/Outputs && mkdir .ios/Outputs/FlutterEngine
- cp FlutterEngine.podspec .ios/Outputs/
- cp -r .ios/Flutter/App.framework/ .ios/Outputs/FlutterEngine/App.framework/
- cp -r .ios/Flutter/engine/Flutter.framework/ .ios/Outputs/FlutterEngine/Flutter.framework/
- pushd .ios/Outputs && zip -r FlutterEngine.zip ./ && popd
deploy:
provider: releases
api_key: ${GITHUB_TOKEN}
file:
- .ios/Outputs/FlutterEngine.zip
skip_cleanup: true
on:
tags: true
...
~~~
將這段代碼提交后可以看到,Flutter Module 工程也可以自動的發布原生組件了。
:-: 
圖 13 Flutter Module 工程發布構建產物
通過這些例子我們可以看到,**任務配置的關鍵在于提煉出項目自動化運行需要的命令集合,并確認它們的執行順序。**只要把這些命令集合按照 install、script 和 deploy 三個階段安置好,接下來的事情就交給 Travis 去完成,我們安心享受持續交付帶來的便利就可以了。
## 總結
俗話說,“90% 的故障都是由變更引起的”,這凸顯了持續交付對于發布穩定性保障的價值。通過建立持續交付流程鏈機制,我們可以將代碼變更與自動化手段關聯起來,讓測試和發布變得更快、更頻繁,不僅可以提早暴露風險,還能讓軟件可以持續穩定地保持在隨時可發布的狀態。
在今天的分享中,我與你介紹了如何通過 Travis CI,為我們的項目引入持續交付能力。Travis 的自動化任務的工作流依靠.travis.yaml 配置文件驅動,我們可以在確認好構建任務需要的命令集合后,在這個配置文件中依照 install、script 和 deploy 這 3 個步驟拆解執行過程。完成項目的配置之后,一旦 Travis 檢測到代碼變更,就可以自動執行任務了。
簡單清晰的發布流程是軟件可靠性的前提。如果我們同時發布了 100 個代碼變更,導致 App 性能惡化了,我們可能需要花費大量時間和精力,去定位究竟是哪些變更影響了 App 性能,以及它們是如何影響的。而如果以持續交付的方式發布 App,我們能夠以更小的粒度去測量和理解代碼變更帶來的影響,是改善還是退化,從而可以更早地找到問題,更有信心進行更快的發布。
**需要注意的是,**在今天的示例分析中,我們構建的是一個未簽名的 ipa 文件,這意味著我們需要先完成簽名之后,才能在真實的 iOS 設備上運行,或者發布到 App Store。
iOS 的代碼簽名涉及私鑰和多重證書的校驗,以及對應的加解密步驟,是一個相對繁瑣的過程。如果我們希望在 Travis 上部署自動化簽名操作,需要導出發布證書、私鑰和描述文件,并提前將這些文件打包成一個壓縮包后進行加密,上傳至倉庫。
然后,我們還需要在 before\_install 時,將這個壓縮包進行解密,并把證書導到 Travis 運行環境的鑰匙串中,這樣構建腳本就可以使用臨時鑰匙串對二進制文件進行簽名了。完整的配置,你可以參考手機內側服務廠商蒲公英提供的[集成文檔](https://www.pgyer.com/doc/view/travis_ios)了解進一步的細節。
如果你不希望將發布證書、私鑰暴露給 Travis,也可以把未簽名的 ipa 包下載下來,解壓后通過 codesign 命令,分別對 App.Framework、Flutter.Framework 以及 Runner 進行重簽名操作,然后重新壓縮成 ipa 包即可。[這篇文章](https://www.yangshebing.com/2018/01/06/iOS%E9%80%86%E5%90%91%E5%BF%85%E5%A4%87%E7%BB%9D%E6%8A%80%E4%B9%8Bipa%E9%87%8D%E7%AD%BE%E5%90%8D/)介紹了詳細的操作步驟,這里我們也不再贅述了。
我把今天分享涉及的 Travis 配置上傳到了 GitHub,你可以把這幾個項目[Dart\_Sample](https://github.com/cyndibaby905/08_Dart_Sample)、[Module\_Page](https://github.com/cyndibaby905/28_module_page)、[Crashy\_Demo](https://github.com/cyndibaby905/39_crashy_demo)下載下來,觀察它們的配置文件,并在 Travis 網站上查看對應的構建過程,從而加深理解與記憶。
## 思考題
最后,我給你留一道思考題吧。
在 Travis 配置文件中,如何選用特定的 Flutter SDK 版本(比如 v1.5.4-hotfix.2)呢?
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略