共计 6099 个字符,预计需要花费 16 分钟才能阅读完成。
本文的核心内容包含:
- 数据逻辑解决
- 布局中的逻辑解决
- Flutter 类型数据处理
一、数据逻辑解决
咱们接触的每一个 Flutter 界面,大多由布局和逻辑相干的代码组成。如 Flutter 初始工程的 Counting Demo 的代码:
class _MyHomePageState extends State<MyHomePage> { | |
// 变量 | |
int _counter = 0; | |
// 办法 | |
void _incrementCounter() {setState(() {_counter++;}); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar(title: Text(widget.title), // 内部参数 | |
), | |
body: Center( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: <Widget>[ | |
Text('You have pushed the button this many times:',), | |
Text( | |
'$_counter', // 变量应用 | |
style: Theme.of(context).textTheme.headline4, | |
), | |
], | |
), | |
), | |
floatingActionButton: FloatingActionButton( | |
onPressed: _incrementCounter, // 事件回调 | |
tooltip: 'Increment', | |
child: Icon(Icons.add), | |
), | |
); | |
} | |
} |
如上,正文标记的就是页面中的数据以及数据处理逻辑局部。Fair 的逻辑动态化是要实现这部分变量、办法等内容的脚本级动静解析以及 DSL 布局属性的绑定和后续用户操作事件办法的调用。
如何把 Dart 文件中的逻辑,变成可动静运行的变量和相干的操作呢?咱们在我的项目初期也进行了若干计划的探讨与 Demo 开发尝试。例如间接下发 Dart 文件,在纯 Dart 侧实现布局和逻辑的解析与数据绑定,此操作相似于要开发一个 Dart Script 引擎,开发成本较高。咱们最终确定:研发 Dart2JS 工具,把界面 Dart 文件的逻辑局部转换成 JS 脚本,而后利用原生的 JSCore 实现逻辑脚本的动静运行,由此咱们最终的构造如下。
咱们实现了,把一个 @FairPatch 注解标注过的界面文件(Widget),通过 Fair-Compiler 工具编译后生成 DSL 布局文件和 JS 逻辑文件。为什么咱们须要应用 FairPatch 注解,标注后生成?是因为咱们整体的设计理念是 Flutter 原生界面和动静界面能够应用一个源文件来转换,动态化只是在紧急需要或者需要不确定的 A / B 测等长期场景应用,需要稳固之后,积淀下来的源文件能够持续跟版应用,最大化的放弃 Flutter 的性能。
Widget 转换出的动静脚本,如下图所示。具体的原理请看“Fair 下发产物–DSL 生成原理与实现”和“Fair 下发产物–Dart2JS 生成原理与实现”。
在产物生成过程中,为了反对逻辑动态化,咱们须要转译的原始 Dart 语法减少了很多。我拿 Demo 例子中的_counter 变量举例,它是一个根底 int 类型。应用在如下代码中:
Text('$_counter')
尽管看起来是语句简短,然而在生成 Text Widget 时,数据绑定的阶段须要做粗疏的辨别。如下是 num 转 string 再做参数传递的场景:
"className": "Text", | |
"pa": ["#($_counter)" | |
], |
逻辑动态化,最外围的难点就是如何解决变量。Flutter 中的变量,能够是数据变量也能够是办法变量。依据 Demo 中的场景,我列举一下(咱们依据数据绑定的须要,雷同解决形式的变量表达式咱们做了合并):
字符串强转根底变量
Text('$_counter') => #($_counter)
widget 传参变量
Text(widget.title) => #(widget.title)
办法变量(回调执行)
onPressed: _incrementCounter => @(incrementCounter)
这些 Flutter 代码中的变量和逻辑办法,通过 Fair 编译工具的转译变成了在 DSL 文件中的标识,变量和办法逻辑最终都会在 JS 引擎中具体实现。当然在从布局 DSL 生成对应界面的时候,须要咱们实现数据绑定的模块。数据绑定过程就是依据下面提到的 #()、@() 等不同表达式,实现 Flutter 侧和 JS 侧的变量解决需要,最终实现指标 Widget 的生成。
1.1 Dart 和 JS 双域变量映射
只从 Dart 侧去发动所需的变量值查问和对变量解决的办法调用,JS 侧根本只作为数据值存储和数据逻辑相干的操作,工作示意图如下。
Fair 中的 MVVM 依赖于 Flutter 原生模式,如上图所示,JS 域的数据同步给 Dart 域,只须要在 JS 侧调用相熟的 setState 即可。当然这部分对应用 Fair 框架的开发者是无感知的,编译工具帮咱们实现了相干的转换。原生代码和生成的 JS 代码,比照如下:
JS:_incrementCounter: function _incrementCounter() { | |
const __thiz__ = this; | |
with (__thiz__) {setState('#FairKey#', function dummy() {_counter++;}); | |
} | |
}, |
Dart:void _incrementCounter() {setState(() {_counter++;}); | |
} |
能够看到,除了 JS 简化拜访域的 with 和通信指标对象须要的 FairKey,其余代码差异并不大。
1.2 生命周期
下面介绍了,双域的变量值如何映射。可能有同学就会问,Dart 和 JS 的生命周期是怎么同步的呢?(Fair 提供的是 StatefulWidget 载体,所以此处只介绍 StatefulWidget 生命周期和 JS 生命周期的对应关系)
如上图所示,Fair 提供给 JS 域 2 个感知的生命周期的回调。onLoad 是在 JS 加载实现,告诉 JS 侧进行变量初始化;onUnload 是页面隐没,告诉 JS 侧进行资源回收。
1.3 最小渲染
Fair 中对 Widget 属性的值变量都会生成对应的 ValueNotifier 对象,来实现部分的组件渲染。计算型变量,例如办法参数,咱们不做非凡解决。参加 Widget 层级治理的变量,咱们须要通过 setState() 来实现 Reload 全布局。
Sugar.ifEqualBool(_countCanMod2(), // 管制 Widgte 布局的 | |
trueValue: Image.asset('assets/image/logo.png'), | |
falseValue: Image.asset('assets/image/logo2.png')), |
到这里咱们能够整体回顾一下,Fair 架构的工作流,如下图所示:
阐明如下:
- Dart 界面源文件通过 Fair 编译工具转化成布局 DSL 和逻辑 JS 文件
- JS 逻辑文件加载到 JSCore,并实现 JS 域的初始化
- DSL 布局文件通过解析模块,生产对应的界面 Widget
- 在 Widget 的生成过程中,属性、事件等注册;值拜访等须要依赖 5 的通信管道
- 通过 Flutter 和 JS 模块的通信,实现 JS 域的值读取和办法解决
二、布局中的逻辑解决
2.1 布局中的逻辑解决
Fair 把在布局中罕用的逻辑,进行了语法糖的封装。为什么采纳语法糖的形式,次要是心愿缩小 Fair Compiler 工具的转换老本。开发者也能够依据本人的须要进行扩大。举个例子,有如下一段逻辑:
condition == true ? widget1 : widget0
采纳语法糖封装后,(判断_count 是否为 2),如下:
Sugar.ifEqual(_count,2, | |
trueValue:Image.asset('assets/image/logo.png'), | |
falseValue: Image.asset('assets/image/logo2.png')), |
此外,Fair 布局中还反对 ifRang、map、mapEach 等语法糖。语法糖设计与实现,详见“Fair 逻辑语法糖设计与实现”。
2.2 布局中的子办法解决
在我的项目中,build 内的布局代码往往会十分多,大部分开发者都会拆分成小的布局子办法。例如:
// 定义布局子办法 | |
Widget _titleWidget() {return Text(getTitle()); | |
} | |
// 组合布局 | |
... | |
appBar: AppBar(title: _titleWidget(), | |
) | |
... |
下面的布局通过 DSL 转化后,失去的转化 DSL JSON 构造如下:
{ | |
"className": "Scaffold", | |
"na": { | |
"appBar": { | |
"className": "AppBar", | |
"na": {"title": "%(_titleWidget)" | |
} | |
}, | |
"body": {...}, | |
}, | |
"methodMap": {"createState": "%(_State)", | |
"_titleWidget": { | |
"className": "Text", | |
"pa": ["%(getTitle)" | |
] | |
} | |
} | |
} |
通过子布局办法在 methodMap 注册,在构建时实现整体拼装,来满足开发者对布局拆封的需要。到这里大家也会发现,Fair 的设计是尽可能的把逻辑解决在 Dart 侧实现,缩小 Flutter 和 JS 的通信次数,进步整体动态化的性能。
三、Flutter 类型变量的解决
可能大家会问,Fair 是把 Flutter 外面的数据类型在 JS 侧都实现了一遍吗?答案是否定的,在 JS 侧咱们尽管筹备了一些 Dart 根底类和 JS 根底类的对应类型,然而并没有齐全笼罩。那开发者遇到 Flutter 内置类型,比方像 ScrollController,咱们怎么解决呢?
Fair 提供了原生模版加界面动静的模式,提供给开发者扩大。咱们用加载更多列表 Demo 举例,原生模版如下:
class ListDelegate extends FairDelegate { | |
ScrollController _scrollController; | |
@override | |
Map<String, Function> bindFunction() {var functions = super.bindFunction(); | |
functions.addAll({ | |
'_itemBuilder': _itemBuilder, | |
'_onRefresh': _onRefresh, | |
}); | |
return functions; | |
} | |
@override | |
Map<String, PropertyValue> bindValue() {var pros = super.bindValue(); | |
pros.addAll({'_scrollController': () => _scrollController, | |
}); | |
return pros; | |
} | |
@override | |
void initState() { | |
// 监听滑动 | |
_scrollController = ScrollController() | |
..addListener(() { | |
// 判断是否滑到底 | |
if (_scrollController.position.pixels == | |
_scrollController.position.maxScrollExtent) {_onLoadMore(); | |
} | |
}); | |
} | |
@override | |
void dispose() {super.dispose(); | |
_scrollController.dispose();} | |
void _onLoadMore() {runtime?.invokeMethod(pageName, '_onLoadMore', null); | |
} | |
Future<void> _onRefresh() async {await runtime?.invokeMethod(pageName, '_onRefresh', null); | |
} | |
// 构建 item 动静界面 | |
Widget _itemBuilder(context, index) {var result = runtime?.invokeMethodSync(pageName, '_onItemByIndex', [index]); | |
var value = jsonDecode(result); | |
var itemData = value['result']; | |
return FairWidget(name: itemData['name'], | |
path: itemData['path'], | |
data: {'fairProps': jsonEncode({'item': itemData['props']})}, | |
); | |
} | |
} |
这样设计的益处是,当须要不停的监听_scrollController.position 的地位变动时,如果这个过程设计成 Flutter 不停的往 JS 侧发送地位偏移值,由 JS 侧实现阀值判断的话,对 bridge 的压力会十分大,性能也就十分的低。
通过原生模版,再配合模版中应用的 JS 通信协议,能够齐全自定义 JS 侧的逻辑,此例的对应的 JS 如下:
GLOBAL['#FairKey#'] = (function (__initProps__) { | |
const __global__ = this; | |
return {list: List(), | |
_scrollController: null, | |
listIsEmpty: function listIsEmpty() { | |
const __thiz__ = this; | |
with (__thiz__) {return list == null || list.isEmpty;} | |
}, _onLoadMore: async function onLoadMore() {// 上拉加载更多解决}, _onRefresh: async function _onRefresh() {// 下拉加载更多解决}, onLoad: function onLoad() {// 界面逻辑数据初始化}, onUnload: function onUnload() {// 界面逻辑数据开释}, _itemCount: function _itemCount() {// 下拉加载更多解决}, _onItemByIndex: function _onItemByIndex(index) { | |
// 获取单个列表卡片数据 | |
const __thiz__ = this; | |
with (__thiz__) {return list[index]; | |
} | |
} | |
}; | |
})(convertObjectLiteralToSetOrMap(JSON.parse('#FairProps#'))); |
如上代码中的 onItemByIndex、onRefresh、onLoadMore 等办法名,只须要跟 Dart 侧的 Delegate 模版调用 一一对应起来就好。在模版中实现反对用户的自定义行为。
总结一下,Fair 逻辑动态化的设计和实现,对 Fair 如何实现 Flutter 逻辑动态化曾经有了肯定的理解。咱们从纯数据逻辑、布局中的逻辑、以及最初的模版 + 视图,来残缺的实现了逻辑动态化的反对。