IT科技

打通前后端逻辑,客户端Flutter代码一天上线

时间:2010-12-5 17:23:32  作者:应用开发   来源:IT科技  查看:  评论:0
内容摘要:背景随着闲鱼的业务快速增长,运营类的需求也越来越多,其中不乏有很多界面修改或运营坑位的需求。闲鱼的版本现在是每2周一个版本,如何快速迭代产品,跳过窗口期来满足这些需求?另外,闲鱼客户端的包体也变的很大

背景

随着闲鱼的打通端逻r代业务快速增长,运营类的前后需求也越来越多,其中不乏有很多界面修改或运营坑位的辑客需求。闲鱼的户端版本现在是每2周一个版本,如何快速迭代产品,码天跳过窗口期来满足这些需求?上线另外,闲鱼客户端的打通端逻r代包体也变的很大,Android的前后包体大小,相比2016年,辑客已经增长了近1倍,户端怎么能将包体大小降下来?码天首先想到的是动态化的解决此类问题。

对于原生的上线能力的动态化,Android平台各公司都有很完善的打通端逻r代动态化方案,甚至Google还提供了Android App Bundles让开发者们更好地支持动态化。前后由于Apple官方担忧动态化的辑客风险,因此并不太支持动态化。因此动态化能力就会考虑跟Web结合,从一开始基于 WebView 的 Hybrid 方案,服务器托管到现在与原生相结合的 React Native 、Weex。

与此同时,随着闲鱼Flutter技术的推广,已经有10多个页面用Flutter实现,上面提到的几种方式都不适合Flutter场景,如何解决这个问题Flutter的动态化的问题?

动态方案

我们最初调研了Google的动态化方案CodePush。

01

Code Push

CodePush是谷歌官方的动态化方案,Dart VM在执行的时候,加载 isolate_snapshot_data 和 isolate_snapshot_instr 2个文件,通过动态更改这些文件,就达到动态更新的目的。官方的Flutter源码当中,已经有相关的提交来做动态更新的内容,具体可以参考 ResourceExtractor.java。目前,此功能还在开发中,期待ing。

02

动态模版

动态模板,香港云服务器就是通过定义一套DSL,在端侧解析动态的创建View来实现动态化,比如LuaViewSDK、Tangram-iOS和Tangram-Android。这些方案都是创建的Native的View,如果想在Flutter里面实现,需要创建Texture来桥接;Native端渲染完成之后,再将纹理贴在Flutter的容器里面,实现成本很高,性能也有待商榷,不适合闲鱼的场景。

所以我们提出了闲鱼自己的Flutter动态化方案,前面已经有同事介绍过方案的原理:《做了2个多月的设计和编码,我梳理了Flutter动态化的方案对比及最佳实现》,下面看下具体的实现细节。

模版编译

自定义一套DSL,维护成本较高,怎么能不自定义DSL来实现模板下发?闲鱼的方案就是直接将Dart文件转化成模板,这样模板文件也可以快速沉淀到端侧。站群服务器

01

模版规范

先来看下一个完整的模板文件,以新版我的页面为例,这个是一个列表结构,每个区块都是一个独立的Widget,现在我们期望将“卖在闲鱼”这个区块动态渲染,对这个区块拆分之后,需要3个子控件:头部、菜单栏、提示栏;因为这3部分界面有些逻辑处理,所以先把他们的逻辑内置。

内置的子控件分别是 MenuTitleWidget、 MenuItemWidget和 HintItemWidget,编写的模板如下:

