Flutter Web 稳定版本发布至今也有一年多了,经过这一年多的发展,今天就让我们来看看 Flutter Web 究竟有什么不同之处,本篇分享主要内容是目前 Flutter 下少有较为全面的 Web 内容。
一、起源与实现
说起 Flutter 的起源就很有意思,大家都知道早期 Flutter 最先支持的平台是 Android 和 iOS,至今最核心的维护平台依然是 Android 和 iOS,但是事实上 Flutter 其实起源于前端团队。另外前端的同学应该知道,Dart 起初也是为了 Web 而生,事实上 Dart 诞生至今也有 10 年了,所以可以说 Flutter 其实充满了 Web 的基因。 但是作为从 Web 里诞生的框架,和 React Native/ Weex 不同的是,前者是先有了 Web 下的 React 和 Vue 实现之后才有的客户端支持,而对于 Flutter 则是反过来,先有客户端实现之后才支持 Web 平台,这里其实可以和 Weex 做个简单对照。 Weex 作为曾经闪耀过的跨平台框架,它同样支持 Android、iOS 和 Web 三个平台,在 Android 和 iOS 上 Weex 和 React Native 差异性不大,在 Web 上 Weex 则是删减版的 Vue 支持,而由于 API 和平台差异性的问题,Weex 在 Web 上的支持体验一直不是很好:Flutter 来源于前端 Chrome 团队,起初 Flutter 的创始人和整个团队几乎都是来自 Web,在 Flutter 负责人 ErIC 的相关访谈中,Eric 表示 Flutter 来自 Chrome 内部的一个实验,他们把一些乱七八糟的 Web 规范去掉后,在一些内部基准测试的性能居然能提升 20 倍,因此 Google 内部就开始立项,所以 Flutter 出现了。
因为 Weex 需要依赖平台控件实现渲染,导致一个 Text 控件需要兼顾 Android、iOS 和 Web 上原生平台接口的逻辑,从而出现各种由于耦合带来的兼容性问题。
而 Flutter 实现更为特别,通过 Skia 实现了独立的渲染引擎之后,在 Android 和 iOS 上控件几乎就与平台无关,所以 Flutter 上的控件可以做到独立且不同平台上渲染一致的效果。
但是回到 Web 上又有些特殊,首先 Web 平台完全是 html / js / css 的天下,并且 Web 平台需要同时兼顾 PC 和 Mobile 的不同环境,这就让 Flutter Web 成了 Flutter 所有平台里 "最另类又奇葩" 的落地。
首先 Flutter Web 和其他 Flutter 平台一样共用一套 Framework,理论上绝大多数的控件实现都是通用的,当然如果要说最不兼容的 API 对象,那肯定就是 Canvas 了,这其实和 Flutter Web 特殊的实现有关系,后面我们会聊到这个问题。
而由于 Web 的特殊场景,Flutter Web 在 "几经周折" 之后落地了两种不同的渲染逻辑: html 和 canvaskit,它们的不同之处在于:
默认情况下 Flutter Web 在打包渲染时会把 html 和 canvaskit 都打包进去,然后在 PC 端使用 canvaskit 模式,在 mobile 端使用 html 模式,当然您也可以在打包时通过 flutter build web --web-renderer html --release 之类的配置强行指定渲染模式。
既然这里我们讲到了 Flutter Web 的打包构建,那就让我们先从构建打包角度开始来深入介绍 Flutter Web。
二、构建和优化
Flutter Web 虽说是和其他平台共用一个 framework,但是它在 dart 层开始就有一套自己特殊的 engine 实现,并且这套实现是独立于 framework 的一套特殊代码。
所以在 Flutter Web 打包时,会把默认的 /flutter/bin/cache/lib/_engine 变成了 flutter/bin/cache/flutter_web_sdk/lib/_engine 的相关实现,这是因为 Flutter Web 在 framework 之下的 engine 需要一套特殊的 API。
下图右侧构建是指定 web 的打包路径,和左边默认时的对比。
同样下图所示,可以看到 web sdk 里会有如 html、canvaskit 这样不同的实现,甚至会有一个特殊的 text 目录,这是因为在 web 上对于文本的支持是个十分复杂的问题。
那到这里我们知道了在 _engine 层面,Flutter Web 有着自己一套独立的实现,那构建之后的产物是什么样的情况呢?
如下图所示是 GSY 的一个简单的开源示例项目,在部署到服务器后可以看到,默认情况下在不做任何处理时,在 PC 端打开后会使用 canvaskit 渲染,主要会有:
可以看到这些文件占据了 Flutter Web 编译后产物的大部分体积,并且从大小上看确实让人有些无法接受,因为示例项目的代码量并不大,结构也不复杂,这样的体积肯定十分影响加载速度。
所以我们首先考虑在 html 和 canvaskit 两种渲染模式中先选定一种,出于实用性考虑,结合前面的对比情况,选用 html 渲染模式在兼容性和可优化上会更友好,所以这里优化的第一步就是先指定 html 模式作为渲染引擎。
开始优化
首先可以看到 CupertinoIcons.ttf 这个矢量图标文件,虽然默认创建项目时会通过 cupertino_icons 被添加到项目里,但是由于我们不需要使用,所以可以在 yaml 文件里去除。
之后通过运行 flutter build web --release --web-renderer html 后,可以看到使用 html 模式加载后的产物很干净,而需要优化的体积现在主要在 main.dart.js 和 MaterialIcons-Regular.otf 上。
虽然在项目中我们会使用到 MaterialIcons 的一些矢量图标,但是每次加载都要全量加载一个 1.5 MB 的字体库文件显然并不符合逻辑,所以在 Flutter 里官方提供了 --tree-shake-icons 的命令帮助我们优化这部分的内容。
但是不幸的是,如下图所示,在当前的 2.10 版本下该配置运行会有 bug,而不幸中的万幸是,在原生平台的编译中 shake-icons 行为是可以正常执行。
所以我们可以先运行 flutter build apk,然后通过如下命令,将 Android 上已经 shake-icons 的 MaterialIcons-Regular.otf 资源复制到已经编译好的 web/ 目录下。
cp -r ./build/app/intermediates/flutter/release/flutter_assets/ ./build/web/assets
再次打包后可以看到,经过优化后 MaterialIcons-Regular.otf 资源如今只剩下 3.2 kB,那接下来就是考虑针对 2.2 MB 的 main.dart.js 进行优化处理。
要优化 main.dart.js,我们就要讲到 Flutter 里的 deferred-components,在 Flutter 里可以通过把控件定义为 "deferred component" 来实现控件的懒加载,而这个行为在 Flutter Web 上被编译之后就会变成多个 *part.js 文本,原理上就是对 main.dart.js 进行拆包。
举个例子,首先我们定义一个普通的 Flutter 控件,按照正常的控件进行实现就可以。
import 'package:flutter/widgets.dart';
class DeferredBox extends StatelessWidget {
DeferredBox() {}
Widget build(BuildContext context) {
return Container(
height: 30,
width: 30,
color: Colors.blue,
);
}
}
在需要的地方 import 对应控件然后添加 deferred as box 关键字,之后在适当时机通过 box.loadLibrary() 加载控件,最后通过 box.DeferredBox() 渲染。
import 'box.dart' deferred as box;
class MainPage extends StatefulWidget {
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
void initState() {
super.initState();
}
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: box.loadLibrary(),
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return box.DeferredBox();
}
return CircularProgressIndicator();
},
);
}
}
当然,这里还需要额外在 ymal 文件里添加 deferred-components 来制定对应的 libraries 路径。
deferred-components:
- name: crane
libraries:
- package:gsy_flutter_demo/widget/box.dart
回归到上面的 GSY 示例项目中,通过相对极端的分包实现,这里把 GSY 示例里的每个页面都变成一个独立的懒加载页面,然后在页面跳转时再加载显示,最终打包部署后如下图所示:
可以看到拆分之后 main.dart.js 从 2.2 MB 变成了 1.6 MB,而其他内容通过 deferred components 变成了各个 part.js 的独立文件,并且只在点击时才动态下载对应的 part.js 文件,但是此时的 main.dart.js 依旧不小,而官方提供的能力上已经没有太多优化的余地。
在这里可以通过前端的 source-map-explorer 工具去分析这个文件,首先在编译时要添加 --source-maps 命令,这样在打包时会生成 main.dart.js 的 source map 文件,然后就执行 source-map-explorer main.dart.js --no-border-checks 生成对应的分析图:
这里只展示能够被 mapped 的部分,可以看到 700k 几乎就是 Flutter Web 整个 framewok + engine + vm 的大小,而这部分内容其实可以优化的空间并不大,尽管会有一些如 kIsWeb 的冗余代码,但是其实可以调整的内容并不多,大概有 36 处可以调整和删减的地方,实质上打包时 Flutter Web 也都有相应的优化压缩处理,所以这部分收益并不高。
另外,如下图所示是两种不同 web rendder 构建后代码上的差异,可以看到 html 和 canvaskit 单独构建后的 engine 代码结构差异性还是很大的。
而如果您在编译时默认的 auto 模式,就会看到 html 和 canvaskit 的代码都会打包进去,所以相对的 main.dart.js 也会增加一些。
那还有什么可以优化的地方吗?还是有的,通过外部手段,例如通过在部署时开启 gzip 或者 brotli 压缩,如下图所示,开始 gzip 后大概可以让 main.dart.js 下降到 400k 左右。
另外也有在 index.html 里增加 loading 效果来做等待加载过程的展示,例如:
<html>
<head>
<meta charset="UTF-8">
<title>gsy_flutter_demotitle>
<style>
.loading {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
position: ABSolute;
top: 50%;
left: 50%;
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.loader {
border: 16px solid #f3f3f3;
border-radius: 50%;
border: 15px solid ;
border-top: 16px solid blue;
border-right: 16px solid white;
border-bottom: 16px solid blue;
border-left: 16px solid white;
width: 120px;
height: 120px;
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
style>
head>
<body>
<div class="loading">
<div class="loader">div>
div>
<script src="main.dart.js" type="application/javascript">script>
相关资料