无影Flutter for Web技术预研

本文涉及的产品
无影云电脑企业版,4核8GB 120小时 1个月
无影云电脑企业版,4核8GB 120小时 1个月
无影云电脑个人版,黄金款:40核时/1个月有效
简介: ## 介绍[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多端一体化开发和原理分析
目录
相关文章
|
3天前
|
SQL 缓存 搜索推荐
后端技术在现代Web开发中的应用与挑战
本文将深入探讨后端技术在现代Web开发中的重要性,涵盖从基础架构到性能优化的多个方面。通过分析当前主流后端技术的优缺点,并提供一些实用的解决方案和建议,帮助开发者更好地应对日常开发中的挑战。
17 1
|
8天前
|
安全 JavaScript Java
后端技术在现代Web开发中的实践与挑战
本文旨在探讨后端技术在现代Web开发中的关键作用,分析其在数据处理、业务逻辑实现和系统安全等方面的重要性。通过阐述常见的后端技术和框架,如Node.js、Django和Spring Boot,展示它们在实际项目中的应用。同时,文章将讨论后端开发所面临的主要挑战,包括性能优化、扩展性和维护性问题,以及如何应对这些挑战。最终,通过对实际案例的分析,总结出一套行之有效的后端开发最佳实践,为开发者提供参考。
34 5
|
8天前
|
人工智能 关系型数据库 数据安全/隐私保护
后端技术在现代Web开发中的应用与挑战
本文将深入探讨后端技术在现代Web开发中的重要性,通过分析其在数据处理、业务逻辑实现和安全性保障方面的应用,揭示后端技术的核心价值。同时,本文还将讨论当前后端开发面临的主要挑战,如高并发处理、数据安全、微服务架构的复杂性等,并给出相应的解决方案。无论是后端开发者还是对后端技术感兴趣的读者,都可以通过这篇文章获得启发和指导。
|
19天前
|
前端开发 安全 Java
技术进阶:使用Spring MVC构建适应未来的响应式Web应用
【9月更文挑战第2天】随着移动设备的普及,响应式设计至关重要。Spring MVC作为强大的Java Web框架,助力开发者创建适应多屏的应用。本文推荐使用Thymeleaf整合视图,通过简洁的HTML代码提高前端灵活性;采用`@ResponseBody`与`Callable`实现异步处理,优化应用响应速度;运用`@ControllerAdvice`统一异常管理,保持代码整洁;借助Jackson简化JSON处理;利用Spring Security增强安全性;并强调测试的重要性。遵循这些实践,将大幅提升开发效率和应用质量。
46 7
|
21天前
|
开发框架 中间件 API
揭秘!Tornado技术如何颠覆传统,解锁Web开发速度极限?你的高效Web应用就差这一步!
【8月更文挑战第31天】Tornado 是 Python 生态中的一款高性能 Web 开发框架,以其非阻塞 I/O 模型和高并发处理能力著称。它采用协程和异步 I/O 实现了高效的并行处理,使服务器能同时处理大量连接。
25 1
|
21天前
|
测试技术 开发者 Python
Bottle技术:如何用Python打造小巧而强大的Web应用,让你一鸣惊人?
【8月更文挑战第31天】本文介绍了Bottle——一种轻量级Web框架,以其简洁的语法和强大功能受到开发者喜爱。文章涵盖Bottle的核心概念(路由、模板、请求对象),并展示了其简单易用的特性及快速开发能力。通过遵循最佳实践,开发者能够高效地利用Bottle创建高质量Web应用,迎接未来Web开发的挑战。
14 1
|
21天前
|
测试技术 Python
Bottle技术:如何用Python打造小巧而强大的Web开发利器?
【8月更文挑战第31天】Bottle是一个用Python编写的轻量级Web框架,设计简洁、快速且小巧,适用于快速开发Web应用程序。其主要特点包括简单易学、快速开发、小巧轻量及强大的功能,如路由、模板和表单验证等。Bottle的核心概念包括路由、模板和请求对象,可通过示例了解其基本用法。此外,合理使用路由、编写测试和利用Bottle扩展等最佳实践有助于更高效地进行Web开发。随着Bottle生态的不断发展,它将在未来Web开发中扮演更重要的角色。
24 1
|
7天前
|
数据处理 Python
Django视图:构建动态Web页面的核心技术
Django视图:构建动态Web页面的核心技术
|
15天前
|
关系型数据库 Java MySQL
"解锁Java Web传奇之旅:从JDK1.8到Tomcat,再到MariaDB,一场跨越数据库的冒险安装盛宴,挑战你的技术极限!"
【9月更文挑战第6天】在Linux环境下安装JDK 1.8、Tomcat和MariaDB是搭建Java Web应用的关键步骤。本文详细介绍了使用apt-get安装OpenJDK 1.8、下载并配置Tomcat,以及安装和安全设置MariaDB(MySQL的开源分支)的方法。通过这些步骤,您可以快速构建一个稳定、高效的开发和部署环境,并验证各组件是否正确安装和运行。这为您的Java Web应用提供了一个坚实的基础。
30 0
|
21天前
|
Java Maven Apache
Struts 2 配置不再难!跟着这篇详解从零搭建开发环境
【8月更文挑战第31天】要搭建Struts 2开发环境,需先安装JDK,然后下载并解压Struts 2二进制包,将其核心库`struts2-core`添加到项目类路径中。使用Maven或Gradle时,可在配置文件中添加依赖。接着,在`web.xml`中配置Struts 2过滤器及其映射。`struts.xml`通常位于`src/main/resources`目录下,用于定义动作映射和拦截器等核心配置。最后,通过配置类路径下的`log4j.properties`文件,可以设置Struts 2的日志记录级别及输出方式。完成以上步骤后,即可开始基于Struts 2框架进行Web应用开发。
39 0

相关产品

  • 无影云电脑