FairPushy 是基于Flutter+Dart三端一体化打造的动态更新平台主要由Web + Server + Native全部使用Flutter+Dart编写,为Flutter动态化场景提供动态分发能力,全方位降低上手成本,提升开发体验。实现了动态化资源产物自动化打包和动态分发的能力,让开发者摆脱了技术栈的壁垒,并且系统轻量化、简单易用,目前项目已在Github开源:
动态化平台 github:https://github.com/wuba/fairpushy
Fair github:https://github.com/wuba/fair
Flutter相信大家一定不陌生了,它的设计初衷,就是允许在各种操作系统上复用同样的代码,例如 iOS 和 Android,用Flutter写的软件程序就能够在不同的平台上拥有原生体验的高性能应用。得益于他在每一个平台上,都会包含一个特定的嵌入层,从而提供一个程序入口,程序由此可以与底层操作系统进行协调。从2018年2月17日发布的beta1版本到现在3.0版本已经过了4个多年头。之所以这么火热无疑离不开他可以跨平台的特性和较高的UI性能,几乎满足的所有跨平台开发者的幻想,但是包大小和动态化问题也一直争议不断。
58也自研了Flutter动态化Fair,他是支持不发版(Android、iOS、Web)的情况下,通过业务bundle和JS下发实现更新,方式类似于React Native。Fair的UI渲染是无损的,可以做到像素级别的还原,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h1gw7bIE-1661761091066)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0b2a88a95246469db7803065e32b51d4~tplv-k3u1fbpfcp-zoom-1.image)]
动态化带来的好处毋庸置疑,会及时高效的满足业务需求,提升用户体验同时也会减少初始包的大小,提高装机率。如果出现质量问题也可以在不发版的情况快速得到解决。然而Fair动态化的bundle和JS产物需要有一个Web平台来管理分发,涉及Server、Web和Flutter插件,Flutter是基于Dart语言,Dart语言官方给的定义是”是面对对象的、单继承的语音他的语法与C语言有点类似,可在任何平台上开发快速的应用程序“因此我们打算全部用Dart语言来开发动态化平台。
目前开发Server的语言已经很成熟了,比如常见的JAVA、Node.js、Go、PHP、Python等(排名不分先后)。从这几年的排行榜看Java仍稳坐铁王座第一名,是最受欢迎的语言,这肯定离不开他的语言特性:是一种简单的,面向对象的,分布式的,健壮安全的,可移植的,性能优异、多线程的动态语言。PHP(PHP: Hypertext Preprocessor)即“超文本预处理器”,是在服务器端执行的脚本语言,尤其适用于Web开发并可嵌入HTML中。PHP语法学习了C语言,吸纳Java和Perl多个语言的特色发展出自己的特色语法;该语言当初创建的主要目标是让开发人员快速编写出优质的web网站比较适合用于个人网站、企业官网等轻量级的项目开发。Python语法和动态类型,以及解释型语言的本质,使它成为多数平台上写脚本和快速开发应用的编程语言。Go(又称Golang)是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。而Node.js和Dart的介绍如下:
Node.js: 一个基于 Chrome 的 JavaScript 运行时构建的平台,使用了一个事件驱动、非阻塞式I/O模型,让JavaScript 运行在服务端的开发平台,如果你有前端开发经验无疑更适合用Node.js来开发服务端。
Dart: 一种新的 Web 编程语言,包含库、虚拟机和工具。Dart 是一个内聚的、可扩展的平台,用于构建在 Web(您可以使用 Polymer)或服务器(例如使用 Google Cloud Platform)上运行的应用程序。使用 Dart 语言、库和工具编写从简单脚本到功能齐全的应用程序的任何内容。
语言没有绝对的好坏,需要根据业务和成本选择合适的。单领出来Node.js和Dart介绍主要考虑使用Flutter开发项目的大部分是前端和移动端同学,具体选择那个需要从业务的场景中考虑,如果说我是移动端开发Flutter项目对Dart语言相当熟悉这样就更适合用Dart来开发Server,反之如果你是一名前端开发则用Node.js更合适些。既然是开发Flutter项目如果能使用Dart开发后端服务最大的优势也就是不需要学习新的语言、最大程度保证平台一致性、减少语言的学习成本和重复工作成本;因此我们打算全部用Dart语言来开发动态化平台。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GHIjXsDI-1661761091067)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/04e761b54e2d4964b4c80c618ec74962~tplv-k3u1fbpfcp-zoom-1.image)]
整体设计是以Dart语言为支撑,Dart语言是面向对象的,特点和平常用的语言有类似的语法、运行时环境变量,可以运行在浏览器、dart虚拟机和移动设备上。并且同时支持 JIT(Just In Time,即时编译)和 AOT(Ahead of Time,运行前编编译)的语言之一。上图的FairPushy大致可以分为四个方面如下:
1. Dart support: 其中包括dart:core实现基础的内置类型、集合以及其它的一些核心功能和isolate实现并发编程
2. Dart Server: 主要包括数据库和连接池、ORM、RPC框架的建设和对Web和移动端提供上层业务的的HTTP接口的支撑
3. Flutter Web和Fair Sdk: Flutter Web提供Fair产物的打包上传、环境切换和动态化编译功能,Fair SDK主要负责Fair资源产物的下载、缓存和加载功能
4. 运维和研发支撑: 因为整体的设计语言是相同的所以一些研发的基础组件也可以共用比如一些日志库、网络库、Crash监控和性能监控。
由于篇幅问题本分只分析Dart Server开发的相关知识点,伟人曾经说过 你要想知道梨子的滋味,就要亲口尝一尝,但是任重而道远。Dart Server实现的难点:
基础组件建设: 如何写HTTP接口、如何设计SQL表、如何链接远端数据库等等
生态建设: 客户端同学初次接触到后端的知识,需要了解后端如何开发、部署和排查解决错误
监控系统: 代码出现异常错误如何监控和告警?
并发问题: Dart语言编写的后端服务是否可以满足高并发下业务请求?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1DDCLVun-1661761091068)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4548ee35024a4d42ab6eb3d6af36756c~tplv-k3u1fbpfcp-zoom-1.image)]
整体从初次接触后端服务开发者角度分析主要分为两个流程,开发阶段和部署阶段:
开发阶段:需要进行Dart环境的配置和Studio安装。我们封装了基于Dart Server基础库建设,其中最主要的是日志库、路由框架和Widget封装。其次是生态环境的建设这也是Dart Server最难的地方。像Java有Spring全家桶可以很方便的进行业务需求的开发,而Dart Server生态环境建设大致可分为异常的监控、RPC框架、ORM框架和MySql框架。具备了这些功能就可以进行业务需求开发了。
部署阶段:借助的58云平台完成服务的自动化部署,需要构建基于Dart环境的基础镜像来运行服务。之后通过Docker编写脚本通过Git分支拉取Dart Server的业务代码,然后执行run/server.dart脚本来服务的启动。启动后可以通过监控服务监听业务是否正常运行,如果出现阈值之外的异常会通过告警组通知开发人员。
让Dart语言开发后端服务需要一些基础库,比如如何链接远端数据库?如何接收HTTP接口请求?如何处理异常的错误?等等。具备了这些基础库就可以实现一些简单的接口开发了。在Flutter中一切皆可Widget,我们为此保持了与Flutter统一的编码风格,在写后端接口也可以和写前端页面一样一切皆可以Widget。在服务启动的时候需要注册ServerPages,所有的HTTP接口需要在此注册。原理和Flutter中的路由配置类似。其中name表示接口请求的路径,page表示实现这个接口逻辑具体实现类,method表示HTTP请求的方式Get或Post等,needAuth是一个bool值,表示这个接口是否需要登录鉴权,true则会校验Token如果不满足会返回鉴权错误的Response。
mixin ServerPages {
static final routes = [
GetPage(
name: Routes.GET_APP_PATCH,
page: () => GetBundlePage(),
method: Method.get,
needAuth: false,
),
GetPage(
name: Routes.GET_PROJECT,
page: () => GetProjectPage(),
method: Method.post,
),
];
}
我们封装了FairServiceWidget可以通过继承实现server()来很方便的获取HTTP接口请求的参数;PatchDao是ORM框架的模型的实现类后面会详细介绍,可以通过他来实现数据库的增删改查,searchBundleId是查询数据的具体SQL语句,最后通过toBundleJson来实现数据的序列化。以动态化平台实际的功能获取Bundle资源接口为例,一个完整的HTTP接口请求大致如下1、继承FairServiceWidget实现Server()处理业务逻辑2、通过request_params获取请求的参数和参数的校验3、参数正确会通过PatchDao查询远端数据库信息4、获取bundle数据后会进行Response封装返回:
class GetBundlePage extends FairServiceWidget {
@override
Future service(Map? request_params) async {
var bundleId = request_params?['bundleId'];
if (bundleId == null || bundleId == "") {
return ParamsError(msg: "bundleId==null");
}
var bundleList = [];
await withTransaction(() async {
final dao = PatchDao();
var rows = await dao.searchBundleId(bundleId);
for (int i = 0; i < rows.length; i++) {
bundleList.add(rows[i].toBundleJson());
}
}).catchError(((error, stack) {
ResponseError(msg: error.toString());
}));
return ResponseSuccess(data: bundleList);
}
}
Dart是单线程执行,也就是说一旦Dart函数开始执行,就会一直持续直到结束,Dart函数不能被其他Dart代码中断。不过Dart可以通过 async-await、isolate 支持并发代码编程。Dart 代码并不在多个线程上运行,取而代之的是它们会在 isolate 内运行。每一个 isolate 会有自己的堆内存,其各自的GC不会影响到其他isolate的,从而确保 isolate 之间互相隔离,无法互相访问状态。由于这样的实现并不会共享内存,所以不需要担心 互斥锁和其他锁。所以我们可以通过把用内存空间较大且生命周期较短的接口放到isolate中,这样即使另外一个isolate GC了并不会对我们正常的业务流程造成影响。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LSsqnHHw-1661761091068)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5309962fdce84769bfb91770bbb85c56~tplv-k3u1fbpfcp-zoom-1.image)]
在使用 isolate 时, Dart 代码可以在同一时刻进行多个独立的任务,并且使用可用的处理器。 Isolate 与线程和进程近似,但是每个 isolate 都拥有独立的内存,以及运行事件循环的独立线程。Event queue能够确保同时处理多个任务。event loop的工作就是从event queue内拿一个event然后处理它,一直重复这个操作直到queue里全部处理完毕。event queue内的event有可能是文件I/O、timers等等
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GZwKpG4k-1661761091068)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4ef6fc1733d447d7a9381ced38c12fc5~tplv-k3u1fbpfcp-zoom-1.image)]
如图所示,Dart应用程序在其main isolate执行应用程序的main()函数时开始执行。 main()退出后,main isolate开始逐个处理events queues的内容。一个Dart应用程序只有一个event loop,但是有两个Queue,event queue包含所有的外部事件,I/O、timers、两个isolates之间的消息等,microtask queue则表示一个短时间内就会完成的异步任务。它的优先级最高,高于event queue,只要队列中还有任务,就可以一直霸占着事件循环。microtask queue添加的任务主要是由 Dart内部产生。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f5st7Q1D-1661761091068)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/158af580f4af413d80c5c1f21e3035ea~tplv-k3u1fbpfcp-zoom-1.image)]
Dart开发后端服务是空白的,需要一些组件才能让开发更便利,比如远端SQL链接、连接池、日志监控和ORM等。当然业界也有一些开源的框架可以直接使用。因为篇幅受限,所以我们找其中一个主要的点来分析:ORM框架在Dart Server中的实践。
ORM 的英文是 Object Relation Mapping,对象关系映射,是 RDBMS 和业务实体对象之间的一个映射,把底层的 RDBMS 封装成业务实体对象,提供给业务逻辑层使用。ORM框架提供了一种持久化模式,在Java中比较常见框架是MyBatis,他可以高效地对数据库进行访问。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vq0rPp9h-1661761091068)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c30e51178f0e4eeb9f1c4689d32208ec~tplv-k3u1fbpfcp-zoom-1.image)]
在Dart Server中实现ORM需要封装对数据库的连接、数据对象映射和增删改查等,可以按照前面介绍的获取Bundle资源接口为例子,需要先创建PatchDao类,内部需要实现了对数据信息的封装,只需要填写要操作的表明、映射的对象类和关键查询语句即可。比如要更新补丁表信息则可以直接传入最新的映射的对象类实现更新即可。
class PatchDao extends Dao {
PatchDao() : super(tablename);
PatchDao.withDb(Db db) : super.withDb(db, tablename);
static String get tablename => 'patch_info';
@override
Patch fromRow(Row row) => Patch.fromRow(row);
Future updateByPatch(Entity entity) async {
final fields = entity.fields;
final values = convertToDb(entity.values);
final sql = 'update patch_info '
'set `${fields.join("`=?, `")}`=? '
'where bundle_id=?';
await db.query(sql, [...values, entity.id]);
}
}
可以看到上面在做update的sql语句是通过拼接处理,我们提供一套公共sql语句模板,然后在具体实体对象操作的时候将实体对象的属性名称和属性值当作参数拼接进去,组装成完整的sql语句。如果查询操作则会根据返回的数据映射成Dart对象类,所以数据驱动返回的数据通常都是以数据为核心的数据集合,我们通过ORM框架将类对象和数据库返回的列数据进行一一匹配获取,然后赋值到对象上:
List fromResults(Results results) {
final rows = [];
for (final results in results) {
rows.add(fromRow(Row(results.fields)));
}
return rows;
}
ORM还为能我们做了什么?ORM框架做的最多的便是“缓存”。因为数据库操作是要和硬盘打交道的,而程序是在内存中运行的,操作内存的速度要比操作硬盘快数十倍以上,可见一个访问量较高的大型系统很容易由于数据库操作过于频繁而拖慢整体速度,从而影响系统的使用。因此,ORM框架还需要帮助我们减少数据库的访问,加快系统速度。
Flutter Fair动态化产物有json和js文件需要上传到CDN服务动态分发到APP上来实现动态化功能,其中产物需要在Flutter Web平台进行打包上传,目前支持两种方式:
手动上传:需要开发者在本地编译器编译生成Fair产物,到Flutter Web平台手动上传打包好的产物包\
自动上传:开发者无需关心Fair产物,只需要在Flutter Web平台配置项目Git地址、Flutter版本等,点击在线编译即可触发自动化构建和Fair产物上传
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DYaqzr4k-1661761091069)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d1bfec5c493147a38aa0c4125d76d3c0~tplv-k3u1fbpfcp-zoom-1.image)]
如图所示,自动化上传Fair产物由开发者在Flutter Web平台配置构建信息,转到Dart Server 进行编译处理,根据Web平台的构建配置信息会进行入库操作,把Git地址、Git分支和Flutter版本等与本次创建的Fair资源做关联记录。之后会进行环境清理、拉取填入的GIt项目地址到服务器,执行flutter pub get和flutter pub run build_runner build命令生成Fair产物,关键脚本如下:
var shell = Shell();
shell = shell.cd("/opt/");
await shell.run('''
rm -rf $git_dir_path
mkdir -p $git_dir_path
''');
shell = shell.cd(git_dir_path);
await shell.run('''
git clone $patchGitUrl ./
git checkout $patchGitBranch
flutter pub get
flutter pub run build_runner build
''');
拿到Fair编译产物后会上传到cdn,然后获得cdn上传后的url,把url更新patch_online_build数据库表中。至此完成了整个动态化编译流程。其中需要借助Docker来完成一部分的脚本执行,docker是一个开源的应用容器引擎,基于go语言开发并遵循了apache2.0协议开源。 docker可以打包应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何的linux服务器,也可以实现虚拟化。 容器是完全使用沙箱机制,并且容器开销极其低,感兴趣的可以查阅资料了解下。
本文分享主要以Flutter Fair动态化为背景的热更新平台实现方案,通过对Dart Server研究和实践确认Dart开发后端服务的可行性,对于开发Flutter的客户端和前端同学可以扩充视野和提高整体化思维,并且极大的减少沟通成本。“任总而道远”需要实现的功能还是有很多,并且需要开发者经过不停的迭代与优化才能越做越好,这个过程将会是一个漫长且繁琐的过程。目前项目已经在Github上开源,也欢迎对Flutter Dart和动态化感兴趣的同学给我们点个star给予鼓励:
动态化平台 github:https://github.com/wuba/fairpushy
Fair github:https://github.com/wuba/fair
参考文献和其他Fair文章: