likes
comments
collection
share

Flutter for Web在无影中的应用

作者站长头像
站长
· 阅读数 13

无影是使用Flutter的重度用户,无论是在成熟的移动Android、iOS上,还是桌面端MacOS、Windows、还有各种硬件终端上(Linux)上都有应用

Flutter for Web在无影中的应用

今年无影使用Flutter的端又新添了一名成员——浏览器。很兴奋的告诉大家,我们已经将Flutter应用到了Flutter能够覆盖的所有场景。

去年11月我开始研究Flutter for Web的可行性(Flutter for Web技术预研,部分内容我不再复述,大家自行查看这篇内容了解),12月初开始,我就投入了大部分精力到无影桌面端转Web上了,到今年1月初,Flutter转Web工程开始提测,2月9日,发布了第一个线上版本(wuying.aliyun.com/v3 )。这篇文章给大家分享我们的改造过程及踩坑过程。

背景

在使用Flutter for Web前,我们无影客户端是桌面端和Web客户端各自开发维护。桌面端采用Flutter开发,Web端使用React开发,同时桌面端和web端的功能也几乎一致。还有,我们无影是一个高速发展的项目,功能迭代很快,每个版本都会新增大量功能以及会有大量功能的重构,这从下面的图就可以看出来。去年7月份我们重构了UI发布了5.0版本,到去年11月的时候,又发布了为云栖打造的新的交互版本。

Flutter for Web在无影中的应用

由于使用两套代码开发,这给我们项目的迭代带来了很大的挑战,我们需要大量人力去做开发,同时还存在两端发布时间间隔较长、功能不一致等问题,而如果能将桌面端代码直接复用到Web端,就能完美解决这些问题,这也促使我们花费更多的精力去研究Flutter for Web。

面临的挑战

虽然Flutter for Web在Flutter2.0版本就已经合入主线,但还是存在很多问题

我们选择了canvaskit渲染模式,因为html渲染模式还存在一些解不了的问题:

  1. html渲染模式存在很多兼容性问题,部分Flutter API直接无法使用,比如BlendMode.clear,在html渲染模式中使用会直接报错,我们项目中还大量使用了该API,且没有找打其它方式去替代。
  2. html模式渲染效果部分地方同桌面端存在差异
  3. html渲染模式在列表滚动、动画下渲染效果十分糟糕,我们的客户端主页里正好存在这些case(同时存在Flutter布局更新及DOM的布局更新导致渲染性能下降)

因为这些原因,我们放弃了使用html模式,使用了canvaskit模式。不过canvaskit模式也依然存在部分问题

  1. Flutter for Web本身的编译产物就很大,而canvaskit渲染模式还需要额外引入canvaskit.wasm,极大增加了首屏渲染下载资源的大小
  2. canvaskit使用WebGL渲染,WebGL无法加载系统字体,加载额外的字体文件也给首屏渲染增加负担
  3. canvaskit渲染模式使用了WebAssembly和WebGL,所以存在一些兼容性问题。

同时,还面临了一些不可避免的挑战

  1. 基础功能如日志、埋点、存储等需要在Web侧进行支持
  2. 工程化生态不足,构建、部署及静态资源处理都需要从0到1支持
  3. 存在一些dart:html提供的标准API兼容性问题,如退出全屏document.exitFullscreen不是所有浏览器都兼容

功能适配

  1. 桌面端已有功能适配

我们桌面端存在大量插件来扩展Flutter的能力,所以在Web端,我们必须逐一去分析插件的能力然后使用Web的方式去兼容,兼容方式可以看看我上篇文章平台兼容,这里工作量巨大且容易遗漏,只能靠测试来保证功能完整,存在一些风险。

  1. JS侧已有功能适配 集团有很多JavaScript版本的库,在dart中并没有相应的版本,而桌面端使用的是C++实现的,所以需要进行适配。比如我们的埋点库,其架构图如下

