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

共 29886字,需浏览 60分钟

 ·

2024-04-12 02:58

182d1b8cea8b0f1341e8107543dbc438.webp

e9306f41e63266a8d5d2e1e65af04542.webp

本文字数: 7743

预计阅读时间: 60 分钟

e179ebfcfd932ea2610bc80de4c92d7e.webp

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

bb9de9312e3c6177e35beec6686c9064.webp

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

51a7f1ad06ed3be724362a8ef03f506b.webp

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

flutter create --platforms=web .

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

89555f57415de49cada880afacd0d8c1.webp

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

510bd2c34d2b637b22a37e0d1517ee40.webp

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

52f485008cc5059e6b5a1b6fbd806613.webp

业务功能上的开发实现我就不做赘述了,可以告诉做过  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   都是没问题的,也可以看到源码,可以抓包:

8f8cdc722b72bca4c3258f361d745660.webp ba32bc1daa47844faaa7483aa274e844.webp 1a744df97b52a82b7b5f2373b544114f.webp

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

2、移动端调试
918bb692d4f28939756420858ecfd3c8.webp

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

5a508247219ca32cb75c867437259b89.webp

我们可以在手机浏览器上输入  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   的  开发   菜单,找到你手机的名称,然后选择相应的地址,如下图所示:

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

    351b27c086978af9f6a2ea0ed6001f74.webp

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

05

打包部署上线

1、打包

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

flutter build web

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

flutter build web -t lib/main_dev.dart

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

3a5867295d83d1ecc45cdc871245161c.webp

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

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

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

f8805cf28f084fe202b432a37a376ac7.webp

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

2、部署

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

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

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

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





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

3df107cf1b56b1cbbb9cd3fa0fe354a7.webp

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 有如下的参数可传:

88055fc09f3bd219fe023c471a2d10f5.webp 4556e67231ce5f3e34cea5b2431edb4f.webp


其中  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   时就会出现以下问题:

cdc6147f18cb3891263bf3afde167c99.webp

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

                   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   应用重新加载,体验非常不好,如下图所示:

77da971108a7b650de29642c48a17500.webp

目前这个问题官方没有很好的解决方法,我们只能通过对  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   的更新,看看未来会不会有更好的支持。

浏览 20
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报