乐趣区

关于flutter:阿里卖家-Flutter-for-Web-工程实践

作者:马坤乐 (坤吾)

Flutter 自 2015 年首次亮相以来,通过了多年的倒退曾经相当成熟,在阿里、美团、拼多多等互联网公司都有宽泛的利用。在 ICBU 阿里卖家上 90+% 的新业务应用 Flutter 开发,ICBU 客户端开发组领有泛滥的 Flutter 开发人员。

Flutter for Web (FFW) 晚期试验版于 2019 年公布,在过后曾经有很多感兴趣同学对其进行调研,过后因为刚公布存在诸多问题不适宜在生产环境中应用。在往年(2021)三月份,Flutter 2.0 公布,FFW 正式进入 stable 分支。

阿里卖家外贸资讯版块次要应用 Flutter 开发,在本财年的指标中,外贸资讯的 App 外推广为开源引流的重要一环。App 外资讯推广须要一个承载内容 Web 页面,对该 Web 页面的要求如下:

  • 复刻 App 端相干页面的 UI、性能(次要蕴含一个 dart 编写的自定义 html 解析渲染引擎)【次要工作量】
  • 疾速上线
  • App 端性能同步

因为不足前端同学的反对,想要实现此页面只能由 App 端上同学本人投入,通过肯定的思考咱们抉择了 FFW,理由如下:

  • 切换到前端技术栈 Rax 等老本稍高,同时指标页面性能复刻须要较多工夫
  • 应用 FFW 指标页面上绝大部分代码可复用端上现成 dart 代码
  • App 端上 Flutter 技术栈同学笼罩广

通过以上思考,正式开启 FFW 填坑之旅。

Demo

目前阿里卖家 FFW 相干页面已上线,从 FFW 公布至今产物 js 文件大的问题就始终存在,实践上会很影响页面加载体验,理论测试中察看到在 PC、挪动设施上加载体验尚可,运行很晦涩,相干 Demo 如下:

  • 外投内容展现页 demo:https://alisupplier.alibaba.c…
  • 阿里卖家 App 下载页面:https://alisupplier.alibaba.c…

问题总览

创立 FFW 工程比较简单,Flutter 切换到 stable 版本,之后运行命令 flutter create xxxProject 进入工程后点击运行一个 Demo 工程便可运行起来。要将 FFW 利用到理论的工程中,须要思考的是工程的问题和如何融入阿里的体系的问题,如:怎么公布、开发流程如何管控、怎么申请接口等,总结如下:

以上为阿里卖家 FFW 开源引流最小闭环实际中遇到的问题,除此之外 FFW 待建设的问题还有:

工程根底

接下来是对最小闭环实际中,工程根底问题的呈现起因和解决方案的阐明。

环境和复用

参考 App 端 Flutter 开发,FFW 中首先要思考抉择 Flutter 的什么版本,其次是思考如何复用已有的 Flutter 代码。

Flutter 版本抉择

版本抉择问题因 FFW 和 Flutter for App (FFA) 的 Flutter 版本无奈对立产生。FFW 须要的 Flutter 版本为 2.0+,而目前咱们 App 端内的 Flutter 版本为 1.X+,要降级到 2.0+ 版本还需期待不确定的工夫。通过肯定的思考目前咱们 FFW 和 FFA 抉择版本如下:

FFA: hummer/release/v1.22.6.3          -- UC 的 Hummer 分支
FFW: origin/flutter-2.8-candidate.9     -- 官网分支 

FFW 不选用 stable 版本是因为在最近刚公布的 iOS 15 上 FFW 页面会因 webGL 问题会卡死,该问题修复计划目前已集成到了 candidate 版本。(以后最新 stable 版本为 2.10.0,问题已解决)

代码复用

FFA 代码复用到 FFW 中要思考的问题分两块:

  • Dart 代码复用
  • 平台相干插件能力复用

Dart 代码复用

