Flutter Web: A compilation problem takes you to understand the package construction and subpackage implementation of Flutter Web

As the most special platform in the Flutter framework, Flutter Web has two different rendering engines by default due to the particularity of the Web platform:

  • html: The layout is drawn through the canvas and Element of the platform;
  • canvaskit: Draw controls through Webassembly + Skia;

Although we all know that canvavskit is closer to the design concept of Flutter, due to the cost considerations caused by the wasm file size and font loading problems it builds, the industry generally chooses a lighter html engine , and today's problem is also based on the html engine. Expand.

This article is one of the few articles about deferred-componentsand Flutter Web construction process analysis .

一、deferred-components

We all know that the main.dart.jsfiles will be very large, so **generally adopt some methods to optimize the package size, and one of the most common methods is to use deferred-components **.

For the deferred-componentsofficial at first, it was mainly used to support dynamic publishing on the Android App Bundle, but after adaptation, this capability has been well extended to the Web, and the size of the filedeferred-components can be easily split according to requirements .main.dart.js

Of course, this is not an introduction to how to use deferred-componentsit, but when using deferred-componentsit , I encountered a magical problem about the packaging and construction of Flutter Web .

First of all, the code is shown in the figure below. As you can see, here is mainly to turn an ordinary page into a normal page through the deferred askeyword deferred-components, and then render the page after libraryFutureloading .

image-20220325173721875

The irrelevant yaml file code is omitted here, so do you think there is any problem with the above-mentioned simple code ?

At first, I didn't think there was any problem. It was no problem to debug by flutter run -d chrome --web-renderer html running to the browser, and the pages could be loaded and opened normally, but when I flutter build web --release --web-renderer htmlpackaged and deployed to the server, I encountered this problem when I opened it :

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)
复制代码

This is very strange, obviously there is no problem when debug is running, why is not loadedthe ?

After simple debugging and printing, it was found that when an error occurs, the code cannot enter ContainerAsyncRouterPagethe container at all, that is, an not loadedexception occurs outside, but it is obviously widgetcalled in the ContainerAsyncRouterPagecontainer, why is not loadedthe ?

By comparing the source code of the exception information, it is found that a section of checking logic will be inserted when processing the at compile time, so the code that throws the exception is added when processing atdeferred ascheckDeferredIsLoadedimport * deferred as .

image-20220325231047005

By looking at the packaged file, you can see that if the loading is not completed checkDeferredIsLoadedbefore , that is, the corresponding is importPrefixnot added setto , an exception will be thrown.

image-20220325214838143

So preliminary inference, the problem should be in the debug and release, there are differences in import * deferred asthe compilation processing.

Second, the construction difference

通过资料可以发现,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

In addition, there is no clue why the conversion of -O0 will not run successfully. If there are any friends who know about it, please comment and let me know~.

Guess you like

Origin juejin.im/post/7079062175532187656