简介
Flutter 作为当前最热门的跨端技术之一,于 2.0 版本稳定支持 Web 平台。其创始人 Eric Seidel 曾工作于 Chrome 团队,期间遇到一些难以解决的问题。为了是 Web 的体验更加流畅平滑,他花费数周时间移除历史兼容和几乎没人使用的功能模块,结果发现性能提升 20 倍。所以他尝试抛弃历史包袱,做一个更快,更简单的东西,最后出现 Flutter。
Web 支持
Flutter 与 2018 年 11 月宣布支持 Web 平台,但是到 2021 年 3 月才正式发布 2.0 版本才稳定支持 Web 平台,引擎采用了 Native 完全不同的渲染方式

Flutter 官方提供了 2 种渲染模式
- HTML 渲染,使用 HTML,CSS,Canvas 和 SVG 元素来渲染,应用的大小相对较小
- CanvasKit 渲染,将 Skia 编译成 WebAssembly 格式,并使用 WebGL 渲染(包增大2M左右)
HTML 模式
写了一个 dome,通过运行 flutter run web --web-renderer html ,代码和效果图如下
import 'package:flutter/material.dart';class MyApp extends StatelessWidget {@overrideWidget build(BuildContext context) {return MaterialApp(title: 'h51',home: Center(child: Text('Hello Word!'),),);}}

可以看出使用 HTML 模式时,FLutter 将 Text 组件转译成 HTML 的 p 标签和 span 标签使用 HTML 渲染出来,其中样式均设置在 style 属性里面。这里是基于 dart:js 和 dart:html 两个成熟的 SDK 完成转化。
CanvasKit 模式
CanvasKit 模式是基于 WebAssembly(wasm) 使用 WebGL 渲染。WebAssembly 或者 wasm 是一个可移植、体积小、加载快并且兼容 Web 的全新格式,主要使用 C++ 编写,配套工具都是 C++ 系列。WebGL 是 Web 新推出的图形库,支持用户使用硬件图形加速。
用上面 demo 代码运行 flutter run web --web-renderer canvaskit 强制指定渲染模式为 CanvasKit,效果图如下:
可以看出 DOM 结构里面只有一个 canvas 元素。当使用 CanvasKit 模式时,会通过 Server Worker 作为中转服务器,拦截 Fetch 请求,并加载 canvaskit.wasm 等资源,然后 JavaScript 调用 WebGL 和 canvaskit.wasm API 绘制对应界面。
渲染原理
基于上面感性的了解,下面简单看下 Flutter 渲染不同内容的方式,主要分为文字,图片,图形 3 个模块,这里我们分析其中的文字渲染和图片渲染情况。
上图主要是文字渲染的简单版调用链,具体代码就不贴出来了。其中可以看出以下几点:
- Flutter 层和 dart:ui 层的 3 种模式都是一致的,只有渲染引擎部分不一致
- CanvasKit 模式下 Engine 渲染与 Native 流程几乎全部一致
- HTML 模式下引擎使用 dart:js 和 dart:html 将代码转为 HTML

上图主要是图片渲染的简单版调用链,与文字模式的大部分情况相同,一致的 Flutter 层和 dart:ui 层。区别在于 HTML 模式的图片渲染用的是 Canvas API 渲染,而不是 Image 标签
Dart 与 JavaScript 互调
Dart 支持与 C++、JavaScript 等语言相互调用,我们实现以下 Dart 和 JavaScript 互相调用的能力。官方通过提供 dart:js 这个库实现的相互调用
Dart 调用 JavaScript
下面代码案例是调用 JavaScript 的 JSON.stringify API
@JS()library stringify;import 'package:js/js.dart';// Calls invoke JavaScript `JSON.stringify(obj)`.@JS('JSON.stringify')external String stringify(Object obj);
JavaScript 调用 Dart
@JS()library callable_function;import 'package:js/js.dart';/// Allows assigning a function to be callable from `window.functionName()`@JS('functionName')external set _functionName(void Function() f);/// Allows calling the assigned function from Dart as well.@JS()external void functionName();void _someDartFunction() {print('Hello from Dart!');}void main() {_functionName = allowInterop(_someDartFunction);// JavaScript code may now call `functionName()` or `window.functionName()`.}
上面给出的官方提供的实例,实际上里经使用了 js/js.dart 的能力,还可以通过更加底层的能力 js/js_util.dart 去获取 JavaScript 的全部变量,即挂在在 window 下的属性值,去实现对应操作。
编译
虽然 Google 在浏览器规范提出将 Dart 语言集成到浏览器,但目前没有得到评委的支持,所以 Google 开发 dart2js 将 Dart 转化为 JavaScript 的 SDK。我们简单看下编译的流程
- code -> app.dill -> App.framework/app.so
- code -> app.dill -> main.dart.js
Native 和 Flutter For Web 的前半段编译流程类似,都是生成了 dill 中间文件,后半部则根据不同 AST 语法树做不同的转换,下面以 dart2js 为例介绍下编译过程。
前编译
调用 flutter build web 命令后会将项目的 main.dart 传入编译流程,最终输出的是中间文件 app.dill
- flutter_tools 将传入参数进行组件,然后调用 dart2jsSnapshot,比如—no-source-maps参数是生成sourcemap的选项
- 在 kernel/loader.dart 的 load() 方法中,通过 dart2js 的 compile 方法生成 Component,Component是代码静态语法树的根节点,通过对 Component 进行遍历,可以找到 app 中所有的 Library,Library 中包含了库中定义的所有的方法节点、变量节点等。然后将 Component 写入文件
- compile 最终会调用到 kernel_target.dart 中的 buildComponent() 方法
- 其中 buildBodies() 对每一个Library进行词法分析和语法分析,把 dart 源码中的每一个 Library 解析保存在Component 中
- runBuildTransformations() 方法是对Component做一些转换主要包括 evaluate constants,add constant coverage 和 lower value classes,主要是对代码中的常量做处理,对 dart 中对 js 的调用做转换等
- BinaryPrinter会对Component进行语法树的遍历,将Component中每一个node按照一定格式写入到 dill 文件
查看 dill 文件内容 dart /{{dart_SDK}}/pkg/vm/bin/dump_kernel.dart /{{dill_path}}/app.dill /{{out_path}}/out.dill.txt
后编译
Dart2js 后端编译是将前端编译生成的 dill 文件通过编译生成 js 代码
- 和前端编译一样,首先通过 flutter_tools 调用到 dart2jsSnapshot,其中参数 O1 代表优化等级,最高级 O4 代表优化代码性能,减少产物大小等
- 编译器会将传入的 dill 通过 BinaryBuilder 加载到 Component 中并存储在 KernelResult 中
- computeClosedWorld() 方法会将第一步解析出来的所有 Library 解析成 JsClosedWorld,JsClosedWorld 代表了通过 closed-world 语义编译之后的代码,存储内容是什么方法被调用,哪些类被初始化,哪些语言特性被使用到等
- 执行 JsClosedWorld、performGlobalTypeInference 进行代码优化
- generateJavaScriptCode() 方法会将上边返回的结果通过JSBuilder生成最终的 js AST
单页面打包
Flutter 支持用户指定入口去打包部分 Widget,比如只需要将某个页面打包成 h5 的情况,可以新建一个 share 入口,在里面根据命令行配置所需打包的页面进行动态打包 ```dart // lib/pages/share.dart import ‘package:flutter/material.dart’; import ‘./test/testpage.dart’;
class MyApp extends StatelessWidget {
const pages = String.fromEnvironment(“PAPE”);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: ‘test page’,
home: Center(
// 伪代码
child: pages
void main() async { runApp(MyApp()); }
// lib/pages/test/testpage.dart class TextPage extends StatelessWidget { @override Widget build(BuildContext context) { return Text(‘test pages!’); } }
根据以上伪代码去编写,然后执行```dartflutter run -d chrome --dart-define="PAPE=TextPage" --target="lib/pages/share"
最终会打包成一个由 share 入口的 web 应用,如果 TestPage 没有其他页面依赖,就是一个单页面,如下图所示
混合开发
既然打包出一个单页面,即有可能进行混合嵌套,即 Flutter 开发的 h5 页面可能嵌套在 Native 中,也可能嵌套在 Web 应用中,那么就会出现通信问题。
1)嵌套在 Web
Flutter 无法打包成一个 div 标签而嵌套到其他 Web 中,所以需要使用 Iframe 进行页面嵌套。要与父页面产生交互就需要有通信方式,通过 dart:js 调用 PostMessage 与父页面进行通信
2)嵌套在 Native
Flutter 开发的页面通过 Webview 嵌套在 Native 中也无法使用直接与 Native 进行通行,然而 h5 通常需要有与 Native 交互的能力,即使用 JSBridge 或者 URL SCHEME 的方式。所以通过 dart:js 调用 JSBridge 的 API 实现与 Native 通信
应用前景
- 渐进式 web 应用 (Progressive web apps, PWA),兼具 web 的高覆盖面与桌面应用的强大功能(官方)
- 单页应用 (Single page apps, SPA),只需一次加载,并与互联网服务动态互传数据(官方),比如:腾讯企鹅辅导上课页
- 将现有 Flutter 移动应用拓展到 web,在两个平台共享代码(官方)
- 容灾降级应用,当 Flutter 页面出现问题时,将 Flutter 代码编译为 h5 页面动态下发实现不更新修复,比如:Flutter for Web在贝壳找房容灾降级中的应用
- 动态下发渲染,动态的利用 Flutter 引擎渲染界面,ios 禁止的行为,比如:美团外卖Flutter动态化实践、QQ团队开源动态化Flutter
跨平台统一,实现全平台一套代码多端统一界面,比如:企业微信超大型工程-跨全平台UI框架最佳实践
总结
目前 Flutter 生态并不完善,包括性能优化,分包加载优化,样式分离等等都需要自行开发,各大企业都在开发自己的轮子,但发展前景可期待,适合投入学习。
参考文档
- 重磅|庖丁解牛之——Flutter for Web
- Flutter原理与实践
- Flutter For Web多端一体化开发和原理分析
- Flutter Web 支持现已进入稳定版
- Flutter for Web 详细预研
