注册

在微信小程序里运行完整的 Flutter,我们是怎么做到的?

背景


小程序是一种全新的业务形态,特别是微信小程序,既结合了 Web 动态化特性,又拥有 Native 丰富的设备能力支持。


在微信这个宿主上,小程序不仅有稳定的分发渠道,更拥有完善的生命周期、数据、AI 能力支持。


在该微信上开发小程序,一般使用以下两种方法:



  • JavaScript + WXML + WCSS
  • Taro + React + JavaScript

本文要介绍的是使用 Flutter Framework 开发小程序的方法,以及该方法背后的技术原理。


技术挑战


尽管 Flutter 官方已经提供 Flutter Web 实现,Flutter Web 本身就是基于 dart2js 运行的,微信小程序可以运行 JavaScript,在原理上跑起 Flutter Web 是没有问题的。


但仍然存在以下技术挑战:



  • 微信小程序没有 W3C 标准的 JavaScript 对象,Flutter Web 不能直接运行。
  • 微信小程序也没有 DOM 实现,Flutter Web HTML Renderer 不能直接渲染。
  • 微信小程序对包大小的限制十分严格,主包不能超过 2M,而 Flutter Web 所编译的 main.dart.js 初始体积就有 1.3 M,必须有合理的分包机制才能上传。

我们在 MPFlutter 1.x 版本中,针对上述问题已有一定的探索,1.x 版本的解决方法如下:



  • 使用微信开源的 kbone 库,模拟 W3C 实现,并通过模拟的 DOM 对象渲染出符合 WXML 要求的视图树。
  • 通过 Shadow Element Tree 的方式,使用 JSON 在 Dart 与 JavaScript 上下文同步视图树。
  • Fork Flutter Framework,并对其进行外科手术式的裁剪,使 main.dart.js 初始体积降低到 600K。

MPFlutter 1.x 方案已经良好的运行了两年,也收到了开发者非常多的反馈,开发者常诟病于裁剪后的 Flutter Framework 不兼容 Flutter 生态上的插件,同时 material 库也无法使用,需要从头开始编写 UI。


在 MPFlutter 2.0 版本,我们重新思考在小程序上运行 Flutter 的最佳方式,并在最终使用 CanvasKit Renderer 解决以上全部问题。


技术方案


Summary


通过裁剪 Skia 生成符合微信小程序分包要求的 CanvasKit,使用 Flutter Web + W3C BOM + WebGL Canvas 跑通渲染流程。


技术选型


在介绍技术选型前,需要先介绍 Flutter Web 的两种 Renderer。


HTML Renderer


原理是 Flutter Framework 通过 dart:js 库调用 Document 对象,并基于此将各种 RenderObject 转换为对应的 Element + CSS 添加到 DOM 树中。


该方案优点在于兼容性很好,几乎没有额外的依赖;缺点是性能不佳,并且渲染内容一致性难以与 Native Flutter 对齐。


CanvasKit Renderer


原理是通过 WebGL + Skia 渲染界面,该渲染方式与 Native Flutter 是完全一致的。


该方案优点在于渲染性能非常好,一致性与 Native Flutter 几乎没有差别;缺点是内存占用大,且需要从远端加载字体。


MPFlutter 2.0 选型


我们在 1.x 版本中用的是 HTML Renderer,通过 kbone 运行的 DOM 模拟层存在很多的问题,最令人诟病的是数据更新后界面刷新慢。当然问题的并不在于 kbone,而是 MPFlutter 1.x 本身对于 Element Tree 的序列化、反序列化的处理存在天然的缺陷,尽管已经通过 Dirty 和 Diff 等手段优化。


在 2.x 版本中,我们直接抛弃 HTML Renderer 的想法,使用 CanvasKit Renderer。


使用 CanvasKit Renderer 有这几个大前提:



  • 微信小程序已支持 WebAssembly 并支持 Brotli 压缩;
  • 微信小程序 Canvas 的性能相比最初的版本有质的提升,并支持 WebGL;
  • 微信小程序全部分包限制放宽到 20M,足够使用。

Skia 裁剪


Skia 是 Google 开源的 2D 渲染库,凭借良好的跨设备能力,优秀的性能表现,在 Google 多个产品中被使用,包括 Chrome / Flutter / Android / Fuchsia 都有 Skia 的身影。


