本文字数:7743

预计阅读时间:60分钟

01

前言

首先说明一下,这篇文章是给具备Flutter开发经验的客户端同学看的。Flutter的诞生虽然来自Google的Chrome团队,但大家都知道Flutter最先支持的平台是Android和iOS,至今最核心的维护平台依然是Android和iOS。由于dart语言的学习成本不高,Flutter的响应式UI与ComposeUI和SwiftUI都有极大的相似之处,整体的架构思路也更偏向于客户端的模式,再加上为了实现很多硬件或Native相关的基础功能也需要专业的客户端开发知识,所以 Flutter更多的是被客户端开发同学认可并使用(在我们的团队中,Flutter已经是客户端开发同学的必备基本技能)。 在此背景下,Flutter最初并不在web端上发力。不过由于Flutter本身就是携带了web的基因,在Flutter2发布的同时也发布了web的稳定版。那么它有什么优势和劣势呢?

  • 优势:1. 零学习成本:当你已经掌握了Flutter开发能力后,哪怕你对html,css,JavaScript和主流的前端框架不那么了解,也不影响你开发web应用。2. 跨端能力:可将现有Flutter移动应用拓展到web,在多个平台共享代码,降低开发成本。

  • 劣势:1. 兼容性问题:使用html模式来进行渲染时,应用的大小相对较小但可能会出现兼容性问题。2. 包体积增加:使用canvaskit模式来进行渲染时,虽然性能较好,且可以降低不同浏览器渲染效果不一致的风险,但会增加包体积。

分析了优势劣势后,我们发现如果单纯的做个web端应用,Flutter并没有优势,前端开发同学大概也不会使用Flutter进行web开发(确实没必要,比如包体积增加或有一定的性能损失,还需要学习新语言与开发思路,原生开发不香么),Flutter Web到底有什么用呢? 带着这样的想法,在使用Flutter后的很长时间都不曾调研过web端的支持。但随着业务和内部需求的发展变化,我们有了使用Flutter进行web开发的想法。下面我来说一下使用Flutter Web主要的三个场景。

02

Flutter Web的使用场景

1、客户端团队内部的web需求

在后疫情时代降本增效的大背景下,我们会更多的使用自研工具。自研工具的使用和结果展示的可视化通常以网页的形式展现。客户端同学使用 Flutter Web 进行网页开发学习成本低,完全可以快速的开发网页(本人在使用 Vue 框架进行 web 端开发时感受出客户端和前端的 UI 布局思路还是有很大不同的,css 很灵活约束性低,这个与客户端布局的强约束性差异很大,所以对于客户端开发来说,使用 Flutter 开发网页应用时更顺手。对于全员掌握 Flutter 技能的我们团队来说已经是0成本了)。

2、简单的web端业务需求

web 端承载了很多活动需求,这些需求的特点是时效性强,功能较简单,且不需长期维护。但这些需求经常是在某一时间段大量产生的(比如逢年过节的一些活动或榜单),或突然产生的(比如蹭热点的即时需求)。这些工作的插入有时会导致一些长期迭代的 web 端需求需要延期,影响团队的整体排期。由于这些需求开发难度不大,性能要求不高,不需长期维护(意味着即使团队里不再有人使用 Flutter 或 Flutter Web 有一天挂了也没什么影响),那么就可以让 Flutter 开发同学加入进来,平摊了一部分工作,以此来提升整个团队的效率。

3、客户端与web端的跨端

随着 Flutter Web 趋于稳定,用 Flutter 实现的 App 可以低成本的被打包成 web 版了,毕竟对于用户来说使用浏览器打开个网页比下载个 App 成本低多了。这种情况下我们就可以利用 Flutter 的跨端优势,节约很多人力资源,避免去重新开发一套 web 端了。

好的既然有了使用场景,我们就好好来走一下 Flutter Web 是怎么开发部署上线的流程。

03

Flutter Web工程的创建和业务实现

1、创建与运行

我们使用 Android Studio 作为IDE,以 Flutter 3.10.5 版本为基础创建一个 Flutter Web 工程。 创建一个 New Flutter Project,在选择 Platforms 的时候只勾选 Web,然后直接 Create。

然后我们发现在工程目录里多了个web的文件夹:

如果你是为现有的Flutter 工程添加Web 的支持,只需在项目根目录运行如下命令即可:

flutter create –platforms=web .

项目创建好了,如果想要run起来只需选择chrome浏览器,点击run就行了:

然后我们就可以在浏览器看到运行结果了,当然我们也可以打开开发者模式方便查看与调试:

这部分跑通后,非常恭喜你可以愉快的用Flutter开发网页了,接下来我们实现一个业务需求:做一个网页搜索功能。

业务功能上的开发实现我就不做赘述了,可以告诉做过Flutter开发的同学,没什么不同,基础配置/网络模块/数据共享/路由等该怎么封装就怎么封装,我也不过是直接拿了之前客户端Flutter工程相应模块的代码,稍作修改而已。UI上的开发也是该怎么布局怎么布局,业务的开发体验上和客户端使用Flutter没什么不同。

2、window

在web端开发的时候我们通常会使用window对象进行一些操作。window对象代表一个浏览器窗口或一个框架。常用的event监听,打开网页等操作都需要window对象。Flutter自带的dart:html封装了window,我们可以通过它来实现获取window的属性或对window进行操作,比如:

//打开网页window.open("http://www.baidu.com","");//监听eventwindow.addEventListener("mousedown",(event)=>{//dosomething});

另外window也可以帮助我们区分运行环境。

3、浏览器运行环境区分

客户端通常需要区分的是Android和iOS这两个不同的运行环境,而web端是需要通过UA来区分不同的浏览器环境的,不同环境下的UI/逻辑等会有差别。在国内,我们最常需要区分PC端/移动端/Android端/iOS端/微信网页/微信小程序这几个。那么我们可以定义一个类,利用window.navigator.userAgent去区分这些环境:

import'dart:html';class DeviceUtil{static final DeviceUtil _instance=DeviceUtil._private();static DeviceUtil get()=>_instance;factory DeviceUtil()=>_instance;late String ua;DeviceUtil._private(){ua=window.navigator.userAgent;}//移动端isMobile(){returnRegExp(r'phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone').hasMatch(ua);}//iOS端isIos(){returnRegExp(r'\(i[^;]+;(U;)" />

4、开发/测试/生产环境区分

同客户端一样,web端也需要区分开发/测试/生产环境。同客户端的方式一样,我们还是可以通过配置不同的入口文件来实现环境的区分。如:

  • main_dev.dart

voidmain(){AppConfig.init(ConfigType.dev);root_main.main();}
  • main_test.dart

voidmain(){AppConfig.init(ConfigType.test);root_main.main();}
  • main_online.dart

voidmain(){AppConfig.init(ConfigType.online);root_main.main();}

在AppConfig.init()就可以根据不同的环境做不同的配置了。

5、其他常用库或插件

关于数据共享/网络/UI/动画等库就不做介绍了,因为这些库和平台不相关,用各自熟悉的就好,下面是来介绍一下为了实现一些浏览器相关功能需要用到的插件。

  • shared_preferences在客户端开发的时候,我们知道如果需要对一些数据实现轻量级的本地序列化可以使用 shared_preferences,其实现对应Android的SharedPreferences和iOS的NSUserDefaults。而在进行web开发的时候,我们知道如需在本地序列化一些数据的话,可以使用LocalStorage。其实Flutter的shared_preferences插件也是支持web的,其实现也正是封装了LocalStorage。关于shared_preferences的使用也不做赘述了,已经非常熟悉了。

  • image_picker_for_web来自于我们熟悉的image_picker插件。根据浏览器的不同,支持或部分支持拍照/拍视频/读取图片/读取视频等。

  • js这个插件是用来使用注解的方式帮助你用Dart调用JavaScript API或用JavaScript调用Dart API的。

好了,到此为止,我觉着使用Flutter开发一个常规的web业务已经不成问题了。接下来我们探讨一下如何调试呢?

04

调试

跑通后应该如何调试呢?我们先来说明一下 PC 端的调试方式。

1、PC端调试

如果熟悉浏览器开发者模式,可直接使用浏览器进行调试,打log或debug都是没问题的,也可以看到源码,可以抓包:

当然客户端同学可能不熟悉浏览器开发者模式,也没关系,利用Android Studio,之前在客户端写Flutter怎么调试,现在写web端依旧可以怎么调试。 介绍完PC端的调试,那么在移动端应该如何调试呢?

2、移动端调试

我们依旧可以用PC上的浏览器,红色箭头指向的位置可以切换至移动端模拟器设备,可以选择机型。但更多的时候,我们希望可以真机调试。熟悉vue框架的同学都知道,在本地调试的时候,会给出两个地址,如下图所示:

我们可以在手机浏览器上输入Network显示的ip地址进行调试。在Flutter环境上并没有提供相应的ip地址,我们可以通过flutter的本地打包命令指定一个地址,如下所示:

flutter run -d chrome --web-hostname 10.2.136.130 -t lib/main_test.dart --web-port 8080

指定本机的ip地址和端口号,然后在手机浏览器上输入:

10.2.136.130:8080

之后我们如何看到调试信息呢?由于使用Chrome浏览器需要科学上网,在此我们以iPhone的Safari浏览器+PC端的Safari浏览器为例:

  • 1.首先我们需要用数据线将手机和电脑连接起来。

  • 2.找到Safari的开发菜单,找到你手机的名称,然后选择相应的地址,如下图所示:

  • 3.然后我们就可以看到网页检查器进行调试了,如下图所示:

如何进行调试我们已经清楚了,假设我们已经开发完成了,如何打包部署上线呢?

05

打包部署上线

1、打包

Flutter Web 的打包非常简单,运行:

flutter build web

即可。但这样显然是不够的,因为我们需要区分环境来打不通的包。 在上一章节我们配置了不同的入口文件,我们以dev环境为例,其入口文件是main_dev,那么我们的打包命令就变成了:

flutter build web -t lib/main_dev.dart

这行命令执行完成后,报错了,报错信息如下:

这是个图标数据加载问题,我们加上--no-tree-shake-icons即可。执行命令如下:

flutter build web -t lib/main_dev.dart --no-tree-shake-icons

然后我们就会在项目根目录的build文件夹下找到web这个文件夹,对应的就是web前端打出来的dist文件夹。包含了以下文件:

编译产物有了,那么如何部署呢?

2、部署

官方给了如下的部署方式:

https://flutter.cn/docs/deployment/web#deploying-to-the-web

看了官方文档后我发现,这三种部署方式并不适用于我们的项目。由于 CDN 具有提高网站性能和用户体验,减轻原始服务器的负载等优势,目前我们团队已经搭建了 CDN 部署平台。既然如此,我们的部署方案也需要往这方面靠。CDN 部署配置主要要解决的问题就是各种资源的路径问题。

(1)修改index.html的CDN资源路径

我先来简单说明一下FlutterWeb编译产物,如下图所示:

assets 包含了我们所有的静态资源文件:包括图片,字体文件等。 最重要是flutter.js和main.dart.js这两个文件。其中flutter.js为入口的js文件,我们可以打开web目录下index.html:

flutter_web//The value below is injected by flutter build,donot touch.var serviceWorkerVersion=null;-->window.addEventListener('load',function(ev){//Download main.dart.js_flutter.loader.loadEntrypoint({serviceWorker:{serviceWorkerVersion:serviceWorkerVersion,},onEntrypointLoaded:function(engineInitializer){engineInitializer.initializeEngine({}).then(function(appRunner){appRunner.runApp();});}});});

看到这行。而main.dart.js是我们的dart业务代码被编译成的js文件。flutter.js会加载main.dart.js和其它文件。默认情况下,flutter.js会加载各个文件,包括资源文件(assets)都使用的是相对路径。首先就是通过loadEntrypoint ()方法加载main.dart.js这个文件:

//flutter.jsasync loadEntrypoint(options){const{entrypointUrl=`${baseUri}main.dart.js`,onEntrypointLoaded}=options||{};returnthis._loadEntrypoint(entrypointUrl,onEntrypointLoaded);}

但我们发现貌似entrypointUrl是可以自己传递的,于是我们从官网文档里找到了自定义web应用初始化的链接: https://flutter.cn/docs/platform-integration/web/initialization 有如下的参数可传:

其中loadEntrypoint()方法可以传递entrypointUrl参数来指定main.dart.js的路径。而initializeEngine()方法可以通过传递assetBase参数来指定CDN资源路径。这么看来我们完全可以通过将这两个参数设置为绝对路径来解决main.dart.js的加载与CDN资源路径的问题。需要注意的是initializeEngine()方法是Flutter3.7.0开始才支持的。 我们改一下index.html:

window.addEventListener('load',function(ev){//Download main.dart.js_flutter.loader.loadEntrypoint({serviceWorker:{serviceWorkerVersion:serviceWorkerVersion,},entrypointUrl:"YOUR_CDN_ABSOLUTE_PATH/main.dart.js",onEntrypointLoaded:function(engineInitializer){engineInitializer.initializeEngine({assetBase:"YOUR_CDN_ABSOLUTE_PATH"}).then(function(appRunner){appRunner.runApp();});}});});

我们再打个包,还是会报错,找不到flutter.js,还是因为路径问题。处理方式更简单了,直接在index.html里配置成绝对路径即可。另外我们发现Icon-192.png,favicon.png,manifest.json这几个文件也是相对路径,那么我们一次性都改成绝对路径:

flutter_web//The value below is injected by flutter build,donot touch.var serviceWorkerVersion=null;

再打个包上传到CDN,嗯一切都正常了~ 到这里看上去都完美了,但突然想起来不对啊,我们是区分开发/测试/生产环境的,相应的CDN路径也是不同的。修改index.html的方式指定的都是绝对路径,不符合我们的需求啊。既然如此我们再改改。

(2)区分不同环境配置CDN路径

正常情况下,我们开发/测试/生产环境的host会映射到不同的CDN地址上。另外我们在本地调试的时候用的是本地资源,不需要配置CDN地址。那么我们的index.html修改如下:

moyu//The value below is injected by flutter build,donot touch.var serviceWorkerVersion=null;var YOUR_CDN_HOST="";//默认是本地调试,不需要配置cdn地址if(document.location.origin==YOUR_DEV_HOST){YOUR_CDN_HOST=YOUR_DEV_CDN_HOST;}elseif(document.location.origin==YOUR_TEST_HOST){YOUR_CDN_HOST=YOUR_TEST_CDN_HOST;}elseif(document.location.origin==YOUR_PRODUCT_HOST){YOUR_CDN_HOST=YOUR_PRODUCT_CDN_HOST;}//需要相应的element并配置其绝对路径document.getElementById("flutter_js").setAttribute("src",`${YOUR_CDN_HOST}flutter.js`);document.getElementById("manifest").href=`${YOUR_CDN_HOST}manifest.json`;document.getElementById("icon").href=`${YOUR_CDN_HOST}favicon.png`;document.getElementById("apple-touch-icon").href=`${YOUR_CDN_HOST}icons/Icon-192.png`;window.addEventListener('load',function(ev){//Download main.dart.jsif(YOUR_CDN_HOST==""){//本地调试_flutter.loader.loadEntrypoint().then(function(engineInitializer){returnengineInitializer.initializeEngine();}).then(function(appRunner){returnappRunner.runApp();});}else{//部署后_flutter.loader.loadEntrypoint({entrypointUrl:`${YOUR_CDN_HOST}main.dart.js`,}).then(function(engineInitializer){returnengineInitializer.initializeEngine({assetBase:`${YOUR_CDN_HOST}`});}).then(function(appRunner){returnappRunner.runApp();});}});
  • 1.首先根据当前域名document.location.origin的不同,区分不同环境下的CDN地址:YOUR_CDN_HOST。默认是是空,即本地调试情况,不需要配置CDN地址。

  • 2.为flutter.js,icons/Icon-192.png,favicon.png,manifest.json指定id,并通过document.getElementById()方法找到相应元素,为他们配置CDN的绝对路径。

  • 3.如上一章节所示,配置entrypointUrl与assetBase。

一切真正的完美了~到此为止,如果打包部署我们就讲完了。下一章节我要说明一下在开发过程中,遇到的一些意想不到的坑与相应的处理方式。

06

Flutter Web避坑指南

由于在实际项目中,我们是将一个现成的Flutter应用打包成 web版。原先的App已经支持了Android,iOS,Mac,Windows这四个平台。这一章节将针对实际项目中遇到的一些问题进行说明。包含如下几个问题:

  • 1.Dart中int和JS中Number的转换问题。

  • 2.导入特定平台依赖项。

  • 3.路由问题。

  • 4.iPhone手机Safari浏览器的侧滑返回问题。

  • 5.lottie问题。

  • 6.跨域问题。

接下来我会针对这几个问题一一进行说明。

1、Dart中int和JS中Number的转换

由于我们的项目是将一个线上的Flutter的App项目直接打包成web版,在运行的时候发现,我们发送的请求时常返回错误的数据,比如说:

我们请求了一个 feed 列表,然后点击某一个 item 进入详情页。

这时候列表都能正常的展示,但进入详情页服务端会报错:

不存在这个 feed。

通过跟服务端同学的沟通发现,出错的原因是在进入详情页请求feed详情时带的id错了。 这怎么会???id都是列表接口给的,web端也不会做任何处理进详情页直接带过去,而且线上App都是好好的也没有bug啊。 经过排查发现,id定义的是int类型,在Dart中,只有int和double这两种表达数字的数据类型,其中int的取值范围是-2^63 ~ 2^63 - 1,可以同等于Java中的Long。 在打包成web版式,Dart中的int会被编译成JS中的Number,问题就出在这儿了。Number的取值范围是-2^53 ~ 2^53 - 1。很不幸,我们模型中一些的id的取值范围大于2^53 - 1,从而转换成JS的Number后出错了。 原因找出来了,解决方法也显而易见了:这种可能会超出JS取值范围的字段,需要改成String类型。修改完后,这个问题顺利解决。

2、导入特定平台依赖项

在使用Flutter进行web端开发的时候,我们会经常使用dart:html这个库来实现一些功能。在仅仅打包web端时没问题,但由于我们的项目是跨平台的,打包App时就会出现以下问题:

是因为dart:html这个库只在web环境下能找得到,而编译App时并没有这个包,那也就意味着我们只能在web打包时使用dart:html这个库。解决方法如下:

import'dart:html'if(dart.library.io)'io_platform.dart'as platform;

在import的时候需要区分平台,dart.library.io意味着是在非web环境下(dart:io不支持web)。所以在非 web环境下我们import的是io_platform.dart这个文件。这时候我们有个疑问,非web环境下不引入dart:html不就好了么?为什么要引入另一个文件呢?原因是因为编译的时候还是会找相应的方法,我们没有引入任何库,导致相应的代码编译不过,所以我们自己创建了一个io_platform.dart文件,去实现相应的接口。当然由于这些方法不会被调用到,其实只是个空实现。 比方说我们现在用到了dart:html以下的方法和变量:

platform.window.navigator.userAgent;//navigator.userAgentplatform.window.location.origin;//location.originplatform.window.location.href;//location.hrefplatform.window.open(url,"");//open(String,String)

于是我们的io_platform.dart是这么实现的:

IoPlatformWindow get window=>IoPlatformWindow();class IoPlatformWindow{IoNavigator navigator=IoNavigator();IoLocation location=IoLocation();open(String url,String name){}}class IoNavigator{String userAgent="";}class IoLocation{String origin="";String href="";}

实际上只是为了解决编译的问题。如果大家有更好的方式解决这个问题请给我留言哈。接下来我们再来看路由问题。

3、路由问题

我们知道常规web端开发时,进行页面跳转传参是靠在url上拼参数,如:

YOUR_HOST_NAME/PATH" />但显然Flutter并不是这么传参的。比方说我们进入一个详情页,那么它的路由就是:YOUR_HOST_NAME/#detailPage,而参数并不可见。这样的话在我们刷新页面的时候,也拿不到参数自然会出现问题。 解决方法呢,比如说可以在LocalStorage里记录参数信息,然后做一个工具类去记录路由栈。但这也有问题,因为我们可以复制任意链接分享给别人,那么别人打开的时候本地没有记录自然也就无法正常打开页面。这种情况下甚至无法引导用户去首页。既然如此,那我们干脆处理成用户在刷新的时候,重新将网页指定到首页url。

voidregister(){if(platform.window.location.href!=platform.window.location.origin+"/"&&platform.window.location.href!=platform.window.location.origin+"/#/"){platform.window.location.href=platform.window.location.origin+"/";}}

在发现网页url不是首页的情况下,强制将href处理到首页。 然后在runApp(const MyApp());的MyApp控件的initState()方法中调用register()。 到这呢我们起码解决了分享出去一个链接,完全打不开页面的尴尬,好歹让用户看到首页了。接着我们想想办法带点儿参数进去。 在此呢我们可以用window.history.replaceState()为我们的url添加参数,且不会留下历史记录。这正是我们想要的,代码如下:

platform.window.history.replaceState({},"",newUrl);

那么接下来我们应该为url添加什么参数呢?由于 web版是App代码直接改造的,在首页会有很多初始化的处理,直接跳转至某些路由页面,即使带了参数页面也无法正常展示。这时候我想到了我们在App开发的时候常用的跳转协议:

在进行 App 开发的时候,我们会用去 scheme 处理一些的 Push 跳转或网页的跳转,封装成跳转协议。

而在 web我们可以添加跳转协议需要的参数,经过解析后封装成我们既有的跳转协议,低成本的完成页面跳转和加载仿佛是可行的。我们的跳转协议结构如下:

OUR_SCHEME/PATH?param1=1&param2=2

这么看就更简单了,我们将url拼上?param1=1&param2=2,在处理的时候,将?前的内容替换为OUR_SCHEME/PATH就直接将url替换成我们的跳转协议了。然后再调我们统一的协议处理方法即可。经过验证,效果如我们所替代的,完美的实现了刷新/分享链接的处理。

4、iPhone手机Safari浏览器的侧滑返回问题

在使用iPhone真机进行调试的时候,我们发现手势在真机设备的边缘进行侧滑返回的时候,会导致栈底的根页面也返回,并且导致整个Flutter应用重新加载,体验非常不好,如下图所示:

目前这个问题官方没有很好的解决方法,我们只能通过对flt-glass-pane标签(Flutter根布局对应的标签)增加touchstart 监听,对边缘处手势进行忽略。在index.html中增加如下代码:

_flutter.loader.loadEntrypoint({entrypointUrl:`${MOYU_HOST}main.dart.js`,}).then(function(engineInitializer){returnengineInitializer.initializeEngine({assetBase:`${MOYU_HOST}`});}).then(function(appRunner){returnappRunner.runApp();}).then(function(_){boundaryCheck();});functionboundaryCheck(){const flutterRoot=document.getElementsByTagName("flt-glass-pane").item(0);flutterRoot.addEventListener("touchstart",(e)=>{var pageX=e.targetTouches[0].pageX;if(pageX>24&&pageX<window.innerWidth-24)return;e.preventDefault();});}

在main.js.dart加载,Flutter引擎初始化完成后,调用boundaryCheck()方法进行手势位置边缘检测,如果在边缘处则调用preventDefault()方法,避免根部页面返回并重新加载。

5、lottie问题

由于我们的业务中使用了大量的 lottie 动画,在各端,包括 PC 端的浏览器上运行都没有问题。但在移动端真机上,部分 lottie 动画会导致崩溃。查其原因是因为在移动端真机上不支持 BlendMode.clear 模式,部分 lottie 动画由于支持了 BlendMode.clear 模式,导致出现问题。这个需要和 UI 同学进行沟通,更新/替换动画等。

6、跨域问题

跨域问题需要和服务端同学共同解决,都是现成的方案。当然如果是在本地调试阶段(也仅限于本地调试的情况),你也可以通过以下步骤解决跨域问题:

  • 1.前往flutter\bin\cache文件夹,删除flutter_tools.stamp文件。

  • 2.前往flutter\packages\flutter_tools\lib\src\web,打开chrome.dart文件。

  • 3.找到'--disable-extensions'这部分,在最下面添加'--disable-web-security',重新build即可。

07

总结

我们利用Flutter完成了一个web项目的开发,打包部署到CDN上,并最终上线。FlutterWeb虽然已经稳定了一段时间了,但是除非是有明确的跨端需求,并不推荐大家将它用在需要长期迭代,大而重的项目中。不过对于我们客户端开发来说,在拥有了Flutter的技能后,除去我们所熟悉的Android和iOS跨端开发,完全可以拓展自己的业务范畴,分摊一些合适的web端项目进行开发,为自己的团队增加更多的业务可能。 另外虽然Flutter Web确实还没那么完美,之前很多文章分享的延迟组件分包以减小main.dart.js大小的方式貌似也不可用了(官网明确说明是给Android的AAB来使用的)。但有总比没有强,将一个现成的App打包成 web版成本很低。毕竟重新开发一个web版的App功能工作量也是巨大的。目前继续等着Flutter的更新,看看未来会不会有更好的支持。