开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第15天,点击查看活动详情
前文传送门
《从零开始|构建 Flutter 多引擎渲染组件:先导篇》
《从零开始|构建 Flutter 多引擎渲染组件:Flutter 工程篇》
《从零开始|构建 Flutter 多引擎渲染组件:Flutter 代码篇》
《从零开始|构建 Flutter 多引擎渲染组件:Native 调用篇》
前些文章比较注重源码解释,所以堆了较多的代码来做详细说明,这行为有些吃力不讨好 – -|
本篇会用更少的代码以及更多的思考来具体描述我们是如何落地 Flutter 多引擎渲染组件开发工具链的。
引题
先想想我们构建跨端 UI 组件库的目标是什么?除了 UI 一致性渲染外,最重要的还是降本增效啊。毕竟如果公司肯用“爆兵战术”不计成本的话啥能做不出来?(说的是有效成本,当然要排除小马哥那种内部贪腐的例子[狗头])。要用有限的资源来做更大的“瑞士卷”,这即符合今后的发展趋势,也是“秃头”程序员一生所追求的(啊 bushi)。
而在前文的开发流程中,写一个 Flutter 多引擎渲染组件实在实在是太费事了,虽然我们用了 pigeon 减少了一部分通信代码,但剩下的代码流程也足以让一个开发变成退堂鼓选手。如何解决开发难的问题,才是本系列的重中之重。
方案
先回想下笔者在先导篇中所说的 FGUIComponentAPI
, 有看过之前几篇文章的同学应该会对它有所印象,但 FGUIComponentAPI
并不单指跨端组件库,也不单是今天所说的跨端工具链,它是整体方案的集合概念,包括配套环境、开发流程、结果产物。
再回到本文所要讲的跨端工具链,要解决的就是开发流程的问题。简化开发流程,提高开发体验。
《说到跨端工具链,我们是在说什么?》,对跨端工具链概念迷惑的同学可以看一下笔者这篇文章。
实现效果
先看一下现在的使用效果
在 VSCode 中执行 command + shift + p
选择 GaodingFlutter: run generate api
命令后,即会自动根据组件定义生成了相应的基础代码,包括前几篇文章内所讲的 api.dart
、base.dart
、.h / .m
、.kt
等。
这里还要另外说明一下,生成的文件我们选择不上传 git,好处有很多:减少无效 code review,防止提交冲突,生成结果变更对开发者来说无感知。而带来的代价是需要在组件库变化后,需要开发执行一下命令才能正常运行。当然我们在 VS Code Extension 中也如 flutter pub get
命令一样,做了更新提醒。
《构建 VS Code Extension,提高 Flutter 开发效率(一)》 有兴趣可以看看我们如何是开发扩展的
下面就具体讲一下我们在其中做了什么以及是如何做的。
建设要点
整体流程如上图所示,这就是命令执行的整个调用流程,看起来流程不算短,但只要明白了其中思想,整个流程其实是水到渠成的体力活。
定义而非 AST
我们为什么用 YAML 定义组件而不是类似 pigeon 用 Flutter 代码 code to code 的形式实现,最主要的原因是对开发者使用上进行限制及规范,防止不正确的。
我们在项目里增加了一个 schema.json
来提供说明及 Lint 能力。
ui_components.schema.json 部分示例
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://json.schemastore.org/ui_components",
"description": "UI Components 定义规范",
"items": {
"type": "object",
"description": "定义组件",
"required": ["name", "options"],
"properties": {
"name": {
"type": "string",
"default": "XXX",
"description": "组件名称",
"examples": ["XXXView"]
},
"init": {
"type": "array",
"description": "组件初始化参数",
...
}
...
}
...
}
效果如下图,可以很好的限制传入参数的类型或者值,以及提供每一项的说明及快捷生成使用模版。
具体定义规范,我们在之前的文章 《Flutter 多引擎渲染,在稿定 App 的实践(二):原理篇》 中有详细描述,这里不再做赘述。
(定义文件放在组件包内,便于维护)
我们在脚本代码里,也是有定义相关的类,当然,直接用 map
也没什么问题,脚本语言讲究没那么多,这里手段上选的 Ruby
,用 Python
或者 Node
也都可以,但不要用 dart 脚本,效率太低。
拷贝资源文件
如上图所示的 Android 部分,毕竟我们生成后是一个独立的插件包,所以必要的需要复制一些固定资源,这样减少模版代码的工作量。
构造模版代码
什么是模版代码?直接贴图举例,还是上图 Android 模版中,直接看预览:
其实模版就是把真实代码中需要变量的部分,用特定的字符串替代,例如 ${NAME}
,然后在脚本代码中做全局替换即可,是不是十分简单 ~
脚本代码里要做的事,一是类型转换,二是代码字符串拼装即可。
类型转换
我们在 YAML 用的是 Flutter 语法做 type
定义,这里需要转成其他端语言的使用方式,这里举例比较恶心的 OC 代码,就需要提供额外的工具方法来处理修饰符:
以及 *
指针的问题。
这一点除了官方有提供 AST 工具的(比如 Typescript),大部分三方库也都是类似的设计,只不过笔者这里功能比较单一,所以处理方式就更简单粗暴些,有名的三方库会使用比如策略模式等设计模式让代码更优雅,但原理都一样,都是字符串的转换。
代码拼接
代码拼接就更容易理解了,读取模版 – 全局替换 – 写入文件而已,简单发一下代码:
dart_part_api.rb
module DartBaseGenerator
...
def DartBaseGenerator.generator_api(model)
# 写入路径
filePath = File.join(@apiDicPath, model.name.underscore + ".dart")
# 读取模版
content = File.read("./resources/Flutter/api.dart.txt")
# 全局替换
content = content.gsub("${NAME}", model.name) ...
file = File.new(filePath, "w")
if file
file.syswrite(content)
end
file.close
end
end
优化脚本效率
抛开 pigeon 生成部分,脚本效率都是毫秒级的,毕竟只是是 O(n) 复杂度的替换过程以及 I/O 文件读写。但 pigeon 确实非常耗时,文件内容越多执行效率越慢,而且一次只能执行一个文件。
(这里本来想录个屏,发现现在新增一个组件生成起来需要几十秒了,所以还是不上传了)
简单分析下 pigeon 耗时原因,一个问题它是 dart 脚本,和 java 写脚本一样启动虚拟机环境需要时间,二是它使用了 dart AST 解析工具 analyzer,这个可能也是不完善的,解析上耗时很高。
那我们做了哪些优化?
按模块分包
组件库按模块分成多份,fgui、part_home、part_video 等等,这样来平衡 pigeon 脚本执行次数和文件内容大小两者的关系。
执行跳过逻辑
在分包后,简单的基于组件包内的 ui_components.yaml
文件 sha1 标识来做了一个执行跳过逻辑。
简单代码示意:
run_base.rb
module BaseGenerator
...
def BaseGenerator.excute
paths = get_yml_paths()
paths.each do |path|
models = fetch_models(path)
$path_models[path] = models
# 计算 YAML 文件 sha1
sha1 = $script_version + ":" + Digest::SHA1.hexdigest(File.read(path))
# 读取缓存的 sha1 值
save_sha1 = read_yaml_version(path)
if sha1 != save_sha1
# 如果不相等,则执行生成命令
puts "[fgui_component_api]: -- Generating from #{path}"
...
else
# 跳过
puts "[fgui_component_api]: -- skip from #{path}"
end
...
# 保存当前版本号
write_yaml_version(path, sha1)
end
end
end
版本号保存在隐藏文件里 .ui_components.version
前面的 1.0.3 是脚本当前的版本,这样来用于脚本生成代码发生变化时,可以控制让各开发同学都更新,而不是用旧的。
很简单的设计,但很有效,而且可以提高打包机 CI 效率。
生成 Examples
Example 为什么也做生成?除了简便开发外,还有一点是能引导开发提供完善的测试用例。
这在生成上做了三件事:
- 生成索引目录(一级、二级)
- 生成 example.dart
- 生成路由跳转
新增组件后,只需要编码红框 body 部分即可使用调试。
当然,这个功能只对新增的组件有效,会提前判断是否已生成 example 代码。
生成说明文档
有心的同学可以看到流程图上“生成 Docs” 部分的样式跟其他的不同,那是因为这个是执行单独的命令,但也是包括在 FGUIComponentAPI 工具链中。
(执行过程比较长,转成 gif 比较慢,所以视频压缩了下,用糊的)
除了第一步也是要完整生成组件库代码外,这里需要多执行一步:[fgui_component_api]: Generating preivew part
里面也是做了2件事:
构建 Web 产物
Flutter for Web 是真的好用,特别在做组件预览上,这里有几个关键点需要注意,flutter build web --dart-define=FLUTTER_WEB_CANVASKIT_URL=https://x.gdm.gaoding.com/artifacts/doc/canvaskit/ --web-renderer canvaskit;
我们的打包语句如上,--web-renderer canvaskit
是为了预览上渲染一致性。--dart-define=FLUTTER_WEB_CANVASKIT_URL=
是指定到内网 canvaskit 目录下,不然会很慢。
还有一点需要注意的是,打包出来的 web 是绝对路径定位的 <base href='/'>
,由于部署原因,最好换成相对的,这可以直接在 /web/index.html 中修改,也可以像笔者一样,在脚本上增加一个替换即可,这样可以少提交代码。
index_html_content = index_html_content.gsub("\<base href=\"/\"\>", "\<base href=\"./\"\>")
生成 Markown
我们移动端在线文档是用的 vuepress 做的,所以这里需要提供符合规范的 Markdown 文件即可。
这个的做法也类似,提供模版代码,生成 .md
和 flutterComponentsSidebar.js
侧边栏控制。
需要手动提 PR 来触发文档更新。
控制目标版本
这里用 git 分支来实现版本号控制,这样各调用端可以提交分支号来保证提交记录可回溯。
另外漏说了一句,FGUIComponentAPI 产物也和其他工具产物一起放入在 application-services 应用服务中。这个应用服务还没有完全清晰,待完善后会单独写一篇文章来做介绍。
还做了什么
生成代码上我们还做很多事,比如统一的 SLS 性能采集,提供设备信息,提供埋点方法等等。
甚至可以把网络通信等放进去,但这个不是通用组件的实现范畴,所以并不考虑。但也有在规划用 Flutter 多引擎方案做页面来替代 flutter_boost 实现,可能会用类似的方式提供一套 Flutter 多引擎间的状态管理。
未来规划
版本自动化
这里面版本控制问题并没有完全解决,Flutter 版本也需要跟 Native 调用端代码保持一致,那这个版本号变更放到代码合入流程里最为合适,但这之间也有蛮多的前置 CI 问题需要解决。
去除 pigeon
生成效率在当前阶段也还算是可以忍受的,但最佳实践肯定是去除掉 pigeon,用模版代码代替 AST 解析实现,这样整个过程就可以达成毫秒级。
感想
本篇是本系列收尾篇,年前最后一篇文章了。也是对整年工作学习的一个总结:认识本质、讲究方法、达成目标。
整套方案也是存在各种的不足点,毕竟笔者精力还是有限的。做到极致还是做到可用?是要考虑公司的整体规划和利益,毕竟也只是一个普普通通的天选“冬阴工”[手动狗头]。能写在掘金上也是因为公司支持开源,不搞敝帚自珍,陆陆续续把记录在公司文稿上的记录整理了过来。
明年文章的方向上还是会在跨端开发领域,但可能不再聚焦于 Flutter 技术链上。毕竟解决问题的手段并不重要,最重要的是选择合适的手段达成目标,即“不择手段”[手动狗头]。
最后说句祝愿,希望明年是一个好年,疫情向下,经济向上。大家明年共济沧海 ~
感谢阅读,如果对你有用请点个赞 ❤️