Google Flutter 是一个非常优秀的跨端框架,不仅可以运行在 Android、 iOS 平台,而且可以支持 Web 和桌面应用。在国内小程序是非常重要的技术平台,我们也一直思考能否把 Flutter 扩展到小程序端?我们团队之前已经开源了 Alita 项目,Alita 可以把 React Native 的代码转换并运行在微信小程序平台。受此启发,我们认为同样是声明式 UI 框架的 Flutter 同样可以运行在小程序平台。
所以,我们发起了 flutter_mp 开源项目。以微信小程序为例,不过现阶段,flutter_mp 项目还处于早期的实验阶段,很多功能还在探索规划中,欢迎大家在 Github 上随时关注我们的最新进展,或者参与项目共同探索。
虽然还有诸多功能未完成,我们先来谈谈整个flutter_mp的实现原理。篇幅原因,下面我们将只对flutter_mp几个重要的部分进行简单说明。
先看下 flutter_mp 的实际效果:
Flutter 版官方 layout 样例
通过 flutter_mp 转换并运行在小程序端效果
Flutter是声明式 UI 框架,声明式 UI 只需要向框架描述 UI 长什么样子而不用关心框架具体的实现细节,具体到Flutter,上层的 UI 描述使用底层的 skia 图形引擎处理就是原生Flutter,而把底层处理换成 html/css/canvas 就是flutter_web,而flutter_mp则是探索在类小程序上对这些 UI 描述的处理。
var x = 'Hello World'
Center(
child: Text(x)
);
// wxml 部分
{{x}}
// js 部分
Component({
data: {
x: 'Hello World'
}
})
收集 wxml 渲染需要的数据,放置到小程序组件的 data 字段。
我们知道小程序是无法动态操作节点的,wxml 结构需要预先生成,所以Flutter运行在小程序之前,会存在一个编译打包阶段,这个阶段会遍历 Dart 代码,根据一定规则生成 wxml 文件(编译阶段还会做下文将要提到的另外一个重要事情 --- 把 Dart 编译为 js)。
具体的,我们首先会将 Dart 源码处理为可分析的 AST 结构,AST 是源代码的树型表示结构。然后我们深度遍历这份 AST 语法树结构,生成目标 wxml,整个过程如下:
构建 wxml 结构的难点在于:Flutter 不仅是声明式 UI 还是“值 UI”,什么叫“值 UI”?简单来说,Flutter 把 UI 看成是一个普通的值,类似于字符串,数字一样的值,既然是一个普通的值,就可以参与所有的控制流程,可以是函数的返回值也可以是函数参数等等。而小程序的 wxml 虽然也是声明式 UI,却不是“值 UI”,wxml 更加像模版,更加的静态。怎么用静态的 wxml 表达动态的“值 UI”是构建 wxml 结构的关键所在。
Widget getX() {
if (condition1) {
return Text('Hello');
} else if (condition2) {
return Container(
child: ...
);
} else if (condition3) {
return Center(
child: ...
);
}
...
}
Widget x = getX();
Center(
child: x // < --- 如何处理这里的 x??
);
Hello
...
...
在遇到类似 x 这种动态值的时候,固定地会生成一个 template 占位。
在运行阶段,会根据 getX 函数的运行结果来决定 x 映射的“UI 值”,如果 getX 里面 condition1 为 true,那么这里的 templateName 的值就是 template001。具体的数据计算收集工作,参考下面的 “渲染数据收集”过程。
可以看出 flutter_mp 处理“值 UI”方式,完全参考了 Alita。
wxml 结构的生成是在编译阶段就完成了,与它不同渲染数据是运行时的信息,随时会根据 setState 而改变。那么我们怎么收集出我们需要的渲染数据呢?
如果我们还是顺着 Flutter 的架构图,很难插入我们收集的钩子函数,另外 Flutter 的这个架构对于小程序来说太重了,下图红框里的这些过程对于小程序的渲染来说并不必要。最后由于最终的代码会被转化为 js,而 Flutter 本身依赖的库里面很多是不支持转化 js 的,比如 dart:ui 等等。
所以我们实现了一个极简极简的 Flutter 小程序版本 mini_flutter,在编译期我们会把所有对 Flutter 库的引用替换为 mini_flutter, mini_flutter 只存在到上图的 Rendering 阶段,这个 Rendering 的实现也是为小程序定制的, 在运行时期 Rendering 不断收集 Widgets 的信息。最终生成一个 UI 描述的 JSON 结构,这个结构就包含了上文所说的 templateName , templateData,UI 描述将会被下层小程序获得,用来渲染小程序 UI,架构图如下:
Flutter的开发语言是 Dart,而小程序的运行环境是浏览器,所以我们还需要把 Dart 编译为 JavaScript 代码。
在上文的编译打包阶段也提到这一点,这个过程主要是使用了 Dart 提供的 dart2js 工具,不过,针对小程序环境,生成的 js 代码仍需要做一些适配,另外虽然都是 JS 代码,dart2js 生成的 js 和小程序原生 js 的运行环境却是隔离的,也就是说它们是不能共享变量,方法等等,它们各自在本身的"域"里执行。
Widget 初始化 或者 setState 更新,生成的 UI 描述 JSON,如何传递给小程序"域"呢?
相关渲染回调,事件的都发生在小程序"域",这些信息如何传递给 Dart?
总结一下:Dart(最终会编译为 JS)与小程序原生 JS 如何互操作?
解决这个问题主要是借助 dart:js, package:js 这两个库:
这样当 Dart 代码调用 stringify 方法的时候,实际上会执行window.JSON.stringify
方法。
// js 调用
window.dartHi()
这里只是简单说明 Dart 与 JS 的互操作,另外由于小程序的运行环境是阉割以后的浏览器环境,flutter_mp的实现还稍有不同。
总之,Dart 与 JS 是可以互操作的,这样就打通了上层Flutter环境和下层小程序环境。
Flutter的布局系统不同与 css,但是和 css 颇相似。
在上文提到的 Rendering 阶段,会根据 Widget 的布局属性、类别、约束条件生成一个等效的 css 样式。注意,这里边界约束是上下文相关的。比如一个没有宽高的 Container 实际大小,不仅和子元素相关,还和父元素传递过来的边界约束条件相关,这个其实是比较麻烦的,能不能把 Flutter 的 Widget 属性,边界约束完全用 css 表达,我们还在寻求有效的方案。
和flutter_web一样,完全把Flutter所有特性渲染到小程序上是不可能的,一般我们觉得应该是部分页面,部分功能需要运行在小程序上,这样使用flutter_mp才是有意义的。
正如前文所说,flutter_mp还在很早期的阶段,社区的支持和反馈对我们来说特别宝贵。同时欢迎广大开发者一起来维护flutter_mp。
flutter_mp: https://github.com/areslabs/flutter_mp
如果你需要在生产环境实现小程序跨端开发,推荐使用我们成熟的 RN 转小程序项目 Alita。
Alita:https://github.com/areslabs/alita
开发者技术前线 ,汇集技术前线快讯和关注行业趋势,大厂干货,是开发者经历和成长的优秀指南。