Skia 屏蔽了不同设备、平台的具体实现,对外统一以标准的 RenderObject、RenderCommand 开放。


Skia 其中一个 Render Target 是 WebGL,也就是 CanvasKit。


然而 Flutter Web 默认使用的 CanvasKit 足有 6M 之大,即使使用 Brotli 压缩后仍然不符合小程序分包要求。


我们可以通过指定编译选项的方式裁剪 CanvasKit 尺寸,以下是 MPFlutter 使用的 build 配置:


./modules/canvaskit/compile.sh release no_skottie no_sksl_trace no_alias_font no_effects_deserialization no_encode_jpeg no_encode_png no_encode_webp legacy_draw_vertices no_embedded_font no_woff2

从配置可见,我们去掉了 skottie、image encoder、内置字体等不必要的功能,这些功能我们可以使用微信小程序 API 补充回来。


Brotli 压缩后的 wasm 文件刚好符合 2M 分包要求。


CanvasKit 加载


Skia 构建完成后,会得到两个产物,canvaskit.wasmcanvaskit.js


canvaskit.js 暴露了 wasm 中的各个 c++ 方法调用,同时也提供加载 wasm 的脚手架。


但是 canvaskit.js 的实现默认是 Web 的,我们需要将其中的 fetch 以及 WebAssembly 替换为微信小程序对应的实现。


这里提供一个使用 Skia 绘制红色矩形的微信小程序工程,有兴趣的同学可以下载到本地研究。


mpflutter.feishu.cn/wiki/LWhrw3…


Flutter Web 在微信中运行


要使 Flutter Web 在微信中运行,最大难点在于 Flutter Web 要求的 Web API 如何补充完整。


特别是 Document 、Window、Navigator 这些类,这些类我已经在 GitHub 上开源了,感兴趣的可以逐个文件阅读。


github.com/mpflutter/m…


这里举一个 window 的文件节选段落讲解:


export class FlutterMiniProgramMockWindow {
// screens
get devicePixelRatio() {
return wxSystemInfo.pixelRatio;
}

get innerWidth() {
return wxSystemInfo.windowWidth;
}

get innerHeight() {
return wxSystemInfo.windowHeight;
}

// webs
navigator = {
appVersion: "",
platform: "",
userAgent: "",
vendor: "",
language: "zh",
};

// 还有更多。。。
}

Flutter Web 在运行过程中,会通过 window.innerWidth / window.innerHeight 获取当前窗口宽高,以便下一步创建合适大小的画布用于渲染。


在微信小程序中,我们需要使用 wx.getSystemInfoSync() 获取对应宽高,并在 MockWindow 中返回给 Flutter。


关于 BOM 的文件,就不详细展开,都是一些胶水代码。


而 Flutter 的 main.dart.js 也需要有一些改造才可以跑在小程序上,主要的改造是通过 export main.dart.js 中的 main 函数,使其适配 CommonJS 可暴露给 Page 调用。


字体的加载


CanvasKit 最大的问题在于字体加载,目前来看是无法复用系统本身的字体的。


我们的做法是通过裁剪 NotoSansSC 字体,只包含常用的 9000+ 汉字,内置于小程序包中优先加载它。


这样有一个好处,小程序不需要强制从 gstatic 下载字体,省流省加载时间。


后续,我们还会研究通过 Canvas 2D 的方式,从本地加载字体。


分包


关于分包,其实是最好做的,因为 Flutter Web 本身就有 defered load 编译能力。


开发者可以轻松地将 main.dart.js 切分成若干个 JS 文件,我们做的就是在 Flutter Web 编译完成后,智能地将这些 JS 文件分配到不同的分包就好了。


资源分包也同理,资源通过 brotli 压缩也可以减少包体积。


总结


整整一套下来,Flutter 已经可以在微信小程序里跑起来了,我们来总结一下做了什么?


我们通过裁剪 Skia 使得 CanvasKit 可以很好地跑在小程序上,通过 BOM 兼容的方法,使得 Flutter Web 可以在微信小程序中找到对应实现,通过字体内置、智能分包的方式很好地解决了微信包体积限制。


该方案目前已经完全跑通,并已可用,同学们可以在 v2.mpflutter.com 文档站了解到更多用法。


如果对方案有任何疑问,也欢迎添加微信交流,感谢大家的关注。


作者:PonyCui
来源:juejin.cn/post/7324923422295670834

0 个评论

要回复文章请先登录注册