继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Fair 逻辑语法糖设计与实现

Fair技术团队
关注TA
已关注
手记 10
粉丝 4
获赞 4

原创 王海君 58技术 2021-09-14 08:47

● 项目名称:Fair 2.0

● Github地址:https://github.com/wuba/fair

● 项目简介:Fair是为Flutter设计的动态化框架,可以通过Fair Compiler工具对Dart源文件的转化,使项目获得动态更新Widget的能力。Fair 2.0是为了解决 Fair 1.0版本的“逻辑动态化”能力不足。

语法糖(英语:Syntactic Sugar )是指 计算机语言 中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。Fair 语法糖 并不是为Dart语法提供易用性的接口,而是为了让Fair在布局和逻辑混编场景下构建更方便。

布局和逻辑混编场景下的思

如果需要转译如下的代码:

 Widget getTitleWidget() {   
  if (_count == 1) {      
      return Text(        'Title 1', 
             style: TextStyle(color: Colors.white), 
                  ); 
   } else {
         return Text(
                 'Title Other',
                         style: TextStyle(color: Colors.red),
                    );    
          }
  }

我们需要怎么处理上面的代码呢?可行的方案应该有:

  1. 把上面的代码归属到逻辑模块,放到JS侧处理?

  2. 观察到此方法返回的是一个Widget,方法内部大部分都是布局相关,属于逻辑的只需要处理一个判断。是不是可以放到Dart侧 构建Widget Tree的时,做一下条件判断处理也可以呢?

方案1:

需要完整提供JS和Dart Widget的映射和JS侧的构建Widget的能力,这就变成了类似MXFlutter和Kraken的方案,区别只是他们需要开发者自己手写,Fair可以工具生成。

方案2:

需要告知布局解析引擎,构建时使用哪个分支。固定格式的判断、循环等,也都可以在Dart侧处理,相关的逻辑呈现就需要在DSL文件内。
经过思考,我们决定采用第2种方案,来解决布局和逻辑混编的场景问题,以及在Dart侧完成布局子方法的调用拼接。

方案3:

如果你有更好的设计,请联系我们。最终Fair中整个逻辑处理单元,如下图所示:

http://img1.sycdn.imooc.com/62d517800001c56510800569.jpg

Fair逻辑处理单元中,语法糖支持布局中常用的分支和循环。Fair的设计区别于kraken和mxflutter这2种动态化方案,Fair的一个愿景是让同一份代码在Flutter原生和动态之间随意切换。在开发跟版本需求时,我们使用原生代码发布,以此持续保持Flutter的性能优势;而在热更新场景可以通过下发动态文件来达到动态更新Widget的目的。所以Fair在设计语法糖是需要考虑Flutter原生和动态2个场景下都可以正常运行。

本文重点介绍:

  • 如何做到“双态“结果一致性

  • 动态下的数据绑定和逻辑处理

  • Fair语法糖支持现状

(后续我们以Fair支持的List Map逻辑为例进行介绍, Fair example中语法糖内容也有示例)

2 “双态”一致性

由于Flutter原声态和动态的载体不一样,需要做“双态”一致性处理,确保2个状态下运行的结果一致,而且需要提供给开发者一致的编码接口。如下图所示,增加语法糖之后的运行流程:

http://img4.sycdn.imooc.com/62d519740001249910800551.jpg

2.1 原生态

在原生态场景下,我们只需要把List Map方法定义成一个静态方法,方法内支持目标语法的转换即可,这部分比较容易实现。例如我们把语法糖方法统一定义到Sugar类中,代码如下:

class Sugar {
    static List<T> map<T, E>(List<E> data, {T Function(E item) builder}) {
        return data.mapEach((index, item) => builder(item))
   }
}

2.2 动态

在动态场景下,Fair框架初始化时,会完成全局的语法糖模块的注册,在FairWidget载体运行时可以动态访问这些语法糖,再经过第2小节会介绍的数据绑定和逻辑处理,最终生成目标布局。接下来我们介绍一下语法糖的注册和动态访问。

语法糖方法注册

FairApp({  ....})
  : placeholderBuilder = placeholder ?? _defaultHolder,
        super(key: key, child: child) {
          // 完成全局的模块的注册,其中就包括语法糖模块
            setup(profile, delegate, generated, modules);
           }