Flutter for Web在无影中的应用

我们根据dart版本的能力抽象出tracer_interface层,然后在JS中找到对应能力并抽象出JS_interface层,其用于对接三方库,然后采用Dart调用JavaScript来对接JavaScript的SDK。

首屏加载优化

刚刚说了首屏资源过大造成资源下载时间变长会极大降低用户使用的意愿。经过一系列探索和试错,我总结了以下优化首屏资源的方案。

文字大小处理

canvaskit渲染模式是采用的webgl渲染,webgl渲染无法使用系统字体,必须加载额外的字体文件,我们使用的是阿里普惠体,完整的阿里普惠体有8MB的大小,我们需要优化这个字体的大小。这里我使用了工具fontTools将常用的3000多个字符包括字母及符号输出到一个大约800KB的文件中,然后首屏渲染时只加载这个优化的字体文件,当首屏渲染完成后,再使用Flutter提供的FontLoader去动态加载完整的字体文件。

var bundle = await rootBundle.load('assets/fonts/xxx.ttf');
var loader = FontLoader('Alibaba PuHuiTi')..addFont(Future.value(bundle));
await loader.load();

MaterialIcon图标文件处理

由于历史原因,我们项目中使用了部分Material的图标,因此在编译时会将完整的MaterialIcons-Regular.otf文件(1.6MB)放到fonts目录并在首次渲染时加载,万幸Flutter提供了工具(传入--tree-shake-icons),可以将这个文件精简,只包含项目中使用到的图标。然而在Flutter3.0.2版本中编译web时传入这个参数会导致编译错误(issue,最新master分支已支持),我们可以通过flutter build apk --tree-shake-icons编译Android的方式来获得这个优化的MaterialIcons-Regular.otf文件,然后在Web的编译产物中替换这个文件。

使用deferred as拆分文件

Flutter会将整个工程编译到一个文件mian.dart.js文件中,我们可以使用deferred as来拆分文件,将文件拆分为mian.dart.js_1.part.jsmian.dart.js_2.part.js等,这样就减少了首次加载文件的大小。

支持Gzip压缩

Gzip压缩跟项目本身的设置没有关系,它需要网站部署的服务去支持,这个步骤对大文件的优化效果很好。顺便提一下,我们的前端使用的是O2 Space部署,目前它对wasm格式的文件没有支持Gzip压缩,这里我通过将canvaskit.wasm改为canvaskit.txt来支持canvaskit文件的Gzip压缩(希望官方尽快支持)。

使用加载动画

我使用了Flutter提供的加载函数并使用了JavaScript及CSS做了一个加载动画盖在Flutter屏幕上面,当Flutter界面加载完成并显示时,通知JS隐藏加载动画来达到一个无痕切换。

经过这一系列优化后,我们无影Web客户端从刚开始首屏加载需要23MB资源到最后只需要加载6.6MB资源,需要说明的是,我们的项目也提供了静态资源缓存,所以只有用户首次访问我们的网站是需要下载这些资源,后续再次访问时能从缓存中获得更快的加载速度。

其它疑难问题

  1. 后备字体处理

