Flutter Web : 一个编译问题带你了解 Flutter Web 的打包构建和分包实现

Flutter Web 作为 Flutter 框架中最特殊的平台,由于 Web 平台的特殊性,它默认就具备了两种不同的渲染引擎:

  • html : 通过平台的 canvas 和 Element 完成布局绘制;
  • canvaskit : 通过 Webassembly + Skia 绘制控件;

虽然都知道 canvavskit 更接近 Flutter 的设计理念,但是由于它构建的 wasm 文件大小和字体加载等问题带来的成本考虑,业界一般会选用更轻量化的 html 引擎,而今天的问题也是基于 html 引擎来展开。

本篇算是目前少有关于 deferred-components 和 Flutter Web 构建过程分析的文章

一、deferred-components

我们都知道 Flutter Web 打包构建后的 main.dart.js 文件会很大,所以**一般都会采用一些方法来对包大小进行优化,而其中最常用的方式之一就是使用 deferred-components **。

对于 deferred-components 官方起初主要是用于支持 Android App Bundle 上的动态发布,而经过适配后这项能力被很好地拓展到了 Web 上,通过 deferred-components 可以方便地根据需求来拆分 main.dart.js 文件的大小。

当然这里并不是介绍如何使用 deferred-components ,而是在使用 deferred-components 时,遇到了一个关于 Flutter Web 在打包构建上的神奇问题

首先,代码如下图所示,可以看到,这里主要是通过 deferred as 关键字将一个普通页面变成 deferred-components ,然后在路由打开时通过 libraryFuture 加载后渲染页面

image-20220325173721875

这里省略了无关的 yaml 文件代码,那么上述简略的代码,大家觉得有没有什么问题

一开始我也觉得没什么问题, 通过 flutter run -d chrome --web-renderer html 运行到浏览器调试也没问题,页面都可以正常加载打开,但是当我通过 flutter build web --release --web-renderer html 打包部署到服务器后,打开时却遇到了这个问题

