跨端工具链,在稿定终端研发中的实践

10,595 阅读7分钟

笔者的感概: 从现在的趋势看,App 开发者未来终究会是终端工程师,而终端工程师要会的技能除了要了解各端开发实现外,更重要的是具备串联各端、各语言的能力。

引言

去年写了篇文章,讲了下笔者理解的跨端工具链是什么样的。

在补齐跨端通信能力(GNB)后,整套工具链在稿定终端开发中已经是一个成熟体系了,可以系统的来介绍下我们在其中是做了些什么,是怎样落地的。

相关文章

说到跨端工具链,我们是在说什么?

跨端通信终结者|看我是如何保证多端消息一致性的

能力概览

跨端工具链是一个抽象概念,目的是把端与端的关系通过工具链条联系起来。

在具体实现上,我们是落在终端服务层(application-services),从物理架构上看,它就是一个独立的 git 仓库。

image.png

包括生成服务、终端组件、开发辅助扩展三个部分。

终端服务 application-services.png

那我们如何来理解终端服务层

我们结合 DevOps 的概念来看,终端服务其实是在 Dev 前置一步的环节中,为了开发服务的一层,往往是在构建前或者构建过程中参与编译使用。

生成服务

生成服务(codegen)类似工厂车间,它可以按照规范输入,依赖固定模板,生成规范输出的各终端产物。

想了解具体生成过程的,可以看这篇文章:从零开始|构建 Flutter 多引擎渲染组件:跨端工具链篇

工具语言选型上,早期多使用 Ruby 语言,原因是为了开发使用方便,毕竟公司内部都是用 Mac 开发,MacOS 内置了 Ruby 环境,没有什么预安装成本,适合前期探索落地。后续,考虑到与打包机、Dock 容器等环境保持一致,所以逐渐迁移成 Python 来开发。

无论是什么语言开发,都会增加 shell 文件main.sh来保证使用入口的一致性。Flutter 组件化工具以及跨端通信协议还会增加preview.sh来生成 markdown 预览文档。

在规范输入上,也会根据实际需求有一些差异,因为数据来源是不一致的,有些服务需要读取服务端接口,有些自闭环的可以写在生成服务本身,还有一些可以写在各个项目中,通过一定规范即可让工具读取到。

那在产物输出上也有区别,有些是作为文件直接生成到项目的模块中,功能上相对独立的会生成产物 SDK,作为终端组件

终端组件

凡是可以打包成 SDK 的,终究会被打包成 SDK。

我们在前期工具链设计上,产物尽量是一个独立的终端组件,以轻量为主。目的就是在后续稳定期可以提供独立的 cocoapods / gradle / npm / pub / ... 组件库,甚至是提供二进制库的形式来减少构建成本。

在组件角色上,现在是划分的比较明确:调用组件/支撑组件。顾名思义,比如 GNB 消息通信能力在 iOS / Android / 桌面端是支撑组件实现,而 Web / Flutter 是调用组件实现。

但这也不是绝对的,比如后续 Rust 加入进来,那 Rust 侧就是支撑组件的形式,而其他代码侧则是构建调用组件。也比如可能也会让 Flutter 来支撑 Web 开发。

在使用上,只要项目集成就可以具备相应的跨端能力,抹平各个平台差异性。

组件 SDK 化的另一个好处是调用 API 是不变的,对于上层业务来说不关心是如何实现的,SDK 如何变化,也无需改变业务代码,更改生成模版代码即可。

开发辅助扩展

可自动化的终究会自动化,可 AI 实现的终究会让我们失业[狗头]。

什么是开发辅助扩展? 提高开发效率,降低人为因素导致的意外问题的工具。我们更想把它理解成稿定终端 IDE的前身,现在是在VS Code上初步建设了Web & App Flutter & App 两个开发辅助扩展,用于把整个跨端开发构建流程自动化起来,减少开发同学心智成本,让开发只关心一端代码即可。

后续演变上,IDE建设就是把项目、运行终端设备、开发辅助扩展三者结合起来,让 Web / Flutter 工程直接在 App 项目中上构建运行调试。

开发参考:构建 VS Code Extension,提高 Flutter 开发效率(一)

落地方式

前面是介绍我们在稿定终端开发中跨端工具链都做了什么,也用终端服务层(application-services) 的形式把它们结合在一起。

但其实有一个十分重要问题,如何把终端服务层(application-services) 落地到具体的各个项目中,这问题的实质是在多个仓库代码如何有机的结合在一起。

这个问题也有很多种解法,笔者一一列出,然后讲一下我们的最佳实践。

方案选型

二方库/三方库

构建完成后组件上传到内部仓库(二方库)或者上传到 github / npmjs / pub / ..(三方库),当作独立组件来使用。

按理说这算是最终形态,但如果改动频繁就涉及到发包的问题、业务项目也要频繁的修改包版本号,对小范围内部开发来讲不友好。而且每个组件都需要独立一个版本号,版本号管理起来成本也十分大,比如发包前就要检查各个组件的版本清单。