canvaskit模式需要加载字体的特性,让Flutter官方团队为了保证所输入的字体都能正常显示,引入了后备字体的概念,当输入的字符缺少字体引入时,会从google字体库fonts.gstatic.com下加载后备字体。我们在测试中发现,即使我们加载了完整的阿里普惠体文件,Flutter还是会去加载后备字体,而如果下载后备字体网络遇到问题,首屏加载的速度非常慢。后来我测试出是我们的字体库中没有零宽字符(#8203)导致。国内因为一些众所周知的原因,访问fonts.gstatic.com有很大概率出现问题,所以只能修改引擎代码,将Flutter加载后备字体的代码去掉。

// 注释flutter/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart中的resolvedFonts.forEach(notoDownloadQueue.add);
// resolvedFonts.forEach(notoDownloadQueue.add);

然后重新编译flutter engine。

使用自定义Flutter Web Engine的方式:最好的方式当然是搭建Flutter Engine源,不过成本太高。我们目前采用的方式是将重新编译的引擎中的{engine路径}/src/out/host_debug_unopt/flutter_web_sdk文件夹替换{flutter路径}/bin/cache/flutter_web_sdk,然后将这个编译后的flutter_web_sdk压缩后团队内共享。

  1. 去除浏览器默认样式 在Safari、FireFox等浏览器中,会存在一些默认样式,比如密码输入框后面会出现自带的图标

Flutter for Web在无影中的应用

如果我们要去除这种默认样式,直接写外联css不会生效,需要在flt-glass-pane下写css来清除。JS代码如下

const parent = document.querySelector('flt-glass-pane')
const showDom = parent.shadowRoot
let style = document.createElement('style');
style.innerHTML = '::-ms-reveal { display: none; } ';
showDom.appendChild(style);

部署

部署流程

上面提到的我们部署Flutter产物使用的O2 Space,部署流程如下:

Flutter for Web在无影中的应用

我新建了github仓库用来放置Flutter编译的web产物,目录结构大概如下

├─ .gitignore
├─ .npmrc
├─ abc.json # O2 Space部署脚本
├─ gulpfile.js # 针对Flutter产物二次编译
├─ package-lock.json
├─ package.json
└─ web # Flutter构建产物
       ├─ .last_build_id
       ├─ assets
       ├─ entry.json
       ├─ favicon.png
       ├─ flutter.js
       ├─ flutter_service_worker.js
       ├─ icons
       ├─ index.html
       ├─ main.dart.js
       ├─ main.dart.js_1.part.js
       ├─ manifest.json
       ├─ schedule.css
       ├─ schedule.js
       └─ version.json

当执行flutter build web后,将构建的web产物上传到该仓库中,gulpfile.js会对产物二次编译,比如清除代码注释、JS代码混淆等,只要我们配置好abc.json这些事情会在O2 Space部署时自动完成。

使用CDN静态资源

Flutter加载的静态资源默认需要和index.html在同一目录下。我们可以通过下面方式分别设置静态资源和JavaScript

  1. 静态资源(除JavaScript外):我们可以通过在index.html中设置meta为assetBase的标签来改变项目中加载图片、字体、json文件等资源,如<meta name="assetBase" content="https://dev.g.alicdn.com/aliyun/cdn/0.0.1/"/>。这部分可以在gulpfile.js中动态替换以达到开发环境和部署环境区分开。
  2. JavaScript:因为Flutter加载JS使用的appendChild来加载带src的script标签,我们可以通过重载document.body.appendChild函数来达到更改src路径的目的。
let cdnURL = document.querySelector('meta[name="assetBase"]') ? .getAttribute('content') ? ? '/';
let appendChild = document.body.appendChild
document.body.appendChild = function (el) {
  return appendChild.call(document.body, function () {
    {
      if (cdnURL !== '/' && el.nodeName.toLowerCase() === 'script' && el.baseURI !== cdnURL) {
        el.src = el.src.replace(el.baseURI, cdnURL)
      }
      return el
    }
  })
}

总结

Flutter for Web对于无影目前的情况来说非常合适,不过还是有一些未解决的问题:

  1. 首屏加载资源依然很大,需要持续优化
  2. 主页长时间静置偶现崩溃,未找到具体原因
  3. 部署流程未全部自动化,有出错风险
  4. 无法方便的使用JS生态,需要手动兼容
  5. 字体问题需要自定义引擎

针对这些问题,我也在我们Flutter for Web工程上提出了一些展望

  1. 将首屏资源加载大小控制在5MB以内
  2. 构建部署docker,将部署流程自动化
  3. 编写工具自动生成JS和Dart接口,方便Dart对接JS生态
  4. 与官方共建,找到更完美使用后备字体的方式