[AliFlutter]Flutter for Web在无影中的应用

简介: 无影是使用Flutter的重度用户,无论是在成熟的移动Android、iOS上,还是桌面端MacOS、Windows、还有各种硬件终端上(Linux)上都有应用![](https://ata2-img.oss-cn-zhangjiakou.aliyuncs.com/neweditor/c2434612-86ee-4fb6-a7d1-1622eb6d050d.png)今年无影使用Flutt

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

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

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

背景

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

由于使用两套代码开发,这给我们项目的迭代带来了很大的挑战,我们需要大量人力去做开发,同时还存在两端发布时间间隔较长、功能不一致等问题,而如果能将桌面端代码直接复用到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++实现的,所以需要进行适配。比如我们的埋点库,其架构图如下

我们根据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等浏览器中,会存在一些默认样式,比如密码输入框后面会出现自带的图标

如果我们要去除这种默认样式,直接写外联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,部署流程如下:

我新建了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. 与官方共建,找到更完美使用后备字体的方式
目录
相关文章
|
15天前
|
前端开发 JavaScript 关系型数据库
从前端到后端:构建现代化Web应用的技术探索
在当今互联网时代,Web应用的开发已成为了各行各业不可或缺的一部分。从前端到后端,这篇文章将带你深入探索如何构建现代化的Web应用。我们将介绍多种技术,包括前端开发、后端开发以及各种编程语言(如Java、Python、C、PHP、Go)和数据库,帮助你了解如何利用这些技术构建出高效、安全和可扩展的Web应用。
|
1月前
|
监控 Serverless 测试技术
Serverless 应用引擎常见问题之做的web服务计费如何解决
Serverless 应用引擎(Serverless Application Engine, SAE)是一种完全托管的应用平台,它允许开发者无需管理服务器即可构建和部署应用。以下是Serverless 应用引擎使用过程中的一些常见问题及其答案的汇总:
407 3
|
8天前
|
缓存 负载均衡 数据库
优化后端性能:提升Web应用响应速度的关键策略
在当今数字化时代,Web应用的性能对于用户体验至关重要。本文探讨了如何通过优化后端架构和技术手段,提升Web应用的响应速度。从数据库优化、缓存机制到异步处理等多个方面进行了深入分析,并提出了一系列实用的优化策略,以帮助开发者更好地应对日益增长的用户访问量和复杂的业务需求。
12 1
|
8天前
|
缓存 监控 数据库
Flask性能优化:打造高性能Web应用
【4月更文挑战第16天】本文介绍了提升Flask应用性能的七大策略:优化代码逻辑,减少数据库查询,使用WSGI服务器(如Gunicorn、uWSGI),启用缓存(如Flask-Caching),优化数据库操作,采用异步处理与并发(如Celery、Sanic),以及持续监控与调优。通过这些手段,开发者能有效优化Flask应用,适应大型或高并发场景,打造高性能的Web服务。
|
9天前
|
数据库 开发者 Python
Python中使用Flask构建简单Web应用的例子
【4月更文挑战第15天】Flask是一个轻量级的Python Web框架,它允许开发者快速搭建Web应用,同时保持代码的简洁和清晰。下面,我们将通过一个简单的例子来展示如何在Python中使用Flask创建一个基本的Web应用。
|
13天前
|
JavaScript 前端开发 API
Vue.js:构建高效且灵活的Web应用的利器
Vue.js:构建高效且灵活的Web应用的利器
|
21天前
|
XML JSON JavaScript
使用JSON和XML:数据交换格式在Java Web开发中的应用
【4月更文挑战第3天】本文比较了JSON和XML在Java Web开发中的应用。JSON是一种轻量级、易读的数据交换格式,适合快速解析和节省空间,常用于API和Web服务。XML则提供更强的灵活性和数据描述能力,适合复杂数据结构。Java有Jackson和Gson等库处理JSON,JAXB和DOM/SAX处理XML。选择格式需根据应用场景和需求。
|
1月前
|
前端开发 JavaScript 安全
深入探索 Qt6 web模块 WebEngineCore:从基础原理到高级应用与技巧
深入探索 Qt6 web模块 WebEngineCore:从基础原理到高级应用与技巧
74 0
|
1月前
|
缓存 监控 应用服务中间件
如何使用负载均衡器提升Python Web应用的性能?
【2月更文挑战第27天】【2月更文挑战第94篇】如何使用负载均衡器提升Python Web应用的性能?
|
1月前
|
物联网 调度 开发者
构建高效Python Web应用:异步编程与Tornado框架解析
【2月更文挑战第27天】 在处理高并发的Web应用场景时,传统的同步阻塞模型往往难以满足性能需求。本文将深入探讨Python世界中的异步编程概念,并结合Tornado这一轻量级、非阻塞式Web服务器及框架,展示如何构建高性能的Web应用。通过实例驱动的方法论,我们将剖析Tornado的核心组件,包括其IOLoop、异步HTTP客户端和服务器端处理机制,以及与协程集成的细节。文章旨在为开发者提供一套实践指南,帮助他们利用Python实现快速响应和资源高效的Web服务。
29 2