无影Flutter for Web技术预研

本文涉及的产品
无影云电脑企业版,4核8GB 120小时 1个月
无影云电脑个人版,1个月黄金款+200核时
简介: ## 介绍[Flutter](https://flutter.dev/)是Google推出并[开源](https://github.com/flutter)的跨平台开发框架,它采用Skia渲染并兼容了Android、iOS、Mac、Windows、Linux及Web,Flutter在2.0版本正式发布了对Web的支持![](https://ata2-img.oss-cn-zhangjiak

介绍

Flutter是Google推出并开源的跨平台开发框架,它采用Skia渲染并兼容了Android、iOS、Mac、Windows、Linux及Web,Flutter在2.0版本正式发布了对Web的支持

Flutter使用Dart开发,Dart本身能通过dart2js将Dart语言转成JavaScript。在Flutter中分为框架层和引擎层。框架层提供了布局渲染更新方式和手势等抽象能力,还提供了常用的组件。引擎层提供了平台差异的抽象同时也抹平了不同平台差异。Flutter for Web通过映射web平台API的代码来取代移动应用所使用的底层C++渲染引擎。

无影本身是一个支持多端的产品,目前支持MacOS、Windows、零终端(Linux)、iOS、Android及Web端,目前PC三端都是通过同一个Flutter工程输出,iOS和Android因为逻辑与UI差异很大,没有使用跟PC端同一工程来开发,但是部分功能如登录逻辑也是通过与PC端引入同一个库的形式集成的。Web端一直采用的是纯Web开发,但目前无影的产品功能迭代很快,经常一个版本UI及功能都有重大变化,目前两端使用不同技术开发给我们开发周期及功能同步带来了很大的挑战。在我们Flutter项目将Flutter引擎升级到3.0.2的时机,提出了将我们Flutter项目转Web的需求,于是有了这次技术预研。

现有网站案例

使用

环境

Flutter: 3.0.2
MacOS: 12.0.1

确保Flutter版本在2.0及以上,使用下面命令创建项目,默认会生成支持Web的项目结构

flutter create {PROJECTNAME}

如果想要对已有项目的支持,只需要控制台进入项目,执行

flutter create --platforms web .

想要知道项目是否支持Web,可以查看项目文件夹是否包含web文件夹,其默认生成的结构如下

web
├─ favicon.png 
├─ icons
│    ├─ Icon-192.png
│    ├─ Icon-512.png
│    ├─ Icon-maskable-192.png
│    └─ Icon-maskable-512.png
├─ index.html  入口文件,通过引入编译后的JS渲染页面
└─ manifest.json  配置PWA
注:icons里的图片是与PWA配合使用,当用户将项目安装到本地时( PWA),该图标会被当做启动图标使用

然后我们就可以在控制台输入下面命令启动项目了

flutter run -d chrome

两种编译模式

flutter提供了两种编译模式,分别适用于开发环境和生产环境。

  • flutterdev: 一种支持增量开发编译模式,可实现代码快速生效。当我们使用flutter run启动项目时就是使用的该方式,它可像开发客户端应用一样支持hot reloadhot restart
  • dart2js: 它是一个优化的编译器,可以将Dart代码编译为快速、紧凑的JavaScript代码,可以极大提高代码的包大小及运行效率。

渲染模式

Flutter for Web提供了两种渲染模式,HTML和CanvasKit,我们在编译的时候可以选择不同的编译模式

  • auto(默认)自动选择要使用的渲染器。当应用程序在移动浏览器中运行时,此选项选择HTML渲染器,当应用程序在桌面浏览器中运行时,采用CanvasKit渲染器。
  • html 使用HTML渲染器。使用HTML元素、CSS、Canvas元素和SVG元素的组合来渲染。此渲染方式采用的包大小更小。
  • canvaskit 使用CanvasKit渲染器。使用WebAssemblyWebGL渲染,将得到与桌面端渲染的一致性,且相比HTML渲染有更高的性能。但是它相比HTML渲染会多7MB左右的包大小(当前使用canvaskit0.33.0)。

我们可以通过编译时传入参数来指定渲染模式

flutter run -d chrome --web-renderer html
flutter build web --web-renderer canvaskit

或者在index.html中注入JavaScript的方式来指定渲染模式(只有编译时选择了auto才生效)

<script type="text/javascript">
  window.flutterWebRenderer = "html"; //or
  // window.flutterWebRenderer = "canvaskit";
</script>