@override Widget build(BuildContext context) {     return new Container(         child: new Column(             children: <Widget>[                 new MenuTitleWidget(data),    // 头部                 new Column(    // 菜单栏                     children: <Widget>[                         new Row(                             children: <Widget>[                                 new MenuItemWidget(data.menus[0]),                                 new MenuItemWidget(data.menus[1]),                                 new MenuItemWidget(data.menus[2]),                             ],                         )                     ],                 ),                 new Container(    // 提示栏                     child: new HintItemWidget(data.hints[0])),             ],         ),     ); }

中间省略了样式描述,可以看到写模板文件就跟普通的widget写法一样,但是有几点要注意:

每个Widget都需要用 new或 const来修饰

数据访问以 data开头,数组形式以 []访问,字典形式以 .访问

模板写好之后,就要考虑怎么在端上渲染,早期版本是直接在端侧解析文件,但是考虑到性能和稳定性,还是放在前期先编译好,然后下发到端侧。

02

编译流程

编译模板就要用到Dart的 Analyzer库,通过 parseCompilationUnit函数直接将Dart源码解析成为以 CompilationUnit为Root节点的AST树中,它包含了Dart源文件的语法和语义信息。接下来的目标就是将 CompilationUnit转换成为一个JSON格式。

上面的模板解析出来build函数孩子节点是 ReturnStatementImpl,它又包含了一个子节点 InstanceCreationExpressionImpl,对应模板里面的 newContainer(…),它的孩子节点中,我们最关心的就是 ConstructorNameImpl和 ArgumentListImpl节点。 ConstructorNameImpl标识创建节点的名称, ArgumentListImpl标识创建参数,参数包含了参数列表和变量参数。

定义如下结构体,来存储这些信息:

class ConstructorNode {     // 创建节点的名称     String constructorName;     // 参数列表     List<dynamic> argumentsList = <dynamic>[];     // 变量参数     Map<String, dynamic> arguments = <String, dynamic>{ }; }

递归遍历整棵树,就可以得到一个 ConstructorNode树,以下代码是解析单个Node的参数:

ArgumentList argumentList = astNode;

for(Expression exp in argumentList.arguments){

if(exp isNamedExpression){

NamedExpression namedExp = exp;

finalString name =ASTUtils.getNodeString(namedExp.name);

if(name ==children){

continue;

}

/// 是函数

if(namedExp.expression isFunctionExpression){

           currentNode.arguments[name]=

FunctionExpressionParser.parse(namedExp.expression);

}else{

/// 不是函数

           currentNode.arguments[name]=

ASTUtils.getNodeString(namedExp.expression);

}

}elseif(exp isPropertyAccess){

PropertyAccess propertyAccess = exp;

finalString name =ASTUtils.getNodeString(propertyAccess);

       currentNode.argumentsList.add(name);

}elseif(exp isStringInterpolation){

StringInterpolation stringInterpolation = exp;

finalString name =ASTUtils.getNodeString(stringInterpolation);

       currentNode.argumentsList.add(name);

}elseif(exp isIntegerLiteral){

finalIntegerLiteral integerLiteral = exp;

       currentNode.argumentsList.add(integerLiteral.value);

}else{

finalString name =ASTUtils.getNodeString(exp);

       currentNode.argumentsList.add(name);

}

}

端侧拿到这个 ConstructorNode节点树之后,就可以根据Widget的名称和参数,来生成一棵Widget树。

渲染引擎

端侧拿到编译好的模板JSON后,就是解析模板并创建Widget。先看下,整个工程的框架和工作流:

工作流程:

开发人员编写dart文件,编译上传到CDN

端侧拿到模板列表,并在端侧存库

业务方直接下发对应的模板id和模板数据

Flutter侧再通过桥接获取到模板,并创建Widget树

对于Native测,主要负责模板的管理,通过桥接输出到Flutter侧。

01

模版获取

模板获取分为2部分,Native部分和Flutter部分;Native主要负责模板的管理,包括下载、降级、缓存等。

程序启动的时候,会先获取模板列表,业务方需要自己实现,Native层获取到模板列表会先存储在本地数据库中。Flutter侧业务代码用到模板的时候,再通过桥接获取模板信息,就是我们前面提到的JSON格式的信息,Flutter也会有缓存,已减少Flutter和Native的交互。

02

widget创建

Flutter侧当拿到JSON格式的,先解析出 ConstructorNode树,然后递归创建Widget。

创建每个Widget的过程,就是解析节点中的 argumentsList和 arguments 并做数据绑定。例如,创建 HintItemWidget需要传入提示的数据内容, newHintItemWidget(data.hints[0]),在解析 argumentsList时,会通过key-path的方式从原始数据中解析出特定的值。

解析出来的值都会存储在 WidgetCreateParam里面,当递归遍历每个创建节点,每个widget都可以从 WidgetCreateParam里面解析出需要的参数。

ArgumentList argumentList = astNode; for (Expression exp in argumentList.arguments) {     if (exp is NamedExpression) {         NamedExpression namedExp = exp;         final String name = ASTUtils.getNodeString(namedExp.name);         if (name == children) {             continue;         }         /// 是函数         if (namedExp.expression is FunctionExpression) {             currentNode.arguments[name] =                 FunctionExpressionParser.parse(namedExp.expression);         } else {             /// 不是函数             currentNode.arguments[name] =                 ASTUtils.getNodeString(namedExp.expression);         }     } else if (exp is PropertyAccess) {         PropertyAccess propertyAccess = exp;         final String name = ASTUtils.getNodeString(propertyAccess);         currentNode.argumentsList.add(name);     } else if (exp is StringInterpolation) {         StringInterpolation stringInterpolation = exp;         final String name = ASTUtils.getNodeString(stringInterpolation);         currentNode.argumentsList.add(name);     } else if (exp is IntegerLiteral) {         final IntegerLiteral integerLiteral = exp;         currentNode.argumentsList.add(integerLiteral.value);     } else {         final String name = ASTUtils.getNodeString(exp);         currentNode.argumentsList.add(name);     } }

通过以上的逻辑,就可以将 ConstructorNode树转换为一棵 Widget树,再交给Flutter Framework去渲染。

至此,我们已经能将模板解析出来,并渲染到界面上,交互事件应该怎么处理?

03

事件处理

在写交互的时候,一般都会通过 GestureDector、 InkWell等来处理点击事件。交互事件怎么做动态化?

以 InkWell组件为例,定义它的 onTap函数为 openURL(data.hints[0].href,data.hints[0].params)。在创建 InkWell时,会以 OpenURL作为事件ID,查找对应的处理函数,当用户点击的时候,会解析出对应的参数列表并传递过去,代码如下:

...

finalList<dynamic> tList =<dynamic>[];

// 解析出参数列表

exp.argumentsList.forEach((dynamic arg){

if(arg isString){

finaldynamic value = valueFromPath(arg, param.data);

if(value !=null){

           tList.add(value);

}else{

           tList.add(arg);

}

}else{

       tList.add(arg);

}

});

// 找到对应的处理函数

finaldynamic handler =

TeslaEventManager.sharedInstance().eventHandler(exp.actionName);

if(handler !=null){

   handler(tList);

}

...

效果

新版我的页面添加了动态化渲染能力之后,如果有需求新添加一种组件类型,就可以直接编译发布模板,服务端下发新的数据内容,就可以渲染出来了;动态化能力有了,大家会关心渲染性能怎么样。

01

帧率

在加了动态加载逻辑之后,已经开放了2个动态卡片,下图是新版本我的页面近半个月的的帧率数据:

从上图可以看到,帧率并没有降低,基本保持在55-60帧左右,后续可以多添加动态的卡片,观察下效果。

注:因为我的页面会有本地的一些业务判断,从其他页面回到我的tab,都会刷新界面,所以帧率会有损耗。

从实现上分析,因为每个卡片,都需要遍历 ConstructorNode树来创建,而且每个构建都需要解析出里面的参数,这块可以做一些优化,比如缓存相同的Widget,只需要映射出数据内容并做数据绑定。

02

失败率

现在监控了渲染的逻辑,如果本地没有对应的Widget创建函数,会主动抛Error。监控数据显示,渲染的流程中,还没有异常的情况,后续还需要对桥接层和native层加错误埋点。

后续计划

基于Flutter动态模板,之前需要走发版的Flutter需求,都可以来动态化更改。而且以上逻辑都是基于Flutter原生的体系,学习和维护成本都很低,动态的代码也可以快速的沉淀到端侧。

另外,闲鱼正在研究UI2Code的黑科技,不了解的老铁,可以参考闲鱼大神的这篇文章《重磅系列文章!UI2CODE智能生成Flutter代码——整体设计篇》。可以设想下,如果有个需求,需要动态的显示一个组件,UED出了视觉稿,通过UI2Code转换成Dart文件,再通过这个系统转换成动态模板,下发到端侧就可以直接渲染出来,程序员都不需要写代码了,做到自动化运营,看来以后程序员失业也不是没有可能了。

基于Flutter的Widget,还可以拓展更多个性化的组件,比如内置动画组件,就可以动态化下发动画了,更多好玩的东西等待大家来一起探索。

参考文献

https://github.com/flutter/flutter/issues/14330

https://www.dartlang.org/

https://mp.weixin.qq.com/s/4s6MaiuW4VoHr7f0SvuQ

https://github.com/flutter/engine

copyright © 2025 powered by 益强资讯全景  滇ICP备2023006006号-31sitemap