做了2个多月的设计和编码,我梳理了Flutter动态化的方案对比及最佳实现

avatar
@阿里巴巴集团
原文链接: mp.weixin.qq.com
01 背景

在端上为了提升App的灵活性, 快速解决万变的业务需求,开发者们探索了多种解决方案,如PhoneGap ,React Native ,Weex等,但在Flutter生态还没有好的解决方案。未来闲鱼都会基于Flutter 来跨端开发,如果突破发版周期,在不发版的情况下,完成业务需求,同时能兼容性能体验,无疑是更快的响应了业务需求。因此我们需要探索在Flutter生态下的动态化。

02 方案选择

借鉴Android 和Ios上的动态性方案,我们也思考了多种Flutter动态性方案。

a.下载替换Flutter编译产物

下载新的Flutter编译产物,替换 App 安装目录下的编译产物,来实现动态化,这在Android 端是可行的,但在Ios 端不可行。我们需要双端一体的解决方案,所以这不是最好选择。

b.类似React Native 框架

我们先来看看React Native 的架构

React Native 要转为android(ios) 的原生组件,再进行渲染。 用React Native的设计思路,把XML DSL转为Flutter 的原子widget组件,让Flutter 来渲染。技术上说是可行的,但这个成本很大,这会是一个庞大的工程,从投入产出比看,不是很好的选择

c.页面动态组件框架

由粗粒度的Widget组件动态拼装出页面,Native端已经有很多成熟的框架,如天猫的Tangram,淘宝的DinamicX,它在性能、动态性,开发周期上取得较好平衡。关键它能满足大部分的动态性需求,能解决问题。

三种方案的比较图表如:

根据实际动态性需求,从两端一致性,和性能,成本,动态性考虑,我们选择一个折中方案,页面动态组件的设计思路是一个不错的选择。

03 页面动态组件框架

在Flutter上使用粗力度的组件动态拼装来构建页面,需要一整套的前后端服务和工具。本文我们重点介绍前端界面渲染引擎过程。

语法树的选择

Native端的Tangram ,DinamicX等框架他们有个共同点,都是Xml或者Html 做为DSL。但是Flutter 是React Style语法。他自己的语法已经能很好的表达页面。无需要自定义的Xml 语法,自定义的逻辑表达式。用Flutter 源码做为DSL 能大大减轻开发,测试过程,不需要额外的工具支持。

 所以选择了Flutter 源码作为DSL,来实现动态化。

如何解析DSL

Flutter源码做为DSL,那我们需要对源码进行很好的解析和分析。Flutter analyzer给了我们一些思路,Flutter analyzer是一个代码风格检测工具。它使用package:analyzer来解析dart 源码,拿到ASTNode。

看下Flutter analyze 源码结构,它使用了dart sdk 里面的 package:analyzer

    dart-sdk:  

        analysis_server:

           analysis_server.dart

           handleRequest(Request request)

        analyzer:

           parseCompilationUnit()

           parseDartFile

           parseDirectives

Flutter analyze 解析源码得到ASTNode过程。

插件或者命令对analysis server发起请求,请求中带需要分析的文件path,和分析的类型,analysis_server经过使用 package:analyzer 获取 commilationUnit (ASTNode),再对astNode,经过computer分析,返回一个分析结果list。

同样我们也可以把使用 package:analyzer 把源文件转换为commilationUnit (ASTNode),ASTNode是一个抽象语法树,抽象语法树(abstract syntax tree或者缩写为AST)是源代码的抽象语法结构的树状表现形式.

所有利用抽象语法树能很好的解析dart 源码。

解析渲染引擎

下面重点介绍渲染模块

架构图:

1.源码解析过程

a.AST树的结构

如下面这段Flutter组件源码:

    import 'package:flutter/material.dart';

    class FollowedTopicCard extends StatelessWidget {

     @override

     Widget build(BuildContext context) {

       return new Container(

         padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 0.0),

         child: new InkWell(

           child: new Center(

             child: const Text('Plugin example app'),

           ),

           onTap: () {},

         ),

       );

     }

    }

它的AST结构:

从AST结构看,他是有规律的.

b.AST 到widget Node

我们拿到了ASTNode,但ASTNode 和widget node tree 完全是两个不一样的概念, 需要递归ASTNode 转化为 widget node tree.

widget Node 需要的元素

用Name 来记录是什么类型的widget

widget的arguments放在 map里面

widget 的literals 放在list 里面

widget 的children 放在lsit 里面

widget 的触发事件 函数map里面