FFW 须要 Flutter 2.0+ 版本对应的 dart 版本为 2.12,此版本的 dart 引入了空平安 (Sound null safety) 个性。FFA 上应用的 Flutter 版本为 1.+ 版本对应的 dart 还未引入空平安。同时 Flutter 中新老版本 dart 库代码无奈混合编译,所以目前对已有 App 端代码库还无奈做到无缝复用,只能通过批改已有代码进行复用,代码批改次要的点有:

  • 可为空的变量,类型后增加?
User? nullableUser;
  • 操作可为空的变量时应用 ? 或 !
nullableUser?.toString();   // 空平安,如为空不会呈现 NPE
nullableUser!.toString();   // 强制指定非空,如为空会报错 
  • 可选参数 @required 注解替换为 required 保留字
/// 老版本
User({
  @required this.name,
  @required this.age,
});

/// 新版本
User({
  required this.name,
  required this.age,
});

低版本代码通过这三步批改后根本可在新版本编译通过,除此之外还会有局部 API 因为版本升级产生变更,也须要相应的批改,如:

/// 老版本
typedef ValueWidgetBuilder<T> 
  = Widget Function(BuildContext context, T value, Widget child);

/// 新版本
typedef ValueWidgetBuilder<T> 
  = Widget Function(BuildContext context, T value, Widget? child);

在 API 变更中这类问题占大多数,批改起来较简略。另外还有一类改变,如在抽象类 TextSelectionControls 中,handleCut 等办法参数的个数产生了变更:

/// 老版本
void handleCut(TextSelectionDelegate delegate) {...}

/// 新版本
void handleCut(TextSelectionDelegate delegate, 
               ClipboardStatusNotifier? clipboardStatus) {...}

这类改变需依据理论状况进行批改,难度中等,新加的参数大概率是能够不应用的。

平台相干插件

平台相干的插件会调用 Native 的能力,要在 FFW 上应用 FFA 中的插件,须要为插件在 Web 平台实现相应的能力,下文 js 调用局部会进行阐明。如果应用的是 pub.dev 中的库,且该库满足如下条件则可间接应用相应的版本:

  • 代码库有 Web 版本
  • 公布的版本中有反对 Null safety 的版本(反对 Web 也会反对这个)
反对 Web 版本 反对空平安

公布体系

本地 Demo 工程创立并运行胜利后,接下来要思考几个问题:

  • 开发到公布的流程如何管控
  • 如何将页面公布到线上公网可拜访

    • 怎么打包构建
    • 怎么公布

对于开发到公布流程的管控,参考前端选用 DEF 平台(阿里外部前端开发公布平台)通过创立 WebApp 形式治理,这里不具体阐明。对于页面公布波及内容如下:

工程构建

FFW 的构建形式有两种,构建的产物在利用中并非全副须要须要进行肯定的精简;另外要在 DEF 平台上公布产物还需对产物进行一些额定的解决。在构建中次要思考如何构建,FFW 编译构建可选命令如下:

/// canvaskit 形式渲染
flutter build web --web-renderer canvaskit
  
/// html 形式渲染
flutter build web --web-renderer html

两条命令的区别是指标页面以何种形式渲染,Flutter 官网对两种形式区别的解释如下:

总结来说如下:

  • Html 形式:页面应用 Html 的根底元素渲染,长处是页面资源文件小;
  • CanvasKit 形式:应用了 WebAssembly 技术,具备更好的性能,然而因为须要加载 WebAssembly 相干的 wasm 文件从而多加载 2.+ MB 的资源文件,更适宜对页面性能有较高要求的场景。

在空工程上两种形式资源加载比照如下,基于对页面大小和页面性能思考咱们抉择应用 html 的形式。

Html 形式 CanvasKit 形式

产物精简和解决

对于新创建的工程,编译后产物位于 ./build/web 目录下,构造为:

build
└── [384]  web
    ├── [224]  assets
    │   ├── [2]  AssetManifest.json
    │   ├── [82]  FontManifest.json
    │   ├── [740K]  NOTICES
    │   └── [96]  fonts
    │       └── [1.2M]  MaterialIcons-Regular.otf
    ├── [917]  favicon.png
    ├── [6.5K]  flutter_service_worker.js
    ├── [128]  icons
    │   ├── [5.2K]  Icon-192.png
    │   └── [8.1K]  Icon-512.png
    ├── [3.6K]  index.html【公布保留】├── [1.2M]  main.dart.js【公布保留】├── [570]  manifest.json
    └── [59]  version.json

其中各目录和文件的作用和阐明如下:

  • assets: 图片、字体等资源文件,对应 yaml 文件中配置的 assets,在 FFW 中图片配置在 TPS 上且不应用 IconFont 的状况下,该目录可不须要;
  • favicon.png: 页面的 icon,应用 TPS 资源时可不须要;
  • flutter_service_worker.js:本地 debug 时管制页面加载、reload、敞开等,公布时不须要;
  • icons:icon 资源,公布到 TPS 可不须要;
  • index.html:页面入口文件,次要工作是引入 main.dart.js 还有一些其余资源,相似 App 的壳工程,须要;
  • main.dart.js:工程中 dart 编译后的产物,须要;
  • manifest.json: 页面作为 webapp 应用的配置,可不须要;
  • version.json: 构建信息,可不须要。

在理论公布中,须要的构建产物只有 index.html 和 main.dart.js,对于每次的迭代,不波及到“壳工程”变更时只须要 main.dart.js 即可。

选定了须要的产物后,在 DEF 平台公布前还须要对这两个文件进行一些解决:

  • html 中对 main.dart.js 的援用替换为相应迭代的 cdn 地址(依据迭代号、公布环境拼接);
  • html 中 <base> 标签批改,参考 https://docs.flutter.dev/deve…;
  • js 和 html 文件内正文移除(def 公布门神查看);
  • js 中替换 ?? 运算符(钉钉 H5 容器中该运算符报错);
  • 将 index.html 和 main.dart.js 挪动到 DEF 平台上的产物文件夹。

页面公布

在 DEF 平台上,产物文件解决实现后 js 和 html 文件会被公布到相应的 cdn,同时 html 会被部署到特定的地址上:

预发:

  • https://dev.g.alicdn.com/alge…
  • https://dev.g.alicdn.com/alge…
  • https://market.wapa.taobao.co… (部署地址)

线上:

  • https://g.alicdn.com/algernon…
  • https://g.alicdn.com/algernon…
  • https://market.m.taobao.com/a… (部署地址)

对于线上环境 index.html 内容如下:

<!DOCTYPE html>
<html>
 <head> 
  <!-- 公布到域名的二级目录时应用 --> 
  <base href="/content_page/" /> 
 </head> 
 <body> 
  <!-- 替换为 main.dart.js 相应的 cdn 地址 --> 
  <script type="text/javascript" src="https://g.alicdn.com/algernon/alisupplier_content_web/1.0.10/main.dart.js"></script>  
 </body>
</html>

至此应用页面部署地址就能够拜访到咱们的指标页面了如果页面是一次性关上的,且不须要在外部进行多页面跳转,到这一步公布工作就实现了。如果波及到多页面跳转,还须要将相干的内容公布到本人的域名下,比较简单的形式为配置重定向,除此之外间接援用产物也可:

  • 指标域名地址重定向:将本人域名下地址重定向到页面部署地址,如将 https://alisupplier.alibaba.c… 映射到 https://market.m.taobao.com/a…。这样 def 每次公布后不需做额定批改。留神:要求 FFW 的 UrlStrategy 为 hash tag 形式(默认的 UrlStrategy);
  • 指标域名地址重定向:在指标域名下创立 index.html 并援用 main.dart.js 文件,或者指标页面内嵌公布的页面。参照:https://docs.flutter.dev/depl…。

代码调试

根底链路跑通后就能够进行需要的开发了,开发过程中比拟重要的环境是代码的调试,FFW 可在 Chrome 中以相似 App 的形式调试且体验较好。在 Debug 环境下在 IDE 中设置断点后,即可在 IDE 中调试断点,也可在 Chrome 中查看断点,Chrome 中甚至可看到 dart 代码。以 VSCode 为例 Debug 过程和体验如下:

启动 Flutter 调试

VSCode 和 Chrome 中可见的断点

能力反对

进入到理论的开发中后,就须要诸如路由、接口申请等能力的反对了,首先是页面路由和地址。

页面路由和地址

在 FFW 利用中呈现多页面,或者须要通过 Http 链接传参时,就须要进行相应的路由配置。相似 FFA,可在根 MaterialApp 中配置相应的 Route,之后应用 Navigator.push 跳转或通过页面地址间接关上页面即可。如下代码可实现命名跳转和页面地址跳转:

/// MaterialApp 配置
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      onGenerateRoute: RouteConfiguration.onGenerateRoute,
      onGenerateInitialRoutes: (settings) {return [RouteConfiguration.onGenerateRoute(RouteSettings(name: settings))];
      },
    );
  }
}
/// 命名路由配置
class RouteConfiguration {
  static Map<String, RouteWidgetBuilder?> builders = {
    /// 页面 A
    '/page_a': (context, params) {return PageA(title: params?['title']);
    },

    /// 页面 B
    '/page_b': (context, params) {return PageB(param1: params?['param1'], param2: params?['param2']);
    },
  };

  static Route<dynamic> onGenerateRoute(RouteSettings settings) {
    return NoAnimationMaterialPageRoute(
      settings: settings,
      builder: (context) {var uri = Uri.parse(settings.name ?? '');
        var route = builders[uri.path];

        if (route != null) {return route(context, uri.queryParameters);
        } else {return CommonPageNotFound(routeSettings: settings);
        }
      },
    );
  }
}

配置实现后即可在页面能进行跳转,或者通欧冠浏览器地址间接跳转:

  • 利用内跳转:配置实现之后,在利用外部可通过 Navigator 跳转到指标页面
/// Navigator 跳转页面 B
Navigator.of(context).restorablePushNamed('/page_b?param1=123¶m2=abc');
  • 地址跳转:在浏览器地址栏中输出页面的地址跳转到页面
/// 页面 B 拜访地址
https://xxx.xx/#/page_b?param1=123¶m2=abc

留神:上述地址跳转形式要求 FFW 的 UrlStrategy 为 hash tag 形式(默认的 UrlStrategy)。

Web 平台的 Native —— JS 调用

通过应用 pub.dev 等仓库,能够在 FFW 中轻松的应用各种能力。对于仓库中没有的能力就要思考进行扩大了。在 FFA 上可通过插件的形式应用 native 的能力,同样在 FFW 上可通过扩大应用 js 的能力。通过调用 js 的能力前端海量的技术积攒便可利用到 FFW 上。

FFW 中的 dart 最终会编译成 js,在 FFW 中理当能够人造应用 js。为了在 dart 中反对 js 的调用,dart 官网公布了 js 库,通过应用该库中的注解可是很不便的在 dart 中调用 js。

比方须要调用 alert 办法时,进行如下定义:

/// 文件:js_interface.dart

/// 调用 js 办法的工具类库,需在 dependencies 中引入 js 库
@JS()
library lib.content;

import 'package:js/js.dart';

/// alert 弹窗
@JS('alert')
external void jsAlert(String msg);

之后在须要 alert 的中央引入 js_interface.dart 并调用 jsAlert 办法即可:

import 'package:mtest/js_interface.dart';

...
jsAlert('测试音讯');
...

更多用法详见 pub.dev 中 js 库的阐明:https://pub.dev/packages/js。买通了 js 的能力后,接下来的很多问题迎刃而解。

Mtop 接口

鉴于 App 端现有 Mtop(阿里 App 应用的一种网关)的建设,如果能在 FFW 中调用现有的 Mtop 将能够缩小很多的工作量。为此须要为 FFW 增加 Mtop 调用的能力,要实现这个工作须要两局部的工作:

  • FFW 端反对 Mtop 调用
  • 服务端反对 H5 形式的 Mtop 调用

FFW 反对 Mtop

通过调用 mtop.js 的能力便可在 FFW 中引入 mtop 的能力。整体流程如下:

1、在 index.html 中引入 mtop.js

<script src="//g.alicdn.com/mtb/lib-mtop/2.6.1/mtop.js"></script>

2、定义接口文件 js_mtop_interface.dart