Deferred library scroll_listener_demo_page was not loaded.
main.dart.js:16911     at Object.d (http://localhost:64553/main.dart.js:3532:3)
main.dart.js:16911     at Object.aL (http://localhost:64553/main.dart.js:3690:34)
main.dart.js:16911     at asV.$1 (http://localhost:64553/main.dart.js:54352:3)
main.dart.js:16911     at pB.BE (http://localhost:64553/main.dart.js:36580:23)
main.dart.js:16911     at akx.$1 (http://localhost:64553/main.dart.js:51891:10)
main.dart.js:16911     at eT.t (http://localhost:64553/main.dart.js:47281:22)
main.dart.js:16911     at Cw.bp (http://localhost:64553/main.dart.js:48714:51)
main.dart.js:16911     at Cw.ih (http://localhost:64553/main.dart.js:48691:9)
main.dart.js:16911     at Cw.rz (http://localhost:64553/main.dart.js:48659:6)
main.dart.js:16911     at Cw.zk (http://localhost:64553/main.dart.js:48689:11)

这就很奇怪了,明明 debug 运行时没有问题,为什么 release 发布就会 not loaded 了?

经过简单调试和打印发现,在出错时代码时根本进入不到 ContainerAsyncRouterPage 这个容器里,也就是在外部就出现了 not loaded异常,但是明明 widget 是在 ContainerAsyncRouterPage 容器内才调用,为什么会在外部就抛出 not loaded 的异常?

通过异常信息比对源码发现,编译时在对于 deferred as 进行处理时,会插入一段 checkDeferredIsLoaded 的检查逻辑,所以抛出异常的代码是在编译期时处理 import * deferred as 时添加

image-20220325231047005

通过查看打包后的文件,可以看到如果在 checkDeferredIsLoaded 之前没有完成加载,也就是对应 importPrefix 没有被添加到 set 里,就会抛出异常。

image-20220325214838143

所以初步推断,问题应该是出现在 debug 和 release 时,对于 import * deferred as 的编译处理有不同之处。

二、构建区别

通过资料可以发现,Flutter Web 在不同编译期间会使用 dartdevcdart2js 两个不同的编译器,而如下图所示,默认 debug 运行到 chrome 时采用的是 dartdevc ,因为 dartdevc 支持增量编译,所以可以很方便用 hot reload 来调试,通过这种方式运行的 Flutter Web 并不会在 build 目录下生成 web 目录,而是会在 build 目录下生成一个临时的 *.cache.dill.track.dill 用于加载和更新。

image-20220325165759471

.dill 属于 Flutter 编译过程的中间文件,该文件一般是二进制的编码,如果想要查看它的内容,可以在完整版 dart-sdk/Users/xxxxx/workspace/dart-sdk/pkg/vm/bin 目录下)执行 dart dump_kernel.dart xxx.dill output.dill.txt 查看,注意是完整版 dart-sdk 。

而 Flutter Web 在 release 编译时,如下图所示,会经过 flutter_toolsweb.dart 内的对应配置逻辑进行打包,使用的是 dart2js 的命令,打包后会在 build 下生成包含 main.dart.js 等产物的 web目录,而打包过程中的产物,例如 app.dill 则是存在 .dart_tool/flutter_build/一串特别编码/ 目录下。

image-20220325164442683

.dart_tool/flutter_build/ 目录下根据编译平台会输出不同的编译过程目录,点开可以看到是带 armeabi-v7a 之类的一般是 Android 、带有 *.framework 的一般是 iOS ,带有 main.dart.js 的一般是 Web 。

而打开 web.dart 文件可以看到很多可配置参数,其中关键的比如:

  • --no-source-maps : 是否需要生成 source-maps ;
  • -O4 :代表着优化等级,默认就是 -O4,dart2js 支持 O0-O4,其中 0 表示不做任何优化,4 表示优化开到最大;
  • --no-minify : 表示是否混淆压缩 js 代码,默认build web --profile 就可以关闭混淆;
image-20220325180245530

所以到这里,我初步怀疑是不是优化等级 -O4 带来的问题,但是正常情况下,Flutter 打包时的 flutter_tools 并不是使用源码路径,而是使用以下两个文件:

/Users/xxxx/workspace/flutter/bin/cache/flutter_tools.stamp

/Users/xxxx/workspace/flutter/bin/cache/flutter_tools.snapshot

难道就为了改个参数就去编译整个 engine ?这样肯定是不值得的,所幸的是官方提供了使用源码 flutter_tools 编译的方式,同样是在项目目录下,通过一下方式就可以用 flutter_tools 源码的形式进行编译:

dart ~/workspace/flutter/packages/flutter_tools/bin/flutter_tools.dart build web --release --web-renderer html

而在源码里直接将 -O4 调整了 -O0 之后,我发现编译后的 web 居然无法正常运行,但是基于编译后的产物,我可以直接比对它们的差异,如下图所示,左边是 O0,右边是O4:

image-20220325163734572
image-20220325164259841

-O0 之后为什么会无法运行有谁知道吗?

首先可以看到, O4 确实做了不少优化从而精简了它们的体积,但是在关键的 loadDeferredLibrary 部分基本一样,所以问题并不是出现在这里。

但是到这里可以发现另外一个问题,因为 loadDeferredLibrary 方法是异步的,而从编译后的 js 代码上看,在执行完 loadDeferredLibrary 之后马上就进入到了checkDeferredIsLoaded ,这显然存在问题。

那为什么 debug 可以正常执行呢? 通过查看 debug 运行时的 js 代码,我发现同样的执行逻辑,在 dartdevc 构建出来后居然完全不一样。

image-20220325181735145

可以看到 checkDeferredIsLoaded 函数和对应的 Widget 是被一起放在逗号表达式里,所以从执行时序上会是和 Widget 在调用时被一起被执行,也就是在 loadDeferredLibrary 之后,所以代码可以正常运行。

通过断点调试也验证了这个时序问题,在 debug 下会先走完 loadDeferredLibrary 的全部逻辑,之后再进入 checkDeferredIsLoaded

image-20220325141938694

而在 release 模式下,代码虽然也会先进入 loadDeferredLibrary , 但是会在 checkDeferredIsLoaded 执行之后才进入到 add(0.this.loadId),从而导致前面的异常被抛出。

image-20220325141617745
image-20220325141632451

那到这里问题基本就很清楚了,前面的代码写法在当前(2.10.3)的 Flutter Web 上,经过 dart2js 的 release 编译后会出现某些时序不一致的问题,知道了问题也很好解决,如下代码所示,只需要把原先代码里的 Widget 变成 WidgetBuilder 就可以了。

image-20220325194206188

我们再去看 release 编译后的 js 文件,可以看到此时的因为多了 WidgetBuilder ,传入的内容变成了 closure69 ,这样就可以保证在调用到 call 之后才触发checkDeferredIsLoaded

image-20220325182649022

三、最后

虽然这个问题不难解决,但是通过这个问题去了解 dart2js 的编译和构建过程,可以看到很多平时不会接触的内容,不过现在我还是不是特别确定是我写法有问题,还是有官方的 dart2js 有 bug

另外 -O0 的转化为什么会不能成功运行也没有头绪,如果有小伙伴知道的欢迎评论告知下~ 。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容