widget node 加fromjson ,tojson 方法

可以在递归astNode tree 时候,识别InstanceCreationExpression来创建一个widget node。

2.组件数据渲染

框架sdk 中注册支持的组件,组件包括:

a.原子组件:Flutter sdk 中的 Flutter 的widget

b.本地组件:本地写好到一个大颗粒的组件,卡片widget组件

c.逻辑组件:本地包装了逻辑的widget组件

d.动态组件:通过源码dsl动态渲染的widget

具体代码如下:

    const Map<String, CreateDynamicApi> allWidget = <String,  

    CreateDynamicApi>{

     'Container': wrapContainer,

        ………….

    }

    static Widget wrapContainer(Map<String, dynamic> pars) {

     return new Container(

       padding: pars['padding'],

       color: pars['color'],

       child: pars['child'],

       decoration: pars['decoration'],

       width: pars['width'],

       height: pars['height'],

       alignment: pars['alignment']

     );

    }

一般我们通过网络请求拿到的数据是一个map。 比如源码中写了这么一个 '${data.urls[1]}' AST 解析时候,拿到这么一个string,或者AST 表达式,通过解析它 ,肯定能从map 中拿到对应的值。

3.逻辑和事件

a.支持逻辑

Flutter 概念万物都是widget ,可以把表达式,逻辑封装成一个自定义widget。如果在源码里面写了if else,变量等,会加重sdk解析的过程。所以把逻辑封装到widget中。这些逻辑widget,当作组件当成框架组件。

b.支持事件

把页面跳转,弹框,等服务,注册在sdk里面。约定使用者仅限sdk 的服务。

4.规则和检测工具

a.检测规则

需要对源码的格式制定规则。比如不支持 直接写if else ,需要使用逻辑wiget组件来代替if else 语句。如果不制定规则,那ast Node 到widget node 的解析过程会很复杂。理论上都可以解析,只要解析sdk 够强大。制定规则,可以减轻sdk的解析逻辑。

b.工具检测

用工具来检测源码是否符合制定的规则,以保证所有的源码都能解析出来。

性能和效果

帧率大于50fps,体验上看比weex相同功能的页面更加流畅,Samsung galaxy s8上,感觉不出组件是通过动态渲染的。

数据结构

服务端请求到的数据,我们可以约定一种格式如下:

    class DataModel {

     Map<dynamic, dynamic>  data;

     String type;

    }

每个page 都是由组件组成的,每个组件的数据都是 DataModel来渲染。 根据type 来找到对应的模版,模版+data,渲染出界面。

动态模版管理模块

我们把Widget Node Tree 转换为一个组件Json模版,它需要一套管理平台,来支持版本控制,动态下载,升级,回滚,更新等。

03 框架的边界

该框架是通过组件的组装,组件布局动态变更,页面布局动态变更来实现动态化。所以它适合运营变化较快的首页,详情,订单,我的等页面。一些复杂的逻辑需要封装在组件里面,把组件内置到框架中,当作本地组件。框架侧重于动态组件的组装,而引擎对于源码复杂的逻辑表达式的解析是弱化的。

04 后续拓展

a.和UI自动化的结合

UI自动化 ,前面已经有文章介绍。UI自动化工具生成组件,再组件转为模版,动态下发,来快速解决运营需求。

b.国际化的支持

App在不同国家会有不同的功能,我们可以根据区域,来动态拼装我们的页面。

c.千人千面

根据不同的人群,来动态渲染不一样的界面。

05 总结

本文介绍动态化方案的渲染部分。该方案都在初探阶段,还有很多需要完善,后续会继续扩展和修改,等达到开源标准后,会考虑开源。动态方案是一个后端前端一体的方案,需要一整套工具配合,后续会有文章继续介绍整体的动态化方案。敬请关注闲鱼技术公共账号,也邀请您加入闲鱼一起探索有意思的技术。

参考资料:

Static Analysis:

https://www.dartlang.org/guides/language/analysis-options https://www.dartlang.org/tools/analyzer

dart analyzer :

https://pub.dartlang.org/packages/analyzer https://github.com/dart-lang/sdk/tree/master/pkg/analyzer_cli#dartanalyzer

dartdevc:

https://webdev.dartlang.org/tools/dartdevc


▼ 往期精彩回顾 ▼ Qcon2018·Flutter&Dart三端一体化开发

GDD2018·TensorFlow应用“UI 2 Code"

千人千面线上问题回放技术揭秘

2018双11·尖货优品实时选——“马赫”

关注二维码,前瞻技术尽在掌握