// 语法糖动态注册
FairWidgetBinding provider = () {
  return {
      'Sugar.map': (props) {
            var items = pa1(props);
                  assert(items is List, 'failed to generate list of Sugar.map');
                        return ((items as List).asIteratorOf<Widget>() ?? items).toList();
                            },
         };

如上代码中所示,setup是注册全局语法糖的入口;provider是提供的语法糖集,更多的语法糖方法可以在provider内定义。

语法糖方法访问

如何把List Item内容,展示到列表中的每个Cell中呢?下面我们给一个显示list数字的例子:

Dart侧,如下:

class _State extends State<MapPage> {
  var _list = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 100];
    ...
      @override  Widget build(BuildContext context) {
          return Scaffold(
                  body: ListView(
                            children: Sugar.map(_list, builder: (item) {
                                        return Container(
                                                      child: Row(...),
                                                       );
          }),
        ));
   }}

转换后的DSL代码,如下:

{
    ...
    "body": {
      "className": "ListView",      
      "na": {
              "children": {
                        "className": "Sugar.map", // 语法糖名称          
                        "pa": [
                                    "^(_list)" // 目标List          
                              ],
                        "na": { 
                          "builder": { // 每一个list item展示的卡片布局
                                        "className": "Container",
                                         "na": {
                                         "child": { 
                                         "className": "Row",
                                             "na": {
                                                "children": [
                                                                 {
                                                                          "className": "Expanded",
                                                                          "na": {
                                                                           "child": {
                                                                                     "className": "Container",
                                                                                      "na": {
                                                                                        "child": {
                                                                                          "className": "Row",
                                                                                          "na": {
                                                                                             "children": [...]
                                                                                             }
                                                                                           }              
                                                                                       }
                                                                                     } 
                                                                                   }  
                                                                   }
                                                     }    
                                                  }  
                                                },  
                             "methodMap": {}
                             }

当构建目标布局时,只需要从pa中取出list数据,迭代构建builder内布局DSL就好。

动态场景下的数据绑定和逻辑处理

在对语法糖模块进行设计时,我们只从JS域访问原始的数据,然后整个布局组合相关的工作都放在Flutter域中完成。List Map的循环迭代逻辑,跟Sugar.ifEqual这样的只需构建一次布局相比,Map、MapEach这样的循环逻辑,需要在布局解析时特殊处理。

如下解析时,处理迭代功能:

List<Widget> _buildSugarMap(
   Function mapEach, Map map, Map methodMap, BuildContext context) {
 var source = pa0(map);  var children = [];  // 根据脚本取出List在JS域的数据
 if (source is String) {
   var r = proxyMirror.evaluate(context, bound, source);
   if (r.data != null) {
     source = r.data;
   }
 }
 if (!(source is List)) {
   throw Exception('Sugar.mapEach has no valid source array');
 }
 //根据List 内容实现迭代解析
 if (source != null && source is List) {
   // Domain 内完成目标数据的内容替换
   children = Domain(source).forEach(($, _) {
     return convert(context, map['na']['builder'], methodMap, domain: $);
   });
 }
 // 组装成整体目标布局
 var params = {
   'pa': [source, children]
 };
 return mapEach.call(params);
}

根据如上的代码以及注释内容,开发者只需要使用 Sugar.map(_list, builder: (item) {}) 语法糖就可以完成List内容和目标Item数据的绑定。但是目前Map和MapEach语法糖只能简单的处理固定名字的例如item和index的变量名称和内容,后续我们会跟整体一次处理的布局数据绑定逻辑处理保持一致。

Fair语法糖支持现状

目前Fair 框架默认内置了if、ifRange、Map等5个语法糖,后续会根据需要逐步扩展。当然开发者也可以提交常用的布局逻辑来扩展Fair语法糖集。

到这里关于Fair框架语法糖相关的内容就介绍完了。其实Fair语法糖模块的出现,归根到底就是Fair编译工具语法支持和开发者易用性方面的权衡的产物。语法糖会改变混编场景下的开发语法,随着解析工具能力和构建时数据处理能力的增强,这部分也会随之弱化。

谢谢大家!

交个朋友,帮我们点个star吧 🌟  😇:Github地址:https://github.com/wuba/fair


                


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP