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

4056e1e58c227ae12b050ebdad0f3c3c.jpeg

e949cc621f9d67ed8bd4158f559fd38d.gif

本文字数:7743

预计阅读时间:60分钟

f630ec1795bc6fc3fbd2abcfc9bbd819.png

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。

ab53e878966d92a84fffc18ed7a0afc8.jpeg

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

ba6c2ce691fa09f3e92d237a47968486.jpeg

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

flutter create --platforms=web .

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

bc363581eb377c46ef797324db1a15b6.jpeg

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

ccb0ac3af6f7a7a00d075bd073b8343a.jpeg

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

331a132ef5dba820795b62519e066072.jpeg

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

b8e2c034c55fcb580a43e8d4324c2c25.jpeg b26d71f5410a27d2b939d67217d2319d.jpeg 4b810680732197a929d4c80fd4240cb2.jpeg

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

2、移动端调试

92041a70f36b467636da6b219b7e352d.jpeg

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

5a84c9d9a32d29ee111e21da6ae47baf.jpeg

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

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

    e5b37da60fc5098c9722bd8bb979bc24.jpeg

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

05

打包部署上线

1、打包

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

flutter build web

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

flutter build web -t lib/main_dev.dart

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

cf70784dd9d80c960a378ad3f93d50a8.jpeg

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

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

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

9c84e7154b3de9f0e5da7770b3d477c3.jpeg

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

2、部署

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

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

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

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

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

728f4cb024a6ca5a9bf7c95efef5636d.jpeg

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

b83f56e435db5974e852592786b95356.jpeg 25dcab37335751f64ccbbf92750b90e3.jpeg

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

b2c0500ecadda2360d3dba2195a2500d.jpeg

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

99e45a87bf06c8ae6b6e6f382d86d8f6.jpeg

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/618803.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PHP在线文档管理系统源码

PHP在线文档管理系统源码 系统功能与介绍 在数据持续、快速增长背景下&#xff0c;企业面临海量非结构化数据处理需求&#xff0c;企业现有架构 通常无法应对海量非结构化数据的管理与应用。 支持私有化部署&#xff0c;完全内网环境下也可正常使用。 Windows、Linux、Mac等全平…

7个向量数据库对比:Milvus、Pinecone、Vespa、Weaviate、Vald、GSI 和 Qdrant

本文简要总结了当今市场上正在积极开发的7个向量数据库&#xff0c;Milvus、Pinecone、Vespa、Weaviate、Vald、GSI 和 Qdrant 的详细比较。 我们已经接近在搜索引擎体验的基础层面上涉及机器学习&#xff1a;在多维多模态空间中编码对象。这与传统的关键字查找不同&#xff08…

通过代理连接sftp

通过nginx代理连接sftp 1.问题描述2.代码实现3.nginx配置3.1 创建sftp.stream文件3.2 修改nginx配置 4.重启nginx生效 1.问题描述 问题是这样的。我们现在需要在微服务所在内网的A机器连接到外网的sftp&#xff0c;但是网络又不能直接到达。然后A机器到B机器是通过的&#xff…

【SAP】如何删除控制范围

经历就是财富&#xff0c;可你终将遗忘。期望文字打败时间。 本周心惊胆战地在配置系统删除了一个控制范围&#xff0c;还是有些收获&#xff0c;特此记录一下。 背景&#xff1a;在删除控制范围之前&#xff0c;我主要做了如下配置。 定义控制范围&#xff08;自动生成了成本…

【UEFI基础】EDK网络框架(IP4)

IP4 IP4协议说明 IP全称Internet Protocol&#xff0c;它属于网络层&#xff0c;对其下各种类型的数据链路层进行了包装&#xff0c;这样网络层可以跨越不同的数据链路&#xff0c;即使是在不同的数据链路上也能实现两端节点之间的数据包传输。 IP层的主要作用就是“实现终端…

C++|19.C++类与结构体对比

类和结构体 类和结构体本质上并没有太大区别。 但两者在默认上有所区别。 类默认成员变量是私有的&#xff0c;而结构体默认成员变量是公有的。 也就是说&#xff0c;对于一个类来说&#xff0c;会默认使用private去保护其内部成员变量使得无法直接访问到其内部的变量。 同时从…

代码随想录算法训练营第27天 | 39. 组合总和 40.组合总和II 131.分割回文串

目录 39. 组合总和 &#x1f4a1;解题思路 &#x1f4bb;实现代码 40.组合总和II &#x1f4a1;解题思路 &#x1f4bb;实现代码 131.分割回文串 &#x1f4a1;解题思路 # 判断回文子串 &#x1f4bb;实现代码 39. 组合总和 题目链接&#xff1a;39. 组合总和 给定…

C++ 开发 + VSCode 调试

C 开发 VSCode 调试 MSYS2 安装 gcc、make下载安装MSMYS2pacman 添加镜像源 GCC1. 安装2. 查看结果3. 环境变量 GDB VSCode 调试所需插件创建项目调试代码1. tasks.json 配置任务2. launch.json 配置调试3. 运行 更进一步的 C/C 设置 参考资料 MSYS2 安装 gcc、make 下载 官…

UCB Data100:数据科学的原理和技巧:第二十一章到第二十六章

二十一、SQL II 原文&#xff1a;SQL II 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 学习成果 介绍过滤组的能力 在 SQL 中执行数据清理和文本操作 跨表连接数据 在本讲座中&#xff0c;我们将继续上次的工作&#xff0c;介绍一些高级的 SQL 语法。 首先&…

解锁营销新高度:幽灵鲨CRM推广平台线索对接功能详解

数字营销时代&#xff0c;线索对接是推动业务增长的关键。你是否为线索分布在不同的平台而来回切换&#xff1f;你是否为无法及时联系客户而错失商机&#xff1f;幽灵鲨CRM系统作为一款领先的客户关系管理解决方案&#xff0c;不仅实现了对主流推广平台的全面对接&#xff0c;更…

C++内存分配策略

目录 基础概念 内存布局 分配方式 实现 1.new和delete 2.利用空间配置器alloc 3.用malloc和free 4.静态内存分配 基础概念 在讲内存分配之前&#xff0c;先对一些基础概念进行阐述&#xff0c;以便能更好的讨论问题 内存布局 代码编译为可执行程序后运行占用的内存可…

了解统计分类中的贝叶斯理论误差限

一、介绍 统计分类和机器学习领域正在不断发展&#xff0c;努力提高预测模型的准确性和效率。这些进步的核心在于一个基本基准&#xff0c;即贝叶斯理论误差极限。这个概念深深植根于概率和统计学&#xff0c;是理解分类算法的局限性和潜力的基石。本文深入探讨了贝叶斯错误率的…

【LabVIEW FPGA入门】使用LabVIEW FPGA进行编程并进行编译

在本文中会进行一个简单的FPGA编程演示&#xff0c;这通常可以验证编译工具链是否正常使用。在LabVIEW FPGA中和rt、PC编程一样使用数据流编程&#xff0c;但是需要注意的是FPGA中有些函数是不可以用的&#xff0c;因为这些函数很占用资源&#xff0c;且FPGA只能同时下载运行一…

AI软件开发:探索原理、挑战与未来趋势

AI软件开发已经成为当前最热门和具有前景的技术领域之一。随着人工智能技术的快速发展&#xff0c;AI软件的应用范围也在不断扩大。本文将主要探讨AI软件开发的原理、挑战以及未来的趋势。 首先&#xff0c;AI软件开发的原理是基于机器学习和深度学习算法。机器学习是一种通过…

Jetbrains ai assistant激活成功了

使用ai assistant插件助手 很完美&#xff0c;第一次用在idea 开发工具就完美的把激活了&#xff0c;你也不妨试试 链接地址&#xff1a;https://web.52shizhan.cn 激活后如下 登录页面 完美使用

python设计模式有哪几种

Python 中常见的设计模式有以下几种 一 单例模式&#xff08;Singleton Pattern&#xff09;&#xff1a;确保一个类只有一个实例&#xff0c;并提供全局访问点。 二 工厂模式&#xff08;Factory Pattern&#xff09;&#xff1a;使用工厂方法来创建对象&#xff0c;而不是直…

c++算法之枚举

目录 解空间的类型 循环枚举解空间 例题 特别数的和 输入格式 输出格式 输入样例&#xff1a; 输出样例&#xff1a; 解 例题 反倍数 问题描述 输入格式 输出格式 样例输入 样例输出 解 例题 找到最多的数 解 枚举算法是一种基本的算法思想&#xff0c;它通过…

喜报 | 联诚发斩获卓越品牌奖,总裁龙平芳荣获优秀企业家奖

1月10日&#xff0c;深圳市半导体产业发展促进会五届三次会员大会暨五届四次理/监事会在深圳宝安登喜路国际大酒店隆重举行&#xff0c;全体会员、党员、兄弟商协会等共600多位代表出席会议。联诚发LCF作为协会会长单位也受邀出席盛会。 本次大会颁布了卓越品牌奖、优秀企业奖、…

79LXX 三端负电源电压调节器,具有一系列固定电压输出,适用于小于100mA电源供给的场合

79LXX系列三端负电源电压调节器是单片双极型线性集成电路&#xff0c;采用TO92、SOT89-3的封装形式封装&#xff0c;有一系列固定的电压输出&#xff0c;适用于小于100mA电源供给的场合。 主要特点&#xff1a; 最大输出电流为100mA 固定输出电压分别为-5V、-6V、-8V、-9V、-1…

QT第三天

使用QT完成水果计价界面和功能&#xff0c;如下图&#xff1a; 运行结果&#xff1a; 代码&#xff1a; widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QListWidgetItem>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_N…