html模式相比canvaskit除了渲染性能更低,还有一些其它问题

  • 不支持Image.toByteData
  • 不支持OffsetLayer.toImageScene.toImage
  • 无法访问动画中的帧数据(Codec.getNextFrame,frameCount始终为1,repetitionCount始终为0)
  • 不支持ImageShader
  • 图像上使用shader功能支持有限
  • 图片无法控制内存,dispose回调不会执行(图片内存管理都被浏览器接管了)

CanvasKit是一种采用Skia开发然后通过WebAssemblyWebGL渲染的技术。我们引入canvaskit.jscanvaskit.wasm包后,就可以通过JavaScript使用Skia API来绘制页面,这里有一个简单的例子。

PWA

Flutter转成Web后会默认会把项目编译成一个PWA项目(学习资料),它会提供一个PWA WEB清单文件并生成一个flutter_server_worker.js文件,Service Workers对Flutter内容进行缓存,当第一次加载完成后,再次请求资源下载都会走缓存。

当我们在Chrome中打开一个PWA项目时,浏览器url右边会出现一个下载按钮,下载后Web应用会被当成一个类似客户端应用来使用。

这就相当于一个简化版的无影APP客户端。

Flutter默认启动了PWA,我们可以编辑manifest.json文件来更改项目配置,具体可以查看Manifest来学习如何配置。

当然如果我们不想使用PWA,可以在编译时添加--pwa-strategy=none命令来禁止使用它。

平台兼容

由于Web平台和MacOS、Windows、Linux平台差异很大,所以很多功能需要针对平台进行改造

通过kIsWeb判断是否在Web平台

跟在客户端使用Platform.is*不一样,Flutter是通过kIsWeb来识别是否在Flutter平台,它使用一种巧妙的方式通过比较0与0.0是否同一个值类型来实现的,因为在JavaScript中不区分double和int,而dart中是需要区分。当我们使用kIsWeb时就像使用Platform.is*一样,如

String getPlatformName() {
  if (kIsWeb) {
    return 'Flutter Web';
  } else if (Platform.isMacOS) {
    return 'MacOS';
  }
}

在Web环境中,使用Platform会在运行时报错导致代码执行中断(issue),如果项目要兼容Web平台,需要将项目中的Platform.is*代码都改成兼容代码。你可以自己封装一个类来兼容,如

import 'package:flutter/foundation.dart';
class MYPlatform {
  static final bool isMacOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.macOS;
  static final bool isAndroid = !kIsWeb && defaultTargetPlatform == TargetPlatform.android;
  static final bool isIOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS;
  static final bool isFuchsia = !kIsWeb && defaultTargetPlatform == TargetPlatform.fuchsia;
  static final bool isWindows = !kIsWeb && defaultTargetPlatform == TargetPlatform.windows;
  static final bool isLinux = !kIsWeb && defaultTargetPlatform == TargetPlatform.linux;
  static final bool isWeb = kIsWeb;
}

使用MYPlatform.is*来全局替换Platform.is*

或者可以引入一个三方库universal_platform,然后使用UniversalPlatform.is*来替换Platform.is*

顺便提一下,在客户端工程中引入dart:html也会报找不到的错误,我们同样可以引入universal_html解决。

有条件导入与导出文件

Flutter提供了导入导出时通过条件判断来支持不同平台引入不同实现。官方文档

import 'package:flutter3_demo/ffi/show_image_platform_interface.dart'
    if (dart.library.io) 'show_image_io.dart'
    if (dart.library.html) 'show_image_web.dart';

通过这样的代码在静态编译时会默认引入show_image_platform_interface.dart代码,当在开发或者打包编译时,如果是Native项目,会引入show_image_io.dart文件,如果是Web项目,会引入show_image_web.dart文件。需要注意的是,我们需要在show_image_io.dartshow_image_web.dart拥有相同名称的类与方法或者变量。

这种方式是通过检查dart:*库是否存在实现的,比如在客户端应用程序中会导入dart:io库,所以相应的上面会导入show_image_io.dart,Web程序中会导入dart:html,所以会导入show_image_web.dart文件。

library的导入规则(部分库在Flutter中不会被引入,所以没列出来)

  • Core: dart:core, dart:async, dart:collection, dart:convert, dart:developer, dart:math, dart:typed_data
  • Native Platform: dart:ffi, dart:io, dart:isolate
  • Web: dart:html, dart:js, dar:js_util, package:js

Federated plugin

官方文档

Flutter提供了一种新的插件开发方式Federated plugin,我们可以在插件的pubspec.yaml文件中指定对应的平台实现

flutter:
  plugin:
    platforms:
      android:
        package: com.example.hello
        pluginClass: HelloPlugin
      ios:
        pluginClass: HelloPlugin
      macos:
        pluginClass: HelloPlugin
      web:
        pluginClass: HelloPlugin
        fileName: hello_web.dart