@JS()
library lib.mtop;

import 'package:js/js.dart';
import 'package:js/js_util.dart';
import 'dart:convert';
import 'dart:js';

/// mtop 申请的参数
@anonymous
@JS()
class MtopParams {
  external String get api;
  external String get v;
  external dynamic get data;
  external String get ecode;
  external String get dataType;
  external String get type;
  external factory MtopParams({
    required String api,
    required String v,
    required dynamic data,
    required String ecode,
    required String dataType,
    required String type,
  });
}

/// lib.mtop 申请函数
@JS('lib.mtop.request')
external dynamic _jsMtopRequest(MtopParams params);

/// dart map 转为 js 的 object
Object mapToJSObj(Map<String, dynamic> a) {var object = newObject();
  a.forEach((k, v) {
    var key = k;
    var value = v;
    setProperty(object, key, value);
  });
  return object;
}

/// mtop js 申请接口
Future mtopRequest(String api, Map<String, dynamic> params, String version, String method) {
  var jsPromise = _jsMtopRequest(
    MtopParams(
      api: api,
      v: version,
      data: mapToJSObj(params),
      ecode: '0',
      type: method,
      dataType: 'json',
    ),
  );
  return promiseToFuture(jsPromise);
}

/// 返回后果解析应用
@JS('JSON.stringify')
external String stringify(Object obj);

3、进行 mtop 接口调用

import 'package:mtest/mtop/js_mtop_interface.dart';

...
try {var res = await mtopRequest(apiName, params, version, method);
   print('res $res');
} catch (err) {data = stringify(err);
}

4、解析后果:接口申请返回的后果是一个 jsObject,可通过 js 办法 JSON.stringify 转成 json 后在 dart 层面应用

String jsonStr = stringify(res);

服务端 H5 Mtop 配置

FFW 中接入 mtop.js 后,须要对指标 mtop 接口进行相应的解决才可调用:

  • mtop 公布 h5 版本
  • 申请配置 CORS 域名白名单

打点

同 mtop 申请,FFW 中可引入黄金令箭的 js 库进行打点,流程如下:

1、index.html 引入 js 文件

<script type="text/javascript" src="https://g.alicdn.com/alilog/mlog/aplus_v2.js"></script>

2、定义接口文件 js_goldlog_interface.dart

@JS()
library lib.goldlog;

import 'package:js/js.dart';

/// record 函数
@JS('goldlog.record')
external dynamic _jsGoldlogRecord(String t, String e, String n, String o, String a);

void goldlogRecord(String logkey, String eventType, String queryParams) {_jsGoldlogRecord(logkey, eventType, queryParams, 'GET', '');
}

3、打点调用

import 'package:mtest/track/js_goldlog_interface.dart';

...
goldlogRecord(logkey, eventType, queryParams);
...

之后在 log 平台进行相应的点位配置即可。

监控

监控能力接入较为简单,这里抉择 arms(利用实时监控服务),间接在 index.html 中引入 arms 即可。流程如下:

1、在 index.html 中引入相干库

<script type="text/javascript">
    var trace = window.TraceSdk({
      pid: 'as-content-web',
      plugins: [[window.TraceApiPlugin, { sampling: 1}],
        [window.TracePerfPlugin],
        [window.TraceResourceErrorPlugin]
      ],
    });
    // 启动 trace 并监听全局 JS 异样,debug 时问题临时正文
    trace.install();
    trace.logPv();
</script>

2、在 arms 平台上进行相干配置

留神:trace.install() 在 Debug 环境下会导致页面不展现,可在 Debug 环境中禁用。

优化和兼容

实现了上述根底能力建设后,FFW 根本可满足简略需要的开发。需要开发之外还需思考体验、兼容性等问题。

加载优化

FFW 从公布至今都存在的一个问题就是包大小问题,对与一个空的 helloworld 工程,单 js 包大小是 1.2 MB(未压缩前),在挪动设施上网络不好的时候可能须要加载好些秒。为了晋升页面加载的体验,思考能够做的事件如下:

期待过程优化

