今天,我们来聊一聊 Flutter 应用的交付这个话题。
软件项目的交付是一个复杂的过程,任何原因都有可能导致交付过程失败。中小型研发团队经常遇到的一个现象是,App 在开发测试时没有任何异常,但一到最后的打包构建交付时就问题频出。所以,每到新版本发布时,大家不仅要等候打包结果,还经常需要加班修复临时出现的问题。如果没有很好地线上应急策略,即使打包成功,交付完成后还是非常紧张。
可以看到,产品交付不仅是一个令工程师头疼的过程,还是一个高风险动作。其实,失败并不可怕,可怕的是每次失败的原因都不一样。所以,为了保障可靠交付,我们需要关注从源代码到发布的整个流程,提供一种可靠的发布支撑,确保 App 是以一种可重复的、自动化的方式构建出来的。同时,我们还应该将打包过程提前,将构建频率加快,因为这样不仅可以尽早发现问题,修复成本也会更低,并且能更好地保证代码变更能够顺利发布上线。
其实,这正是持续交付的思路。
所谓持续交付,指的是建立一套自动监测源代码变更,并自动实施构建、测试、打包和相关操作的流程链机制,以保证软件可以持续、稳定地保持在随时可以发布的状态。 持续交付可以让软件的构建、测试与发布变得更快、更频繁,更早地暴露问题和风险,降低软件开发的成本。
你可能会觉得,大型软件工程里才会用到持续交付。其实不然,通过运用一些免费的工具和平台,中小型项目也能够享受到开发任务自动化的便利。而 Travis CI 就是这类工具之中,市场份额最大的一个。所以接下来,我就以 Travis CI 为例,与你分享如何为 Flutter 工程引入持续交付的能力。
## Travis CI
Travis CI 是在线托管的持续交付服务,用 Travis 来进行持续交付,不需要自己搭服务器,在网页上点几下就好,非常方便。
Travis 和 GitHub 是一对配合默契的工作伙伴,只要你在 Travis 上绑定了 GitHub 上的项目,后续任何代码的变更都会被 Travis 自动抓取。然后,Travis 会提供一个运行环境,执行我们预先在配置文件中定义好的测试和构建步骤,并最终把这次变更产生的构建产物归档到 GitHub Release 上,如下所示:
:-: ![](https://img.kancloud.cn/1e/41/1e416da5f8bd0295b75328c728b75e85_1190x502.png)
图 1 Travis CI 持续交付流程示意图
可以看到,通过 Travis 提供的持续构建交付能力,我们可以直接看到每次代码的更新的变更结果,而不需要累积到发布前再做打包构建。这样不仅可以更早地发现错误,定位问题也会更容易。
要想为项目提供持续交付的能力,我们首先需要在 Travis 上绑定 GitHub。我们打开[Travis 官网](https://travis-ci.com/),使用自己的 GitHub 账号授权登陆就可以了。登录完成后页面中会出现一个“Activate”按钮,点击按钮会跳回到 GitHub 中进行项目访问权限设置。我们保留默认的设置,点击“Approve&Install”即可。
:-: ![](https://img.kancloud.cn/06/55/0655107bfdbc132e9e1ab72dc42c194a_2018x1242.png)
图 2 激活 Github 集成
:-: ![](https://img.kancloud.cn/a5/51/a5512881dd0dd42dd845300302d8fb4d_1214x1584.png)
图 3 授权 Travis 读取项目变更记录
完成授权之后,页面会跳转到 Travis。Travis 主页上会列出 GitHub 上你的所有仓库,以及你所属于的组织,如下图所示:
:-: ![](https://img.kancloud.cn/6f/fd/6ffd97d34bbbb496d95d11fbaf9b2d36_2032x994.png)
图 4 完成 Github 项目绑定
完成项目绑定后,接下来就是**为项目增加 Travis 配置文件**了。配置的方法也很简单,只要在项目的根目录下放一个名为.travis.yaml 的文件就可以了。
.travis.yaml 是 Travis 的配置文件,指定了 Travis 应该如何应对代码变更。代码 commit 上去之后,一旦 Travis 检测到新的变更,Travis 就会去查找这个文件,根据项目类型(language)确定执行环节,然后按照依赖安装(install)、构建命令(script)和发布(deploy)这三大步骤,依次执行里面的命令。一个 Travis 构建任务流程如下所示:
:-: ![](https://img.kancloud.cn/53/56/535659463b5bcc2bde187dcabfa5fc04_1562x342.png)
图 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 构建详情,去查看失败原因并尽快修复问题了。
:-: ![](https://img.kancloud.cn/97/d9/97d9fa2c64e48ff50152c4b346372190_768x290.png)
图 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 进入开发者设置。
:-: ![](https://img.kancloud.cn/c1/5f/c15f24234d621e6c1c1fa5f096acc587_2004x1262.png)
图 7 进入开发者设置
在开发者设置页面中,我们点击左下角的 Personal access tokens 选项,生成访问 token。token 设置页面提供了比较丰富的访问权限控制,比如仓库限制、用户限制、读写限制等,这里我们选择只访问公共的仓库,填好 token 名称 cd\_demo,点击确认之后,GitHub 会将 token 的内容展示在页面上。
:-: ![](https://img.kancloud.cn/1c/7a/1c7ac4bd801f44f3940eff855b9e2171_1600x904.png)
图 8 生成访问 token
需要注意的是,这个 token 你只会在 GitHub 上看到一次,页面关了就再也找不到了,所以我们先把这个 token 复制下来。
:-: ![](https://img.kancloud.cn/8e/f0/8ef0ba439f181596f516ec814d80c5ca_1508x682.png)
图 9 访问 token 界面
接下来,我们打开 Travis 主页,找到我们希望配置自动发布的项目,然后点击右上角的 More options 选择 Settings 打开项目配置页面。
:-: ![](https://img.kancloud.cn/4d/34/4d34efe29bb2135751f5aba3ffdc4694_1746x656.png)
图 10 打开 Travis 项目设置
在 Environment Variable 里,把刚刚复制的 token 改名为 GITHUB\_TOKEN,加到环境变量即可。
:-: ![](https://img.kancloud.cn/67/82/67826feaefba3105368387e1cfefd5c7_1678x236.png)
图 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 构建完毕后可以看到,我们的工程已经具备自动发布构建产物的能力了。
:-: ![](https://img.kancloud.cn/36/2f/362ff95d6f289e75bb238a06daf88d25_1048x1158.png)
图 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 工程也可以自动的发布原生组件了。
:-: ![](https://img.kancloud.cn/80/8a/808aa463cec67002b26ad47a745f8a0d_990x1140.png)
图 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混合开发框架(二)?
- 结束语
- 结束语丨勿畏难,勿轻略