environment:
  sdk: ">=2.1.0 <3.0.0"
  flutter: ">=1.12.0"

例如上面例子,我们可以针对Web平台单独实现hello_web.dart,在hello_web.dart中,我们需要对插件进行注册。

class HelloPluginWindows extends HelloPluginPlatform {
  static void registerWith() {
    HelloPluginPlatform.instance = HelloPluginWindows();
  }
  //...
}

更多例子可以参考官方插件url_launcher的实现。

Dart与JavaScript互调

虽然现在Flutter生态很好,有很多三方库可以使用,但是相比Web生态还是差太多了,在使用Flutter开发时能够与JavaScript的函数互调是一个比较重要的能力。Flutter提供了dart:js_utilsdart:js工具来与Web端的JavaScript互相调用。

Dart调用JavaScript

在web文件夹下新建hello.js

let i = 0;
// 同步函数
window.bindHello = (args) => {
  alert(`Hello ${args}`);
  return i++ % 2 == 0;
}
// 异步函数
window.bindHelloAsync = (args) => {
  return new Promise((resolve) => {
    alert(`Hello ${args}`);
    resolve(i++ % 2 == 0);
  })
}

上面创建了一个同步函数和异步函数,分别演示Dart调用JavaScript的同步函数和异步函数方式。我们需要把函数绑定到window上,Dart才能通过window拿到函数(这里没找到不绑定到window上就能调通方案,如果有人知道可以指导一下)。

index.html中引入hello.js

<script src="hello.js" defer></script>

然后在Dart中分别调用同步函数和异步函数

import 'dart:html' as html;
import 'dart:js' as js;
import 'dart:js_util' as js_util;
class CallJS {
  static bool callHello() {
    bool result = js.context.callMethod('bindHello', ['Flutter Web Sync']);
    return result;
  }

  static Future<bool> callHelloAsync() async {
    var result = js_util
        .callMethod(html.window, 'bindHelloAsync', ['Flutter Web Async']);
    bool returnObj = await js_util.promiseToFuture<bool>(result);
    return returnObj;
  }
}

Dart调用JavaScript有两种方式,可以通过js.context.callMethod(Object method, [List<dynamic>? args])或者js_util.callMethod(html.window,Object method, [List<dynamic>? args])js.context在Web端相当于window

当我们调用callHello时,就能拿到JavaScript执行后的返回结果,调用callHelloAsync时能拿到JavaScript异步的执行结果

JavaScript调用Dart

同步

可以通过js_util.setProperty来提供JavaScript调用Dart能力,例如

import 'dart:html' as html;
import 'dart:js' as js;
import 'dart:js_util' as js_util;
void bindJS() {
  js_util.setProperty(html.window, "callHello", js.allowInterop((args) {
        return '$args from dart';
      }));
}

我们可以通过setProperty第三个参数可以是值类型、数组(js.JsArray)、对象(js.JsObject)等,我们可以通过使用js.allowInterop将Dart函数转成JavaScript函数。

然后在JavaScript中调用

window.callHello('Flutter');

异步

Flutter并没有提供异步JavaScript调用Dart的方式,但我们可以通过使用DartJavaScript的能力间接达到异步能力。

Dart代码:

js_util.setProperty(html.window, "callHelloAsync",
        js.allowInterop((returnName, arg) async {
    // do some thing async
    console.log(arg); // 接收参数
    await Future.delayed(Duration(milliseconds: 1000));
    js.context.callMethod(returnName, ['Result from Dart']);
}));

JavaScript代码

function callDartAsync() {
  return new Promise((resolve) => {
    window.callHelloAsync('callResult', 'hello');
    window.callResult = (args) => {
      resolve(args);
      window.callResult = null;
    }
  })
}

这样JavaScript调用callDartAsync函数就能拿到Dart异步执行后返回的结果了。

首屏加载

资源下载

当Flutter项目转成web后有几个文件比较大

  1. main.dart.js 2.0+ MB
  2. canvaskit.wasm 7.0 MB
  3. MaterialIcons-Regular.otf 等字体或图标文件

Flutter提供了一系列JavaScript API来控制整个资源下载及加载过程,在index.html默认会生成这样的函数调用

window.addEventListener('load', function (ev) {
    // flutter.js加载完成,开始下载main.dart.js资源
    _flutter.loader.loadEntrypoint({
      serviceWorker: {
          serviceWorkerVersion: serviceWorkerVersion,
      }
    }).then(function (engineInitializer) {
      // 加载main.dart.js,下载canvaskit.js canvaskit.wasm 及字体资源
      return engineInitializer.initializeEngine();
    }).then(function (appRunner) {
      // 启动引擎,渲染界面
      return appRunner.runApp();
    }).then(() => {
      // load end
    });
});

当index.html开始加载到第一帧显示,Flutter提供了一系列加载函数

  • window.addEventListener('load'): index.html中的flutter.js文件下载成功并加载
  • loadEntrypoint: 下载favicon.png、main.dart.js、manifest.json等文件
  • initializeEngine: 加载main.dart.js,下载canvaskit.js、canvaskit.wasm、FontManifest.json、MaterialIcons-Regular.otf等项目中使用的资源
  • runApp: 运行app

我针对该过程测试了大致的时间消耗测试

网速 load(s) loadEntrypoint(s) initializeEngine(s) runApp(s)
4G(4Mb/s) 0.22 9.96 31.346 31.447
WIFI(30Mb/s) 0.12 2.115 6.645 6.645
比例 5% 26% 67% 2%

这样,我们就可以根据上面的Web资源加载函数配合上表的每一段函数执行时间比例做一个首屏资源加载进度条来提高用户体验。如果在我们Flutter Web页面前面还有其它Web页面,也可以利用这些API对资源进行预加载。

拆分文件

整个项目都打包到一个文件中,会使首次下载文件变大,Flutter提供了一些方式来拆分文件,只有在加载对应的页面时才下载对应文件,相关资料

代码示例:

import 'mywidget.dart' deferred as foo
final Future<void> loadedLibrary = foo.loadLibrary();
Widget build(BuildContext context) {
  return FutureBuilder(future: loadedLibrary, builder:(context, snapshot) { return foo.MyWidget(); });
}

我们可以使用deferred as来引入其它文件,拿到的foo会有一个loadLibrary函数,它返回一个Future,当这个Future返回时,就能拿到引入文件的具体函数并执行了。

flutter build web后,会在文件夹中生成一个main.dart.js_x.part.js文件,它会在Flutter调用上面FutureBuilder时才下载文件并加载。一般情况下,我们可以使用它对不同页面进行路由拆分以获得最大收益。

路由兼容

传统的路由方式也能在Flutter Web上使用,但是不会更新浏览器的url,所以需要针对传统路由进行兼容处理。

方案1: 升级到Navigaoion2.0并兼容URL

Navigator2.0网络上有很多文章及教程,不展开讲。可以参考Flutter Navigator 2.0 for Authentication and Bootstrapping,它是一篇关于Navigator 2.0使用系列文章,最后一篇讲的是Web适配。

方案2:使用go_router

go_router是Flutter官方基于Navigator 2.0出的一个响应式的三方库,它提供了更多路由通用功能,相比我们直接使用Navigator 2.0,很多功能不需要再重复造轮子。

下面有个简单例子

import 'package:go_router/go_router.dart';

// GoRouter configuration
final _router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => HomeScreen(),
    ),
    GoRoute(
      path: '/users/:userId',
      builder: (context, state) => const UserScreen(id: state.params['userId']),
    )
  ],
);
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerDelegate: _router.routerDelegate,
      routeInformationParser: _router.routeInformationParser,
    );
  }
}

然后可以通过

context.go('/users/123')
//或者
GoRouter.of(context).push('/users/123');

进行路由跳转。

更多可以点击这里

浏览器兼容性

  • Chrome: 84版本及以上
  • Firefox: 72.9版本及以上
  • Safari: 9及以上(对应MacOS10.11)
  • Edge: 1.2.0及以上

疑难问题

  1. 构建的项目中通过https://unpkg.com/canvaskit-wasm@0.33.0/bin/来引入canvaskit.jscanvaskit.wasm

相关issue

如果使用canvasKit渲染模式构建web,会引入canvaskit.jscanvaskit.wasm文件,默认它会指向一个默认的cdn地址,这个cdn地址可能在国内无法访问,解决办法是在index.html中添加script脚本指定文件地址

window.flutterConfiguration = {
    canvasKitBaseUrl: "/canvaskit/" // 指向本目录的canvaskit文件夹下,编译会自动生成该文件;或者指向自己的CDN地址
};

或者在构建时指定

flutter build web --web-renderer canvaskit --dart-define=FLUTTER_WEB_CANVASKIT_URL=/canvaskit/
  1. 构建未生成flutter.js文件和canvaskit文件夹

如果构建未指定构建类型,可能会使用html渲染方式构建,所以需要构建时指定flutter build web --web-renderer canvaskit

  1. 构建后项目中会有一个字体文件指向了如https://fonts.gstatic.com/s/roboto/等地址

相关issue,解决方案是先去Roboto下载字体文件导入到项目中,然后在项目pubspec.yaml中引入

flutter:
  fonts:
    - family: Roboto
      fonts:
        - asset: assets/Roboto-Regular.ttf

效果演示

原客户端功能:


Flutter转Web:

总结

目前Flutter for Web逐渐趋于成熟,但是它本身还有一些问题

  1. 首次下载资源文件太大
  2. 无法友好支持搜索引擎SEO
  3. 与客户端API有兼容差异,工程化考验开发者能力

相应的也有一些好处

  1. 支持PWA
  2. 如果已有Flutter客户端工程,只需要维护一套代码

所以这些问题和收益需要团队自己去衡量。这次预研也有很多收获,如果后续在我们工程中应用,我再来分享我们在工程中遇到的挑战与实战干货。

参考资料

  1. Flutter Dev
  2. Dart Dev
  3. Flutter Web 支持现已进入稳定版
  4. Flutter For Web多端一体化开发和原理分析
目录
相关文章
|
1月前
|
存储 安全 关系型数据库
后端技术:构建高效稳定的现代Web应用
【10月更文挑战第5天】后端技术:构建高效稳定的现代Web应用
55 1
|
2月前
|
SQL 缓存 搜索推荐
后端技术在现代Web开发中的应用与挑战
本文将深入探讨后端技术在现代Web开发中的重要性,涵盖从基础架构到性能优化的多个方面。通过分析当前主流后端技术的优缺点,并提供一些实用的解决方案和建议,帮助开发者更好地应对日常开发中的挑战。
53 1
|
1月前
|
缓存 前端开发 JavaScript
前端技术探索:构建高效、响应式Web应用的秘诀
前端技术探索:构建高效、响应式Web应用的秘诀
62 0
|
18天前
|
SQL 负载均衡 安全
安全至上:Web应用防火墙技术深度剖析与实战
【10月更文挑战第29天】在数字化时代,Web应用防火墙(WAF)成为保护Web应用免受攻击的关键技术。本文深入解析WAF的工作原理和核心组件,如Envoy和Coraza,并提供实战指南,涵盖动态加载规则、集成威胁情报、高可用性配置等内容,帮助开发者和安全专家构建更安全的Web环境。
36 1
|
21天前
|
负载均衡 监控 算法
论负载均衡技术在Web系统中的应用
【11月更文挑战第4天】在当今高并发的互联网环境中,负载均衡技术已经成为提升Web系统性能不可或缺的一环。通过有效地将请求分发到多个服务器上,负载均衡不仅能够提高系统的响应速度和处理能力,还能增强系统的可扩展性和稳定性。本文将结合我参与的一个实际软件项目,从项目概述、负载均衡算法原理以及实际应用三个方面,深入探讨负载均衡技术在Web系统中的应用。
48 2
|
1月前
|
安全 网络协议 算法
HTTPS网络通信协议揭秘:WEB网站安全的关键技术
HTTPS网络通信协议揭秘:WEB网站安全的关键技术
158 4
HTTPS网络通信协议揭秘:WEB网站安全的关键技术
|
1月前
|
人工智能 前端开发
2024 川渝 Web 前端开发技术交流会「互联」:等你来报名!
2024 川渝 Web 前端开发技术交流会「互联」:等你来报名!
2024 川渝 Web 前端开发技术交流会「互联」:等你来报名!
|
1月前
|
存储 安全 数据库
后端技术在现代Web开发中的实践与创新
【10月更文挑战第13天】 本文将深入探讨后端技术在现代Web开发中的重要性,通过实际案例分析展示如何利用先进的后端技术提升用户体验和系统性能。我们将从基础架构设计、数据库优化、安全性保障等方面展开讨论,为读者提供清晰的指导和实用的技巧。无论是新手开发者还是经验丰富的技术人员,都能从中获得启发和帮助。
35 2
|
1月前
|
机器学习/深度学习 移动开发 JavaScript
Web实时通信的学习之旅:SSE(Server-Sent Events)的技术详解及简单示例演示
Web实时通信的学习之旅:SSE(Server-Sent Events)的技术详解及简单示例演示
133 0
|
1月前
|
自然语言处理 Cloud Native 数据安全/隐私保护
后端技术在现代Web开发中的实践与创新
本文探讨了后端技术在现代Web开发中的重要性及其应用。通过分析当前流行的后端框架和开发模式,揭示了如何利用这些技术来构建高效、可扩展的Web应用程序。同时,文章也讨论了未来后端技术的发展趋势,为开发者提供了一些启示。

相关产品

  • 无影云电脑
  • 下一篇
    无影云桌面