Fair 逻辑语法糖设计与实现

679 阅读6分钟

语法糖(英语: 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中整个逻辑处理单元,如下图所示:

1629600918721.jpg

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

本文重点介绍:

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

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

Fair语法糖支持现状

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

“双态“一致性

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

1629602514752.jpg

原生态

在原生态场景下,我们只需要把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));
    }
}

动态

在动态场景下,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编译工具语法支持和开发者易用性方面的权衡的产物。语法糖会改变混编场景下的开发语法,随着解析工具能力和构建时数据处理能力的增强,这部分也会随之弱化。