Git 子模块(submodule)

Git 对于复杂多项目也提供了一个解决方案,使用 submodule 子模块的方式,把组件作为一个独立的仓库加入到业务仓库中。

看起来很符合我们的需求,但它的问题是自身存在很多缺陷,这里不搬运其他文章过来了,有兴趣的可以在掘金搜一下 git submodule 的坑。

我们的最佳实践

最后我们使用的这个方式姑且称作脚本仓库管理,市面上确实没有人说有这么做,但效果确实意外的好。

到底是个什么样的方案?

一句话描述:各终端仓库通过增加脚本代码setup_application_services.sh,自行通过各个语言环境的工具链,集成到项目构建过程中。

setup_application_services.sh

# 链接应用服务
 
#!/bin/sh
 
VERSION=0.1.1 # 使用的版本号,后续讲解作用
 
CURPATH=$(
  cd "$(dirname "$0")"
  pwd
)
cd $CURPATH
 
DIRECTORY=../application-services # as 服务下载位置,建议放到项目根目录
if [ ! -d "$DIRECTORY" ]; then
    git clone git@git.gaoding.com:gdmobile/application-services.git $DIRECTORY
fi
 
cd $DIRECTORY
git clean -f
git reset --hard
git remote update origin
git checkout $VERSION
git pull origin $VERSION
cd ..

详细讲一下这个脚本的作用:

  • 直接把application-services仓库下载到工程内部,作为一个完整的子文件夹。
  • 清理掉可能被开发操作的仓库改动。
  • 拉取该分支下最新的代码。

这样来保证每次执行脚本,都可以获得一个符合版本预期的正确仓库内容。

VESION常量实质上是指定git branch分支号,只不过我们生成名称为版本号的分支来实现的更优雅。(在开发测试中,笔者都是新增开发分支VERSION=dev/xxxx来做)

这种方式带来的好处:

  • 版本号可以统一管理。
  • 保证开发环境的干净,每次执行脚本都会还原一些意外的误改动。
  • 可当作普通组件修改测试,只要不执行脚本,就可以直接修改其中代码来验证问题。

唯一的坏处是拉取的是整个仓库,跟工程无关的代码也会被拉取下来,但这是可接受的,对于开发而言只是硬盘多占用一些,在开发过程中不受影响。

各端落地方式

稿定在各端项目上都是采用 Monorepo 多项目大仓管理的模式,所以我们需要的也是把application-services中的组件也当作它的一个项目,这样来抹平开发的认知成本。

也不能让开发同学手动去调用脚本,这样也太不优雅了,我们还需要把它融入到整个集成流程中。

下面分开讲下不同的工程我们都是如何做的(划重点)。

iOS

iOS 需要在 Pod 之前提前执行application-services.sh

首先需要在Podfile中最前面增加模块引入。

image.png

这样让 Pod 执行前先去执行manager.rb文件。

image.png

manager.rb 中关键代码:

# 拉取最新的 application-services
def setup_application_services
  Pod::UI.puts "check git application-services".green
  system File.join($PROJECT_PATH, "tools", "setup_application_services.sh")
end

在执行pod install / pod update时,就会先去执行setup_application_services.sh脚本。

image.png

然后是如何使用大仓模式呢?

这里推荐大家两个 Ruby 插件:cocoapods-monorepo 、cocoapods-headermap,用于大仓管理的,这都开源在 RubyGems(前同事留下的成果 ~)。

这里增加application-services下载后的文件夹索引路径即可。

image.png

但开源版本只支持传单个 path,需要进行一些扩展改造,建议下载插件源码到本地来使用。

Android

Android 比较简单些,只要新增一个 Gradle 执行文件即可。

application_services.gradle

task Setup(type: Exec) {
    exec {
        commandLine rootDir.getParentFile().getParent() + '/tools/setup_application_services.sh'
    }
}

而 Monorepo 公司 Android 是采用的手动指定,但也完全可以通过一个 Gradle 来遍历查询。

Flutter

Flutter 比较麻烦一点,因为flutter pub get流程并不适合做钩子,所以我们采用了另一种形式,利用 VS Code ExtensionFlutter 开发辅助工具增加命令来帮助开发把这个流程自动化。

image.png

Monorepo 也是通过手动指定依赖路径。

Web

Web 脚手架比较完善,很容易集成进去。

image.png

scripts 增加一下调用即可。

而 Monorepo 前端就更成熟了,我们使用 pnpm 的大仓方案即可。

pnpm-workspace.yaml

packages:
  - "src/*"
  - "../application_services/*"

总结

当前的跨端工具链建设只是开始,当下在稿定内部也逐渐推广起来,慢慢扩散到各个团队中去使用,未来上还是有更多的可能性。


感谢阅读,如果对你有用请点个赞 ❤️

中秋节GIF动图引导在看提示.gif