美团外卖商家端业务围绕数百万商家,在 PC 和 App 上分别提供了交易履约、运营、广告、营销等一系列功能,且常常有外投 H5 的场景(如外卖学院、商家社区、营销活动等)。在这种多形态的业务场景下,如何保障多端体验的一致性,以及如何提高多端迭代的效率,一直是商家端产研关注的重点。html
因为端能力的不一样,致使了业务在 App 和 Web 上存在较大的表现差别,例如:App 上自带动画转场,而在 Web 中的实现成本却较高,每每也就降级舍弃了这部分功能。此外,即便咱们可利用公司内部的 Roo、MTDUI 等多端 UI 组件库来尽可能抹平各端的 UI 差别,但因为组件库在各端的实现不尽相同,很难作到完美的一致性体验。前端
因为各端技术体系的不一样,涉及多端的需求每每须要不一样的开发、测试团队各自完成开发、联调、测试、上线等流程,占用资源巨大,在各团队不可并行支持的状况下,甚至可能致使整个业务交付周期被拉长。虽然 React Native、Flutter 等跨平台方案解决了一部分复用的问题,但显然在商家端业务场景下是远远不够的,咱们的目标是要达到全平台(Android、iOS、PC、H5)复用,最大化地提高多端的迭代效率。ios
MTFlutter 是美团外卖搭建起的公司级 Flutter 研发生态,它的架构图以下图所示:web
如图所示,MTFlutter 已涵盖研发、调试、测试、发布、线上运维及工程管理整套闭环,同时落地了动态化解决方案,支撑了公司多个业务发展。在大前端融合的趋势下,美团外卖商家端持续在探索更优的多端复用方案,经过 MTFlutter 生态的建设,目前 Flutter 技术栈已覆盖商家端 App 中 90%以上的业务,同时具有 Flutter 开发能力的同窗也达到 90% 以上。所以,在有足够技术“储备”的前提下,咱们可以基于 Flutter 作全平台(Android、iOS、PC、H5)复用的探索。算法
2018 年 Google 首次公开 Flutter Web Beta 版,旨在进一步实现一份代码、多端运行的愿景。目前,Flutter Web 已被正式合入 Master,期间通过无数工程师的努力,Flutter Web 已能提供与 Flutter Natvie 较统一的交互行为和视觉体验。docker
如上图可知,Flutter Web 与 Flutter Native 的总体架构类似,两者共用 Framework 层(绿色部分),提供了包括动画、手势、基础 Widget 类,以及大部分应用所需的 Material/Cupertino 主题 Widget 集合。区别在于:Flutter Web 重写了 dart:ui 层(黄色部分),利用 DOM、Canvas 对齐了 Flutter Native 的 UI 渲染能力,使得 Flutter 编写的 UI 可以在现代浏览器上正常展现。编程
此外,得益于 dart2js 这个早已成熟的工具,Dart 逻辑可以很容易的转换为 JavaScript,进而在 Web 中被正常运行。canvas
综上所述,咱们选择基于 Flutter Web 探索跨端(App\PC\H5)解决方案,真正实现“Write Once & Run AnyWhere”。固然,面临挑战也是巨大的,主要体如今 Flutter 和 MTFlutter 现阶段对 Web 支持还不是很充足。后端
Google 官方目前对 Flutter Web 的工做主要还集中在 dart:ui(Web)的对齐,工程化和性能相关的事项作的还比较少,例如:api
虽然 MTFlutter 作了诸多 Flutter Native 层面的定制与优化,但在 Flutter Web 上的建设才刚起步,具体表如今:
上图为 MTFlutter + Web 架构图,由图可知 Flutter Web 页面要知足投产要求,还有大量的工做(上图黄色部分所示),主要包括:
企业级应用的基础开发依赖 (如:请求库、路由库、埋点库等),要从新在 Flutter 中用 Dart 搭建一套,时间成本、兼容性、风险等都是不可控的。而 MTFlutter 是基于原有 Native 基础依赖开发的 Plugin,所以并不支持 Web 端。此章节将展开介绍如何丝滑无感地扩展 MTFlutter 基础依赖在 Web 端的实现。
在 Flutter 中经过使用 Package 能够建立易于共享的模块化代码。官方强烈推荐使用 Package 形式管理各类工具方法。在官方定义中 Package 包含如下两种类别:
下面分别对这两种类型 Package 中如何分平台编程进行介绍。
(1) Dart Package
Dart Package 是纯 Dart 编写,所以大部分代码都可由 dart2js 直接编译出 Web 平台可运行的代码,但某些涉及 Native 能力的库 (如 dart:io)是没法被转译的,所以须要有对平台进行兼容的方法,下面介绍两种在 Dart Package 中分平台编程的方案。
代码级别分平台
针对代码级别的分平台,咱们能够借助 Flutter SDK 提供的一个常量 kIsWeb。使用方法以下:
查看源码可知,kIsWeb 之因此能被用于判断 Web 平台,是利用了 JavaScript 不支持整型的特征,在 Web 环境下,Dart 的 double 和 int 由相同类型的对象支持,浮点数 "0.0" 等于整数 "0",对于在 AOT 或 VM 上运行的 Dart 代码却并不是如此。
import 'package:flutter/foundation.dart'; if (kIsWeb) { print('Web 端') } else { print('其余端'); }
文件级别分平台
针对文件级别分平台,咱们利用条件导入导出,其中条件导出具体用法以下:
// tool.dart export 'src/tool_native.dart' // 兜底导出,即没有命中条件时导出的文件 if (dart.library.html) 'src/tool_web.dart'; // web 端导出的文件,该文件中可使用 dart:html,也能够经过判断 dart.library.js 导出 Web 端文件。
// 引入 tool.dart import 'package:tool/tool.dart'; void main() { print('import tool'); }
条件导入和条件导出相似,仅需将 export 改成 import 便可。在业务开发中这也是一种很是实用的分平台编程方法。
(2) Plugin Package
Plugin Package (下文简称为 Plugin) 在 Android 和 iOS 平台都是经过 MethodChannel 实如今 UI 层和 Platform 层传递消息从而达到特定平台支持的,官方文档中也全方位介绍了在 Android 和 iOS 平台的具体实现方法及例子,Web 平台的实现却介绍的较少。总结起来,Web 平台和 Native 平台实现方式的不一样主要集中在下面两点。
首先,Web Plugin 推荐的方式不是以其平台特有的 JS 语言实现,而是经过 Dart Library 或 Package 实现,对于已有现成可用的 JS SDK 或须要大量使用 JS 实现功能的状况下,官方提供了 package:js 包调用 Javascript,从而实现与 Javascript 的交互。
其次,Web Plugin 不是经过注册 MethodChannel 传递消息的,Flutter 内部可直接调用经过官方指定形式 (Federated Plugin) 编写的 Flutter Web Plugin 类。
下图完整的展现了一个 Plugin 的总体架构:
总体来说,MTFlutter 基础依赖都是使用 Plugin 的形式开发维护的。为处理依赖中的公共逻辑,提升 Plugin 的可扩展性,MTFlutter Plugin 在 Flutter Plugin 架构(各平台原生实现层和 Plugin Interface 层)之上又增长了公共逻辑处理层,最终暴露给用户是 Plugin API 层提供的接口。MTFlutter Plugin 架构图以下:
在细节实现上,因为项目中各类依赖的类型之间存在着差别,所以在依赖处理上也略有不一样,下面介绍拥有不一样特色的依赖所对应解决方案。
(1)各平台实现能在 Web 侧对齐的场景,如埋点库
埋点库不管在 Native 端仍是在 Web 端都是使用公司统一提供的 SDK,在 API 设计上具备自然的一致性,所以咱们彻底有能力在 Plugin Interface 层对齐全部接口,上层业务逻辑只需按需作些兼容处理便可。埋点库 Web 端扩展的总体设计思路以下:
(2)各平台实如今 Web 侧没法对齐的场景,如路由库
MTFlutter 路由库是 Native 底层维护的一套全新的路由体系,依靠原生支持提供了强大的定制化功能,而在 Web 端没法这些没法在各平台原生实现层达到 100% 支持。因为 MTFlutter Plugin 最终暴露的是 Plugin API,所以咱们选择直接对齐 Plugin API 实现路由库在 Web 端的支持(借助 Flutter Navigator、dart:html 用纯 Dart 语言完成了扩展),详细架构以下图所示:
(3)Web 端须要经过大量 JS 实现功能的依赖库,如请求库
因为在现有的 Web 请求中统一封装着大量的业务处理逻辑(如拦截器、异常上报等),若是用 Dart 从新实现一遍,成本仍是较高的。想复用原有基于 Axios( JS 请求库) 封装的请求库就至关于让 Plugin 的 Web 平台实现使用 JS 语言。Dart 和 JS 交互是经过 package:js 进行接口调用,所以咱们在公共逻辑处理层用 Dart 对齐了相应的 API,详细架构图以下图所示:
常规的 Web 项目中,为了保证页面有更好的加载和渲染性能,在静态资源文件的处理方面,咱们须要作不少的工做,例如:资源文件 Hash 化、CDN 化、按需加载处理等,这些能够经过 Webpack、Rollup 等构建工具进行预处理。但在 Flutter Web 中,这些预处理的操做目前官方还不支持,缘由是 Flutter 暴露给咱们的命令只有一个 flutter build web
,致使咱们没法直接进行更细粒度的个性化定制。若是想要让 Flutter Web 达到企业级应用的标准,咱们须要更深层次的探索 Flutter SDK 的运行原理。下面咱们列出目前遇到的性能问题及其解决方案。
Google 官方对 Flutter Web 性能优化所作的事项还比较少,编译输出的页面存在较大的性能问题,主要体如今如下两方面:
经过下图对浏览器网络监控状况的展现,能够清晰的反映出以上问题:
为了解决上述的性能问题,咱们探索了 Flutter SDK 编译过程,总结出从 Flutter 业务代码到 Web 产物的总体流程,详细流程以下图所示:
从流程中咱们能够看到,Flutter 在 Web 端目前只支持 Dart-->JS 的转换,以及 UI 层的对齐,在工程化和性能优化方面作的工做并很少。
所以,咱们必须解决以上的性能问题,才能保证咱们的业务能够正常的交付。经过对编译流程的仔细分析与梳理,咱们在 AOT 产物生成以前对 Flutter SDK 进行定制,分别进行加载性能优化和内存性能优化,下面分别介绍这两部分的内容。
运行 flutter build web
命令以后,咱们获得的主要静态资源有:主文件 main.dart.js(1.1M),各页面的业务代码 xxx.part.js(使用 FutureBuilder 后)、图片文件。直接应用这些资源到项目中,会遇到如下问题:
为此,在加载部分咱们对 Flutter SDK 增长了以下三方面的优化,以达到线上运行的标准,优化步骤以下图所示:
资源文件 Hash 化
除了 web/index.html 文件以外,咱们要对全部的引用到文件进行 Hash 化。对 build_system/web.dart 的修改按如下步骤进行:
大文件分片
Flutter Web 编译以后会生成 main.dart.js 这一主文件,体积为1.1M( Gzip 以后约 400K ),这给页面的加载性能带来很大的影响。为此,咱们对代码进行分片,借助浏览器对多文件并行加载的特性,能够有效提高页面的加载性能。
具体实施步骤是:将 main.dart.js
在 Dart 侧拆分红多份纯文本文件,前端经过 XHR 的方式并行加载并按顺序拼接成 Javascript 代码置于 <script> 标签中,从而实现分片文件的并行加载。
资源文件 CDN 化
因为 Flutter Web 资源引用机制的不一样,即便在资源文件 Hash 化的过程当中,把文件的相对路径替换成带 CDN 域名的绝对路径,也没法实现 CDN 资源的加载。同时本地测试发现图片和 Javascript 资源的加载逻辑还不尽相同,为此针对各自的加载逻辑要分别进行优化。
meta
标签中 assetBase
值进行 URL 路径拼接,根据拼接好的 URL 来获取资源。目前,在项目 web/index.html
模板文件中并无 meta
标签,因而就会根据相对路径进行请求。解决方案是在编译过程当中,根据请求环境增长 meta
标签并把 content
设置为 CDN 路径。assetBase
的 meta
标签,但发现 xxx.part.js
文件依然使用当前域名进行加载,可见 Javascript 资源的加载和图片资源加载的逻辑不尽相同。对 main.dart.js
源码分析,咱们发现请求 xxx.part.js
的域名取决于包含 main.dart.js
内容的 Script
标签的 src
属性。经过对 js_helper.dart
的动态编译,咱们把读取 src
属性修改成读取 window.assetBase
这一全局变量(meta
标签中 assetBase
值加工后的变量)来实现 xxx.part.js
文件的 CDN 加载。当页面出现可滚动区域时,每次页面滚动会建立大量的 Canvas。使用 Safari 的 Canvas 分析工具,咱们发现问题的根本缘由是页面滚动的过程当中,Flutter 会频繁的建立滚动区域的 Canvas,每次建立的 Canvas 内存都在10~70M 不等,滚动的内容越多,内存的占用就会越大,这样滚动几帧以后,内存的占用就会超过浏览器的阈值。
Flutter 对 Canvas 的管理有一个 ReusablePool 的概念,在初始过程当中会建立必定的数量的 Canvas,页面交互过程当中没有变化的部分,会优先使用 pool 中已经缓存过的 Canvas 以便可以节省内存。因为 Flutter Web 自身实现了一套页面滚动机制,页面滚动过程当中,会频繁计算位置信息,引发滚动区域内容被从新建立,这就是为何每次滚动都会建立 Canvas 的缘由。
咱们设计的解决方案是:修改 FlutterSDK,在滚动的过程当中定义一个阈值,当滚动的高度在阈值范围内,咱们就会把当前的 Canvas 缓存起来。这样选择性的建立和销毁 Canvas 能够有效的缓解内存压力,从而提高页面滚动性能。
因为 MTFlutter Web 环境安装步骤较固定,且整个安装过程耗时较长 ( > 80s ) 。所以将其定制为 Docker 镜像并集成至 Talos,Flutter Web 编译阶段便能免去安装流程,有效提高构建效率。Docker 镜像定制和发布的详细流程见官方文档,本文再也不赘述。其中用于定制 Flutter Web 镜像的 Dockerfile 文件以下:
FROM $BaseImage \# 继承基础镜像 RUN apt-get update RUN apt-get install rubygems -y RUN gem install flutter-cli RUN flutter-cli install ENV PATH="/$User/.flutter_sdk/bin:${PATH}" ENV PUB\_HOSTED\_URL="https://xxx.com" \# 私有pub服务 ENV FLUTTER\_STORAGE\_BASE_URL="https://storage.flutter-io.cn" RUN ~/.flutter_sdk/bin/flutter config --enable-web
为了实现持续交付与部署,咱们创建起了 Flutter Web 在 Talos(美团内部前端持续交付解决方案) 中的发布流水线:
能够看到,流水线中已经免去了 MTFlutter Web 环境的安装流程,现有流水线中重要节点介绍以下:
咱们在美团外卖商家学院(一个以文章、视频等形式帮助商家学习外卖运营知识、了解行业发展和平台策略的平台,它有很强的传播属性,具备外部投放的场景)率先落地了 Flutter Web,现以商家学院视频内容页为例,对比 Flutter Native 和 Flutter Web 的展示效果:
能够看出,二者的交互、视觉体验是高度一致的,既保证了业务在 App 内接近 Native 的体验,又极大提升了 Web 与 Flutter Native 的体验一致性。
如前文所述,咱们实施了一系列针对 Flutter Web 的资源优化手段,使得页面加载性能有较大提高,其中页面彻底加载时间大体由 1300ms (TP50) 降到了 580ms(TP50),更多的性能指标数据见下图:
能够看到 Flutter Web 与现有 Web 项目性能指标数据差距已不大,可知足平常业务要求。但加载性能数据仍有较大的优化空间,咱们会持续对其进行探索。
针对滚动优化,咱们经过修改 Flutter SDK,使得 Canvas 在页面滚动时无需重复建立,而是被缓存起来。这样大大节省了内存的开销(优化后页面内存占用稳定为 100M 左右,与常规 Web 页面无异),同时在必定程度上提高了滚动性能。以商家学院文章内容页为例,对比优化先后滚动 FPS :
能够看到,Flutter Web 页面滚动性能已获得较大提高,足以应对大部分业务场景。但因为 Flutter Web 页面滚动过程当中会频繁进行位置信息的计算,在复杂的业务场景(如页面存在大量动画) 仍然会暴露出必定的问题。所以对滚动性能的进一步优化也会是咱们将来的工做重心。
基于团队对 Flutter Web 工程化能力的建设和 Flutter 良好的跨平台特性,Flutter Web 在美团外卖商家学院改版需求的落地,大大提高了迭代效率,估算人效提高 40% 以上,计算公式为:
其中 E 表明人效提高,Ci 指的是兼容和适配所耗费的时间,Np 表示业务跨端数量,目前美团外卖商家学院在 Native 和 H5 两端完成了复用,后续在 PC 侧需求的对齐中,效率提高数值会被放大,预计人效提高达 60% 以上。同时咱们将在更多的业务中进行推广与应用,提高总体业务的迭代效率。
综上所述,美团外卖商家端多元的业务形态和足够的技术“储备”,使得基于 Flutter 实现多端复用成为了可能。而 Flutter Web 在美团外卖商家学院业务中也取得了阶段性的成果,实现了 App、H5 侧的体验一致性,为后续推进更多业务线实现 App-Web 一体化打下了坚实的基础。
能够预见的是,基于 Flutter Web 实现的多端复用,势必会有效缩短项目交付周期。但因为咱们对页面加载性能、滚动性能作的仍不够完美,不足以应对更加复杂的业务场景,所以咱们依然还有许多工做:
咱们会持续基于 Flutter Web 作更多的探索和尝试。若是您对 Flutter Web 也感兴趣,欢迎你们在文末评论区留言或者给出建议,很是感谢。
前端 | 算法 | 后端 | 数据 | 安全 | 运维 | iOS | Android | 测试
| 在公众号菜单栏对话框回复【2020年货】、【2019年货】、【2018年货】、【2017年货】等关键词,可查看美团技术团队历年技术文章合集。
版权声明
本文系美团技术团队出品,著做权归属美团。欢迎出于分享和交流等非商业目的转载或使用本文内容,敬请注明“内容转载自美团技术团队”。本文未经许可,不得进行商业性转载或者使用。任何商用行为,请发送邮件至tech@meituan.com申请受权。