Flutter-Web从0到部署上线(实践+埋坑)

Flutter 的诞生虽然来自 Google 的 Chrome 团队,但大家都知道 Flutter 最先支持的平台是 Android 和 iOS。不过由于 Flutter 本身就是携带了 web 的基因,在 Flutter2 发布的同时也发布了 web 的稳定版。那么它有什么优势和劣势呢?

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","");

//监听event
window.addEventListener("mousedown", (event) => {
     //do something
});

另外 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() {
    return RegExp(
        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() {
    return RegExp(r'\(i[^;]+;( U;)? CPU.+Mac OS X').hasMatch(ua);
  }

  //Android端
  isAndroid() {
    var isAndroid = ua.contains("Android") || ua.contains("Adr");
    return isAndroid;
  }

  //微信环境
  isWechat() {
    return ua.contains("MicroMessenger");
  }

  //微信小程序环境
  isMiniprogram() {
    if (ua.contains("micromessenger")) {
      //微信环境下
      if (ua.contains("miniprogram")) {
        //小程序;
        return true;
      }
    }
    return false;
  }
}

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

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

  • main_dev.dart
void main() {
  AppConfig.init(ConfigType.dev);
  root_main.main();
}
  • main_test.dart
void main() {
  AppConfig.init(ConfigType.test);
  root_main.main();
}
  • main_online.dart
void main() {
  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:

<!DOCTYPE html>
<html>
<head>

  <base href="$FLUTTER_BASE_HREF">

  <meta charset="UTF-8">
  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
  <meta name="description" content="A new Flutter project.">


  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="flutter_web">
  <link rel="apple-touch-icon" href="icons/Icon-192.png">


  <link rel="icon" type="image/png" href="favicon.png"/>

  <title>flutter_web</title>
  <link rel="manifest" href="manifest.json">
  <script>
    // The value below is injected by flutter build, do not touch.
    var serviceWorkerVersion = null;
  </script>
  </script>-->
  <script src="flutter.js" defer></script>
</head>
<body>
  <script>
    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();
          });
        }
      });
    });
  </script>
</body>
</html>

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

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

      return this._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 这几个文件也是相对路径,那么我们一次性都改成绝对路径:

<head>

  <base href="$FLUTTER_BASE_HREF">

  <meta charset="UTF-8">
  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
  <meta name="description" content="A new Flutter project.">


  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="flutter_web">
  <link rel="apple-touch-icon" href="YOUR_CDN_ABSOLUTE_PATH/icons/Icon-192.png">


  <link rel="icon" type="image/png" href="YOUR_CDN_ABSOLUTE_PATH/favicon.png"/>

  <title>flutter_web</title>
  <link rel="manifest" href="YOUR_CDN_ABSOLUTE_PATH/manifest.json">
  <script>
    // The value below is injected by flutter build, do not touch.
    var serviceWorkerVersion = null;
  </script>

  <script src="YOUR_CDN_ABSOLUTE_PATH/flutter.js" defer></script>
</head>

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

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

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

<!DOCTYPE html>
<html>

<head>

  <base id="href">

  <meta charset="UTF-8">
  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
  <meta name="description" content="摸鱼kik.">


  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="moyu">
  <link id="apple-touch-icon" rel="apple-touch-icon" href="icons/Icon-192.png">


  <link id="icon" rel="icon" type="image/png" href="favicon.png" />

  <title>moyu</title>
  <link id="manifest" rel="manifest" href="manifest.json">

  <script>
    // The value below is injected by flutter build, do not touch.
    var serviceWorkerVersion = null;
  </script>

  <script id="flutter_js" defer></script>
</head>

<body>
  <script>

    var YOUR_CDN_HOST = ""; //默认是本地调试,不需要配置cdn地址
    if (document.location.origin == YOUR_DEV_HOST) {
      YOUR_CDN_HOST = YOUR_DEV_CDN_HOST;
    } else if (document.location.origin == YOUR_TEST_HOST) {
      YOUR_CDN_HOST = YOUR_TEST_CDN_HOST;
    } else if (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.js
      if (YOUR_CDN_HOST == "") {
        //本地调试
        _flutter.loader.loadEntrypoint().then(function (engineInitializer) {
          return engineInitializer.initializeEngine();
        }).then(function (appRunner) {
          return appRunner.runApp();
        });
      } else {
        //部署后
        _flutter.loader.loadEntrypoint({
          entrypointUrl: `${YOUR_CDN_HOST}main.dart.js`,
        }).then(function (engineInitializer) {
          return engineInitializer.initializeEngine({
            assetBase: `${YOUR_CDN_HOST}`
          });
        }).then(function (appRunner) {
          return appRunner.runApp();
        });
      }

    });

  </script>
</body>

</html>
  • 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.userAgent
platform.window.location.origin; //location.origin
platform.window.location.href; //location.href
platform.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?feedId=123

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

void register() {
    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 添加参数,且不会留下历史记录。这正是我们想要的,代码如下:

<pre style="font-size: 16px;margin-top: 10px;margin-bottom: 10px;overflow: auto;border-radius: 5px;box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;text-align: left;color: rgb(0, 0, 0);font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace;">```
 platform.window.history.replaceState({}, <span style="color: rgb(152, 195, 121);line-height: 26px;">"", newUrl);<br></br>

那么接下来我们应该为 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 应用重新加载,体验非常不好,如下图所示:

![](https://oss-cn-hangzhou.aliyuncs.com/codingsky/cdn/img/2024-01-11/99e45a87bf06c8ae6b6e6f382d86d8f6)

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

_flutter.loader.loadEntrypoint({ entrypointUrl: ${MOYU_HOST}main.dart.js, }).then(function (engineInitializer) { return engineInitializer.initializeEngine({ assetBase: ${MOYU_HOST} }); }).then(function (appRunner) { return appRunner.runApp(); }).then(function (_) { boundaryCheck(); });

function boundaryCheck() {
  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 的更新,看看未来会不会有更好的支持。

手机扫码阅读