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

1.前言

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

  • 优势:
    1. 零学习成本:当你已经掌握了Flutter开发能力后,哪怕你对htmlcssJavaScript和主流的前端框架不那么了解,也不影响你开发web应用。
    2. 跨端能力:可将现有Flutter移动应用拓展到web,在多个平台共享代码,降低开发成本。
  • 劣势:
    1. 兼容性问题:使用html模式来进行渲染时,应用的大小相对较小但可能会出现兼容性问题。
    2. 包体积增加:使用canvaskit模式来进行渲染时,虽然性能较好,且可以降低不同浏览器渲染效果不一致的风险,但会增加包体积。

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

2.Flutter Web的使用场景

  • 1.客户端团队内部的web需求:在后疫情时代降本增效的大背景下,我们会更多的使用自研工具。自研工具的使用和结果展示的可视化通常以网页的形式展现。客户端同学使用Flutter Web进行网页开发学习成本低,完全可以快速的开发网页(本人在使用Vue框架进行web端开发时感受出客户端和前端的UI布局思路还是有很大不同的,css很灵活约束性低,这个与客户端布局的强约束性差异很大,所以对于客户端开发来说,使用Flutter开发网页应用时更顺手。对于全员掌握Flutter技能的我们团队来说已经是0成本了)。
  • 2.简单的web端业务需求web端承载了很多活动需求,这些需求的特点是时效性强,功能较简单,且不需长期维护。但这些需求经常是在某一时间段大量产生的(比如逢年过节的一些活动或榜单),或突然产生的(比如蹭热点的即时需求)。这些工作的插入有时会导致一些长期迭代的web端需求需要延期,影响团队的整体排期。由于这些需求开发难度不大,性能要求不高,不需长期维护(意味着即使团队里不再有人使用FlutterFlutter Web有一天挂了也没什么影响),那么就可以让Flutter开发同学加入进来,平摊了一部分工作,以此来提升整个团队的效率。
  • 3.客户端与web端的跨端:随着Flutter Web趋于稳定,用Flutter实现的App可以低成本的被打包成web版了,毕竟对于用户来说使用浏览器打开个网页比下载个App成本低多了。这种情况下我们就可以利用Flutter的跨端优势,节约很多人力资源,避免去重新开发一套web端了。

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

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

3.1.创建与运行

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

1688699928797.jpg

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

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

flutter create --platforms=web .

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

1688700684131.jpg

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

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

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

3.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.3.浏览器运行环境区分

客户端通常需要区分的是AndroidiOS这两个不同的运行环境,而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;
  }
}

3.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()就可以根据不同的环境做不同的配置了。

3.5.其他常用库或插件

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

  • shared_preferences
    在客户端开发的时候,我们知道如果需要对一些数据实现轻量级的本地序列化可以使用shared_preferences,其实现对应AndroidSharedPreferencesiOSNSUserDefaults。而在进行web开发的时候,我们知道如需在本地序列化一些数据的话,可以使用LocalStorage。其实Fluttershared_preferences插件也是支持web的,其实现也正是封装了LocalStorage。关于shared_preferences的使用也不做赘述了,已经非常熟悉了。
  • image_picker_for_web
    来自于我们熟悉的image_picker插件。根据浏览器的不同,支持或部分支持拍照/拍视频/读取图片/读取视频等。
  • js
    这个插件是用来使用注解的方式帮助你用Dart调用JavaScript API或用JavaScript调用Dart API的。

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

4.调试

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

4.1.PC端调试

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

1688709563331.jpg

1688709581159.jpg

1688709612744.jpg

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

4.2.移动端调试

企业微信截图_987fc59c-05c8-44e0-841b-48cd59f1a021.png

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

我们可以在手机浏览器上输入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浏览器需要科学上网,在此我们以iPhoneSafari浏览器+PC端的Safari浏览器为例:

  • 1.首先我们需要用数据线将手机和电脑连接起来。
  • 2.找到Safari开发菜单,找到你手机的名称,然后选择相应的地址,如下图所示:
    截屏2023-08-10 11.05.04.png
  • 3.然后我们就可以看到网页检查器进行调试了,如下图所示:


    截屏2023-08-10 11.07.02.png

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

5.打包部署上线

5.1.打包

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

flutter build web

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

flutter build web -t lib/main_dev.dart

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


1688715398852.jpg

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

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

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

1688713886562.jpg

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

5.2.部署

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

5.2.1. 修改index.html的CDN资源路径

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

截屏2023-08-10 10.07.23.png

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

<!DOCTYPE html>
<html>
<head>
  <!--
    If you are serving your web app in a path other than the root, change the
    href value below to reflect the base path you are serving from.

    The path provided below has to start and end with a slash "/" in order for
    it to work correctly.

    For more details:
    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base

    This is a placeholder for base href that will be replaced by the value of
    the `--base-href` argument provided to `flutter build`.
  -->
  <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.">

  <!-- iOS meta tags & icons -->
  <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">

  <!-- Favicon -->
  <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>
  <!-- This script adds the flutter initialization JS code --></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
有如下的参数可传:

1688716397910.jpg

截屏2023-07-07 15.53.54.png

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

<head>
  <!--
    If you are serving your web app in a path other than the root, change the
    href value below to reflect the base path you are serving from.

    The path provided below has to start and end with a slash "/" in order for
    it to work correctly.

    For more details:
    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base

    This is a placeholder for base href that will be replaced by the value of
    the `--base-href` argument provided to `flutter build`.
  -->
  <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.">

  <!-- iOS meta tags & icons -->
  <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">

  <!-- Favicon -->
  <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>
  <!-- This script adds the flutter initialization JS code -->
  <script src="YOUR_CDN_ABSOLUTE_PATH/flutter.js" defer></script>
</head>

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

5.2.2.区分不同环境配置CDN路径

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

<!DOCTYPE html>
<html>

<head>
  <!--
    If you are serving your web app in a path other than the root, change the
    href value below to reflect the base path you are serving from.

    The path provided below has to start and end with a slash "/" in order for
    it to work correctly.

    For more details:
    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base

    This is a placeholder for base href that will be replaced by the value of
    the `--base-href` argument provided to `flutter build`.
  -->
  <base id="href">

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

  <!-- iOS meta tags & icons -->
  <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">

  <!-- Favicon -->
  <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>
  <!-- This script adds the flutter initialization JS code -->
  <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.jsicons/Icon-192.pngfavicon.pngmanifest.json指定id,并通过document.getElementById()方法找到相应元素,为他们配置CDN的绝对路径。
  • 3.如上一章节所示,配置entrypointUrlassetBase

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

6.Flutter Web避坑指南

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

  • 1.DartintJSNumber的转换问题。
  • 2.导入特定平台依赖项。
  • 3.路由问题。
  • 4.iPhone手机Safari浏览器的侧滑返回问题。
  • 5.lottie问题。
  • 6.跨域问题。

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

6.1.Dart中int和JS中Number的转换

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

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

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

不存在这个feed

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

6.2.导入特定平台依赖项

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

截屏2023-08-04 11.49.51.png

是因为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 = "";
}

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

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

 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替换成我们的跳转协议了。然后再调我们统一的协议处理方法即可。经过验证,效果如我们所替代的,完美的实现了刷新/分享链接的处理。

6.4.iPhone手机Safari浏览器的侧滑返回问题

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


目前这个问题官方没有很好的解决方法,我们只能通过对 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()方法,避免根部页面返回并重新加载。

6.5.lottie问题

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

6.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即可。

7.总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容