FFW 页面在 js 加载实现之前都是白屏,给人一种页面卡死的感觉,为此能够在 js 加载实现前减少加载动画不至于让页面始终白屏。参考 App 上管用的做法,可在数据加载进去之间插入骨骼屏,实现如下:

 <iframe src="https://g.alicdn.com/algernon/alisupplier_content_web/0.9.1/skeleton/index.html" id="iFrame" frameborder="0" scrolling="no"></iframe>
  <script>
    function setIframeSize() {<!-- 骨骼屏尺寸设置,占满全屏 -->}
    function removeIFrame() {var iframe = document.getElementById("iFrame");
      iframe.parentNode.removeChild(iframe);
    }
    setIframeSize();
</script>

  <!-- load 实现之后移除骨骼屏 -->
  <script type="text/javascript" src="https://g.alicdn.com/algernon/alisupplier_content_web/1.0.10/main.dart.js" 
    onload="removeIFrame()"></script>

TODO JS 拆包 & 优化

期待过程优化可在肯定水平上晋升期待体验,单治标不治本,要想加载快还得让加载的资源小,对于多页面利用,能够将整个 main.dart.js 拆分成多个小的包,在应用的过程中逐渐加载,目前理解到美团有相应的技术,但实现细节未知,有待钻研。可参考 https://github.com/flutter/fl…

兼容问题

相似 App 在不同设施上会有体验问题,FFW 在不同的 H5 容器中页会存在兼容问题,在咱们的实际中不同 H5 容器踩坑记录如下:

钉钉 H5 容器内白屏问题:

  • 不反对 ?? 语法,替换后解决
  • FFW 产物 js 中蕴含大量 try{}finally{} 无 catch 操作,在钉钉 H5 容器中会报错,打包时应用脚本对立替换解决

微信 H5 容器内白屏问题:

  • 移除 MaterialIcons,改用图片代替

iOS 15 上页面卡死问题:

  • iOS 15 webGL2.0 问题导致,目前已有解决方案待稳定版公布 https://github.com/flutter/fl…

iOS 兼容性问题:

  • 可点击的 RichText,设置下划线属性后,紧跟着图片的链接会被遮挡,暂未找到解法,只能先不应用 RichText 自带的下划线
  • 可点击的 RichText 点击后屏幕会主动滚动。验证为 InteractiveSelectionu 属性导致,设置为 false 后体现和 Android 统一

其余问题

除了 H5 容器的兼容问题外,在实践中还遇到 FFW 本身的一些问题,记录如下:

provider 库问题:

  • provider 库中 /lib/src/provider.dart ProviderNotFoundException 类 toString() 办法中蕴含一个巨长的谬误阐明 String,该 String 编译后的 js 语法会出错,删除后即可

JsonConverter 问题:

  • JsonConverter().convert 运行时会报错,审慎应用,dart array 转 js array 可手动转换

TODO 的内容

以后实际中只实现了业务可用的一个小闭环建设,FFW 中仍有很多 TODO 的内容,如下:

工程构建:

  • DEF 云端构建:经尝试 DEF 云端构建平台装置 Flutter 环境的时候对阿里外内容的申请都会 403,而 Flutter 中有很多内容须要在线拉取,如 Flutter 根目录下 packages 中的内容,目前应用本地构建,待解决;
  • 本地 debug 时 mtop 拜访:mtop 申请需配置 CORS 白名单且端口需是 80,本地 debug 时应用的是 ip、端口为一个随机数,强行设置时报无权操作,目前只能本地运行 http 服务器设置 host 后在 chrome 中 debug,断点 debug 待解决。

根底性能:

  • 视频、音频播放能力待钻研

兼容和优化

  • js 包拆分加载待钻研
  • 自定义字体文件优化待钻研

畅想:

  • App 中 Flutter 动态化:将 App 内的 Flutter 页面替换为 FFW,做成相似 weex 的动态化计划
  • App WebApp 化:Flutter 实现的 App 通过 FFW 能够低成本转成 WebApp,解决诸如 App 没 Mac 版本的问题

关注【阿里巴巴挪动技术】微信公众号,每周 3 篇挪动技术实际 & 干货给你思考!

退出移动版