1.前言

首要阐明一下,这篇文章是给客户端开发同学看的(有Flutter根底最好)。Flutter的诞生尽管来自GoogleChrome团队,但咱们都知道Flutter最早支撑的渠道是AndroidiOS,至今最核心的维护渠道依然是AndroidiOSdart言语的学习本钱不高,Flutter的响应式UI与ComposeSwiftUI都有极大的相似之处,全体的架构思路也更倾向于客户端的方法,再加上为了完成许多硬件或Native相关的根底功用也需求专业的客户端开发知识,所以Flutter更多的是被客户端开发同学认可并运用(在咱们的团队中,Flutter现已是客户端开发同学的必备基本技术)。尽管Flutter最大的亮点便是跨端,但其实客户端和web端之间跨端由于差异性较大所以并不遍及,所以在此布景下,Flutter最初并不在web端上发力。但Flutter本身便是携带了web的基因啊,所以在Flutter2发布的时分终于发布了web的安稳版。
那么已然客户端和web端之间的跨端并不遍及,前端开发同学大约也不会运用Flutter进行web开发(的确没必要,包体积添加且有必定的功能丢失,还需求学习新言语与开发思路,原生开发不香么),Flutter Web到底有什么用呢?
带着这样的主意,在运用Flutter后的很长期都不曾调研过web端的支撑。但随着事务和内部需求的发展变化,Flutter Web的优势也逐步展现出来了。下面我来说一下运用Flutter Web首要的三个场景。

2.Flutter Web的运用场景

  • 1.客户端团队内部的web需求:在后疫情时代降本增效的大布景下,咱们会更多的运用自研工具。自研工具的运用和结果展现的可视化一般以网页的方法展现,尽管团队里有前端开发同学,但考虑到自研工具更多的是组内的尝试且与事务无关,自然不该让前端同学承担这部分作业。而客户端同学运用Flutter Web进行网页开发学习本钱低,完全能够快速的开发网页(本人在运用Vue结构进行web端开发时感受出客户端和前端的UI布局思路仍是有很大不同的,css很灵敏束缚性低,这个与客户端布局的强束缚性差异很大。对于没有Flutter根底的客户端开发来说,Flutter的学习本钱显然更低,开发时运用起来更顺手。对于全员把握Flutter技术的咱们团队来说现已是0本钱了)。
  • 2.不需长期维护的web事务需求web端承载了许多活动需求,这些需求的特点是时效性强,功用较简略,且不需长期维护。但这些需求经常是在某一时间段许多发生的(比如逢年过节的一些活动或榜单),或忽然发生的(比如蹭热点的即时需求)。这些作业的插入有时会导致一些长期迭代的web端需求需求延期,影响团队的全体排期。由于这些需求开发难度不大,功能要求不高,不需长期维护(意味着即使团队里不再有人运用FlutterFlutter Web有一天挂了也没什么影响),那么就特别合适分摊到客户端开发上。客户端开发同学参加进来后,平摊了一部分作业,以此来提升整个团队的效率。
  • 3.客户端与web端的跨端:个人认为这部分需求比较少。但万一有这种需求,那么咱们就能够节约许多人力资源去从头开发一套web端了。

好的已然有了需求,咱们就好好来走一下Flutter Web是怎样开发布置上线的流程。

3.Flutter Web工程的创立和事务完成

3.1.创立与运转

咱们运用Android Studio作为IDE,以Flutter 3.10.5版别为根底创立一个Flutter Web工程。
创立一个New Flutter Project,在挑选Platforms的时分只勾选Web,然后直接Create

Flutter Web从0到部署上线的实践

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

Flutter Web从0到部署上线的实践

假如想要run起来只需挑选chrome浏览器,点击run就行了:

Flutter Web从0到部署上线的实践

然后咱们就能够在浏览器看到运转结果了,当然咱们也能够翻开开发者方法便利查看与调试:

Flutter Web从0到部署上线的实践

这部分跑通后,十分恭喜你能够愉快的用Flutter开发网页了,接下来咱们完成一个事务需求:做一个网页查找功用。

Flutter Web从0到部署上线的实践

事务功用上的开发完成我就不做赘述了,能够告知做过Flutter开发的同学,没什么不同,根底装备/网络模块/数据同享/路由等该怎样封装就怎样封装,我也不过是直接拿了之前客户端Flutter工程相应模块的代码,稍作修正罢了。UI上的开发也是该怎样布局怎样布局,事务的开发体验上和客户端运用Flutter没什么不同。

3.2.调试

跑通后应该如何调试呢?
假如了解浏览器开发者方法,可直接运用浏览器进行调试,打logdebug都是没问题的,也能够看到源码,能够抓包:

Flutter Web从0到部署上线的实践

Flutter Web从0到部署上线的实践

Flutter Web从0到部署上线的实践

当然客户端同学或许不了解浏览器开发者方法,也没关系,运用Android Studio,之前在客户端写Flutter怎样调试,现在写web端仍旧能够怎样调试。

3.3.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.4.浏览器运转环境区别

客户端一般需求区别的是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.5.开发/测验/出产环境区别

同客户端一样,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.6.其他常用库或插件

关于数据同享/网络/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.打包布置上线

4.1.打包

Flutter Web的打包十分简略,运转:

flutter build web

即可。但这样显然是不行的,由于咱们需求区别环境来打不通的包。
在上一章节咱们装备了不同的进口文件,咱们以dev环境为例,其进口文件是main_dev,那么咱们的打包指令就变成了:

flutter build web -t lib/main_dev.dart

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

Flutter Web从0到部署上线的实践

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

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

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

Flutter Web从0到部署上线的实践

编译产品有了,那么如何布置呢?

4.2.布置

官方给了如下的布置方法:
flutter.cn/docs/deploy… 看了官方文档后我发现,这三种布置方法并不适用于咱们的项目。由于CDN具有提高网站功能和用户体验,减轻原始服务器的负载等优势,目前咱们团队现已搭建了CDN布置渠道。已然如此,咱们的布置计划也需求往这方面靠。

4.2.1.计划1——修正index.html

我先来简略阐明一下FlutterWeb编译产品,重点有两个: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="https://juejin.im/post/7253093577600630821/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="https://juejin.im/post/7253093577600630821/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运用初始化的链接:
flutter.cn/docs/platfo… 有如下的参数可传:

Flutter Web从0到部署上线的实践

Flutter Web从0到部署上线的实践

其间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的方法指定的都是绝对途径,不符合咱们的需求啊。经过调研,找到了另一种方法。

4.2.2计划2——–base-href

从头看index.html的代码,发现最上面注释:

  <!--
    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">

大约意思是,咱们能够在运用flutter build打包的运用经过--base-href参数指定base href的值。赶忙查看了一下base href的相关阐明:

base符号是一个基链接符号,是一个单符号。用以改变文件中全部连结符号的参数内定值。它只能运用于符号与之间。
你网页上的全部[相对途径]在链接时都将在前面加上基链接指向的地址。

已然如此,咱们就试试吧~
打包指令更新如下:

flutter build web -t lib/main_dev.dart –base-href YOUER_CDN_PATH –no-tree-shake-icons

需求留意的是YOUER_CDN_PATH并非绝对途径,而是去掉host的途径。比如你的绝对途径是:

cdn-path.com/your/busine… 那么你的YOUER_CDN_PATH应为:

/your/business/path/dev/

再打个包上传到CDN上,全部真正的完美了~

5.总结

咱们运用Flutter完成了一个web项目的开发,并且布置到CDN上。另外在web端还有一些常见的问题,比如说跨域问题,这些需求和服务端同学共同处理,都是现成的计划。FlutterWeb其实现已安稳了挺长期了,但由于运用场景不多所以并没有发展起来。但存在即合理,对于咱们客户端开发来说,在具有了Flutter的技术后,除去咱们所了解的AndroidiOS跨端开发,完全能够拓展自己的事务领域,进行部分的web端开发,为自己的团队添加更多的事务或许。