[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. 与官方共建,找到更完美使用后备字体的方式
目录
相关文章
|
21天前
|
前端开发 JavaScript 测试技术
深入理解Web3:构建去中心化应用的未来
本文将探讨Web3技术的核心原理及其在构建去中心化应用(DApps)中的应用。我们将从Web3的定义开始,深入到其背后的区块链技术,智能合约,以及如何通过使用现代编程语言和框架实现去中心化应用的开发。此外,本文还将探讨当前Web3生态系统中面临的挑战和机遇,为读者提供一个全面的Web3技术概览,旨在启发开发者和技术爱好者探索去中心化世界的无限可能。
21 2
|
5天前
|
Java Maven 开发者
深入剖析Spring Boot在Java Web开发中的优势与应用
深入剖析Spring Boot在Java Web开发中的优势与应用
|
5天前
|
SQL 监控 Java
Java Web应用中数据库连接池的配置与优化
Java Web应用中数据库连接池的配置与优化
|
5天前
|
Java 应用服务中间件 API
深入解析Java Servlet技术在Web开发中的应用
深入解析Java Servlet技术在Web开发中的应用
190 1
|
5天前
|
开发框架 开发者 UED
跨平台开发框架Flutter在移动应用开发中的应用与前景
【2月更文挑战第3天】本文将探讨跨平台开发框架Flutter在移动应用开发中的应用价值和未来发展前景。通过分析Flutter的特点和优势,以及其在实际项目中的应用案例,展示了Flutter在提升开发效率、降低成本、增强用户体验等方面的优势。同时,对Flutter在未来移动应用开发领域的发展趋势进行了展望,指出了其在日益激烈的市场竞争中的巨大潜力。
|
6天前
|
并行计算 开发者 Python
Python多线程和多进程在Web开发中的应用与挑战
Python多线程和多进程在Web开发中的应用与挑战
|
6天前
|
数据库 开发者 Python
Python在Web开发中的应用:Flask与Django框架介绍与实践
Python在Web开发中的应用:Flask与Django框架介绍与实践
|
9天前
|
前端开发 JavaScript Java
从前端到后端:构建现代化Web应用的技术演进
本文将讨论在构建现代化Web应用时涉及的技术演进,并重点关注前端和后端领域的发展。我们将探索各种编程语言(如Java、Python和C),数据库技术以及前沿的前端和后端框架,帮助读者了解如何利用这些工具和技术来构建高效、可扩展和用户友好的Web应用。
|
21天前
|
Rust JavaScript 前端开发
深入探讨WebAssembly在现代Web开发中的应用
随着互联网技术的不断进步,传统的Web开发方法已经无法满足现代应用程序对性能和功能的高要求。WebAssembly(简称Wasm)作为一种新兴的技术,为解决这一问题提供了独特的视角和方法。本文将从WebAssembly的基本概念出发,详细探讨其在前端开发中的应用,包括与JavaScript的互操作性、在性能优化方面的优势,以及如何通过实例来实现复杂应用的性能提升。此外,文章还将探讨WebAssembly在未来Web开发中的潜在发展方向,旨在为开发者提供一种全新的视角,以更高效、更强大的方式构建Web应用。
|
22天前
|
Web App开发 JavaScript 前端开发
构建现代Web应用:Vue.js与Node.js的完美结合
在当今快速发展的Web技术领域,选择合适的技术栈对于开发高效、响应迅速的现代Web应用至关重要。本文深入探讨了Vue.js和Node.js结合使用的优势,以及如何利用这两种技术构建一个完整的前后端分离的Web应用。不同于传统的摘要,我们将通过一个实际的项目示例,展示从搭建项目架构到实现具体功能的整个过程,着重介绍了Vue.js在构建用户友好的界面方面的能力,以及Node.js在处理服务器端逻辑和数据库交互中的高效性。通过本文,读者不仅能够理解Vue.js与Node.js各自的特点,还能学习到如何将这两种技术融合应用,以提升Web应用的开发效率和用户体验。

热门文章

最新文章

相关产品

  • 无影云电脑