Flutter For Web多端一體化開發和原理分析 https://mp.weixin.qq.com/s/rxapVTJIGfnqCNzB3S7C4Q
Flutter For Web多端一體化開發和原理分析

一、Flutter for Web發展現狀
2019年Google I/O大會上,Google首次在flutter 1.5版本中加入對於web的支持;2021年flutter 2.0版本 web正式進入stable通道。
Flutter官方的roadmap中提到2021年在web方向更專注於性能的提升,來證明Flutter在web上也可以提供高性能的體驗。
Flutter for Web最適合的應用場景是當開發者已經完成Flutter Mobile代碼,需要移植到web端。Flutter官方提出的Flutter for Web另外一個使用的場景就是PWA即Progressive Web Application。相對於blog等靜態web頁面,Flutter for Web更適用於頁面元素和交互更加豐富的頁面,它也給我們提供了豐富的控件來使用。
在Flutter應用方面,Google官方內部正在使用Flutter for Web開發一些網頁,包括我們常用的Flutter Dev Tools也是使用Flutter for Web進行開發的。國內除了貝殼找房之外,還有阿里、美團等公司在進行Flutter for Web方向的研究。
貝殼找房Flutter團隊從2020年開始調研Flutter for Web,並將Flutter for Web運用於客戶端容災降級(見:《Flutter for Web在貝殼找房容災降級中的應用》)。從2021年開始,我們也開始了Flutter for Web的多端一體化建設,主要包括基礎能力補齊、性能優化與監控、構建與部署等。
下面結合我們的經驗來介紹如何使用Flutter for Web進行多端一體化開發,並對其中的原理進行分析。
二、Flutter for Web原理介紹
下面我們從接入、編譯、部署、渲染等幾個方面來看一下如何使用Flutter for Web進行多端一體化開發及其中的原理。
2.1 讓你的工程支持Flutter for Web
Flutter 2.0之前的版本由於web還處於beta通道,需要通過:
flutter channel beta
將本地的Flutter版本切換到beta之后才可以運行在web上。
本文以Flutter 2.2.2版本為例,如果想讓自己的Flutter代碼運行在瀏覽器上,Flutter 2.0以后新創建的工程是默認支持web的,如果你的工程使用老版本創建的,需要執行:
flutter create .
來支持web。重新打開工程后我們發現工程中多了一個web目錄包含以下文件:
Web
├── favicon.png
├── icons
│ ├── Icon-192.png
│ └── Icon-512.png
├── index.html
└── manifest.json
這些文件最終都會編譯到我們的web產物中。
添加web的支持后,我們可以將代碼運行在Chrome或者啟動Web Server在瀏覽器中訪問。

當我們調用flutter build web之后,在我們的build目錄就會得到如下產物:

其中assets文件夾中包含了我們app中的圖片,字體等;main.dart.js中包括了所有的Flutter web sdk和我們的業務代碼。
flutter_service_worker.js.map,瀏覽器在做source mapping時會用到,我們可以通過--no-source-maps選項來關閉這個文件的創建。
那么Flutter編譯器是如何將我們寫的dart代碼編譯成js代碼的呢?下面我們來了解一下Flutter for Web的兩種編譯模式。
2.2 Flutter for Web的兩種編譯模式
Flutter官方給我們提供了dartdevc和dart2js兩個編譯器。我們不僅可以將代碼直接運行在Chrome瀏覽器,也可以將Flutter代碼編譯為js文件部署在服務端。
如果代碼運行在Chrome瀏覽器,flutter_tools會使用dartdevc編譯器進行編譯,dartdevc是支持增量編譯的,開發者可以像調試Flutter Mobile代碼一樣使用hot reload來提升調試效率。
Flutter for Web調試也是非常方便的,編譯后的代碼是默認支持source map,當運行在web瀏覽器時,開發者是不用關心生成的js代碼是怎樣的。
如下圖,開發者可以使用Chrome自帶的開發者工具在dart代碼中打斷點,當執行到相應的js代碼時會斷到dart代碼中。
如果需要編譯成release產物部署在服務器,需要運行flutter build web命令調用dart2js編譯器進行編譯。
下面我們就以dart2js為例來了解一下整個編譯流程是怎樣的。
2.2.1 Dart2js前后端編譯流程
首先我們來回顧一下Flutter Mobile的編譯流程:

Native編譯的前端部分會將源碼編譯成app.dill中間文件,后端編譯會將中間文件進一步編譯成安卓/iOS的so/framework。

Flutter for Web的編譯主要通過dart2js來完成,dart2js中包括了web的前端和后端編譯,前端編譯和native的編譯流程類似,都會生成dill中間文件,主要的差異點是使用了不同的dart sdk,並且針對AST做的轉換也有所不同;后端編譯部分則差異比較大。
下面我們來具體看一下Flutter代碼是如何被編譯成js文件的。
2.2.1.1 Dart2js前端編譯

在調用flutter build web命令后會將項目的main.dart傳入編譯流程,最終輸出的是中間文件app.dill。
flutter_tools首先會將傳入的參數進行組裝,然后調用dart2jsSnapshot。dart2jsSnapshot是dart-sdk中dart2js.dart的快照,我們需要下載dart-sdk來查看相應的源碼。dart2js.dart代碼的位置在dart-sdk/pkg/compiler/lib/src/dart2js.dart這個路徑下。
調用dart2jsSnapshot的參數如下:
--libraries-spec=/Users/beike/flutter/bin/cache/flutter_web_sdk/libraries.json
--native-null-assertions
-Ddart.vm.product=true
-DFLUTTER_WEB_AUTO_DETECT=true
--no-source-maps
-o
/Users/beike/build_path_to_dill/app.dill
--packages=.packages
--cfe-only
/Users/beike/path_to_main/main.dart
--no-source-maps參數就是我們上文提到的是否生成sourcemap的選項;
--cfe-only參數代表只完成前端編譯,生成kernel文件后就不繼續下面的后端編譯流程。
完整的參數列表我們可以在dart-sdk/pkg/compiler/lib/src/options.dart查看。
前端編譯的主要邏輯在kernel/loader.dart的load()方法中。主要代碼如下:
Future load(Uri resolvedUri) {
[省略部分代碼]
initializedCompilerState = fe.initializeCompiler(
initializedCompilerState,
target,
_options.librariesSpecificationUri,
dependencies,
_options.packageConfig,
explicitExperimentalFlags: _options.explicitExperimentalFlags,
nnbdMode: _options.useLegacySubtyping
? fe.NnbdMode.Weak
: fe.NnbdMode.Strong,
invocationModes: _options.cfeInvocationModes,
verbosity: verbosity);
component = await fe.compile(initializedCompilerState, verbose,
fileSystem, onDiagnostic, resolvedUri);
[省略部分代碼]
api.BinaryOutputSink dillOutput =
_compilerOutput.createBinarySink(_options.outputUri);
BinaryOutputSinkAdapter irSink =
new BinaryOutputSinkAdapter(dillOutput);
BinaryPrinter printer = new BinaryPrinter(irSink);
printer.writeComponentFile(component);
[省略部分代碼]
}
前端編譯主要分為兩步,第一步通過dart2js的compile方法生成Component,第二步是將component寫入文件。Component是代碼靜態語法樹的根節點,通過對Component進行遍歷,可以找到app中所有的Library,Library中包含了庫中定義的所有的方法節點、變量節點等。
在compile方法中最終會調用到kernel_target.dart中的buildComponent()方法,該方法的實現如下:
Future buildComponent({bool verify: false}) async {
if (loader.first == null) return null;
return withCrashReporting(() async {
ticker.logMs("Building component");
await loader.buildBodies();
finishClonedParameters();
loader.finishDeferredLoadTearoffs();
loader.finishNoSuchMethodForwarders();
List myClasses = collectMyClasses();
loader.finishNativeMethods();
loader.finishPatchMethods();
finishAllConstructors(myClasses);
runBuildTransformations();
if (verify) this.verify();
installAllComponentProblems(loader.allComponentProblems);
return component;
}, () => loader?.currentUriForCrashReporting);
}
-
其中buildBodies()對每一個Library進行詞法分析和語法分析,把dart源碼中的每一個Library解析保存在Component中;
-
runBuildTransformations()方法是對Component做一些轉換主要包括evaluate constants,add constant coverage 和lower value classes,主要是對代碼中的常量做處理,對dart中對js的調用做轉換等。
-
BinaryPrinter會對Component進行語法樹的遍歷,將Component中每一個node按照一定格式寫入到dill文件。
如果想查看dill文件的內容,可以使用dart-sdk/pkg/vm/bin/dump_kernel.dart將dill轉化為可讀格式。命令如下:
/path_to_flutter_SDK/dart-sdk/bin/dart
/path_to_dart_SDK/pkg/vm/bin/dump_kernel.dart
/ path_to_dill/app.dill
/ path_to_output/out.dill.txt
2.2.1.2 Dart2js后端編譯

Dart2js后端編譯是將前端編譯生成的dill文件通過編譯生成js代碼。
和前端編譯一樣,首先通過flutter_tools調用到dart2jsSnapshot。調用的參數如下:
--libraries-spec=/Users/beike/flutter/bin/cache/flutter_web_sdk/libraries.json
--native-null-assertions
-Ddart.vm.product=true
-DFLUTTER_WEB_AUTO_DETECT=true
--no-source-maps
-O1
-o
/Users/beike/path_to_js/main.dart.js
/Users/beike/path_to_dill/app.dill
其中O1代表優化等級,dart2js支持O0-O4共5中不同的優化,O0代表不做任何優化,包括內聯調用優化、運行時調用優化和全局類型推斷優化,O4的優化程度最高。通過優化可以減少產物的大小並且優化代碼的性能。
Dart2js的后端編譯主要包括以下代碼:
KernelResult result = await kernelLoader.load(uri);
[省略部分代碼]
JsClosedWorld closedWorld = selfTask.measureSubtask("computeClosedWorld",
() => computeClosedWorld(rootLibraryUri, libraries));
[省略部分代碼]
GlobalTypeInferenceResults globalInferenceResults =
performGlobalTypeInference(closedWorld);
[省略部分代碼]
generateJavaScriptCode(globalInferenceResults);
-
首先,編譯器會將傳入的dill通過BinaryBuilder加載到Component中並存儲在KernelResult中;
-
computeClosedWorld()方法會將第一步解析出來的所有Library解析成JsClosedWorld,JsClosedWorld代表了通過closed-world語義編譯之后的代碼。它的結構如下:
class JsClosedWorld implements JClosedWorld {
static const String tag = 'closed-world';
@override
final NativeData nativeData;
@override
final InterceptorData interceptorData;
@override
final BackendUsage backendUsage;
@override
final NoSuchMethodData noSuchMethodData;
FunctionSet _allFunctions;
final Map<classentity, Set> mixinUses;
Map<classentity, List> _liveMixinUses;
final Map<classentity, Set> typesImplementedBySubclasses;
final Map<classentity, Map> _subtypeCoveredByCache =
<classentity, Map>{};
// TODO(johnniwinther): Can this be derived from [ClassSet]s?
final Set implementedClasses;
final Set liveInstanceMembers;
/// Members that are written either directly or through a setter selector.
final Set assignedInstanceMembers;
@override
final Set liveNativeClasses;
@override
final Set processedMembers;
[省略部分代碼]
}
通過傳入的app入口,也就是main()函數,我們能夠知道什么方法被調用,哪些類被初始化,哪些語言特性被使用到等。從結構我們可以看出JsClosedWorld就是用來存儲這些信息的。這些信息將決定后續的編譯流程如何優化,代碼如何生成。
然后,對於JsClosedWorld進行代碼優化,包括上面代碼中的performGlobalTypeInference()等。
最終,generateJavaScriptCode()方法會將上邊返回的結果通過JSBuilder生成最終的js AST。
簡單了解了Flutter for Web的編譯模式和編譯流程之后,下面我們看一下如何部署Flutter for Web產物。
2.3 部署
flutter build web之后的產物我們可以直接部署到服務上,官方建議的服務包括Firebase Hosting、Github Pages和Google Cloud Hosting等。
下面我們以Firebase為例來看下如何部署Flutter Web產物。
1. 執行下面命令安裝Firebase CLI,如已安裝可跳過。
curl -sL https://firebase.tools | bash
2. 使用如下命令與Firebase賬號進行關聯,如已關聯可跳過。
firebase login
3. 使用如下命令進行初始化項目目錄
firebase init
4. 將Flutter Web的產物復制到上一步初始化的目錄中並執行如下命令進行部署
firebase deploy
部署完成后,通過控制台輸出的URL就可以訪問Flutter Web頁面部署的地址。
2.4 Service Worker
Flutter for Web默認支持Service worker。如果想禁用Service Worker,在編譯時加上--pwa-strategy=none參數即可。
Service worker是和JavaScript主線程執行在不同線程的woker,可以攔截和修改資源訪問,更細粒度的緩存資源。它的生命周期包括注冊、安裝和激活,提供了回調方法在這幾個生命周期進行一些自定義任務。
Service worker提供了message和fetch兩個回調方法。Message用於service worker和JavaScript線程進行通信;fetch可以對發出的fetch進行攔截,在攔截方法中實現自己的緩存邏輯。
比如我們通過flutter build web命令生成的flutter_service_worker.js中fetch方法的實現如下:
self.addEventListener("fetch", (event) => {
if (event.request.method !== 'GET') {
return;
}
var origin = self.location.origin;
var key = event.request.url.substring(origin.length + 1);
// Redirect URLs to the index.html
if (key.indexOf('?v=') != -1) {
key = key.split('?v=')[0];
}
if (event.request.url == origin || event.request.url.startsWith(origin + '/#') || key == '') {
key = '/';
}
// If the URL is not the RESOURCE list then return to signal that the
// browser should take over.
if (!RESOURCES[key]) {
return;
}
// If the URL is the index.html, perform an online-first request.
if (key == '/') {
return onlineFirst(event);
}
event.respondWith(caches.open(CACHE_NAME)
.then((cache) => {
return cache.match(event.request).then((response) => {
// Either respond with the cached resource, or perform a fetch and
// lazily populate the cache.
return response || fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
})
})
);
});
其中RESOURCES默認緩存了我們App中使用到的資源,當去拉取這些資源的時候,會默認返回緩存中的資源,當沒有命中緩存再去請求網絡資源。
2.5 渲染
2.5.1 CanvasKit和HTML
Flutter for Web默認支持以下兩種渲染器:
-
CanvasKit
-
HTML
默認情況下,當app運行在手機瀏覽器中時會以HTML模式渲染,運行在桌面瀏覽器時將會使用CanvasKit進行渲染。
如果要指定渲染模式,在編譯時可以指定--web-renderer參數為html或者canvaskit。如:
flutter build web --web-renderer html
CanvasKit和HTML渲染器在性能方面也各有優缺點:
-
CanvasKit以WASM為編譯目標,使用WebGL進行渲染,有更好的性能,但是在加載時會額外加載一個2MB的wasm文件,這就導致加載時會有較長的等待時間。
-
HTML渲染器使用 HTML,CSS,Canvas 和 SVG 元素進行渲染,相對CanvasKit渲染器,HTML的加載更快。
2.5.2 HTML元素生成過程
下面我們用一個例子來看下Image是如何被加載出來的。
我們在dart代碼中定義了一個FadeInImage widget:
FadeInImage.assetNetwork(
fit: BoxFit.fill,
placeholder: this.placeholderPath,
image: this.imgUrl,
height: 75,
width: 124,
);
當調度任務調用到handleDrawFrame()方法之后,會調用到BitmapCanvas的drawImage()方法:
html.HtmlElement _drawImage(
ui.Image image, ui.Offset p, SurfacePaintData paint) {
[省略部分代碼]
imgElement = _reuseOrCreateImage(htmlImage);
[省略部分代碼]
final String cssTransform = float64ListToCssTransform(
transformWithOffset(_canvasPool.currentTransform, p).storage);
imgElement.style
..transformOrigin = '0 0 0'
..transform = cssTransform
..removeProperty('width')
..removeProperty('height');
rootElement.append(imgElement);
_children.add(imgElement);
return imgElement;
}
方法中會創建img元素,然后修改img的css樣式,最終將imgElement添加到rootElement,也就是當前的flt-canvas元素中。生成的html如下:

三、 Flutter for Web開發技巧
3.1 使用Navigation 2.0實現路由
Flutter從1.22版本開始支持Navigation 2.0,相對於1.0版本,2.0能夠更加靈活的對路由進行操作。對於Flutter Web頁面, navigation 2.0能夠保持路由狀態與瀏覽器中的URL保持一致,並且能很好的支持瀏覽器的回退操作。
Navigation 2.0各個類之間的交互如下:
其中比較重要的兩個類是:RouteInformationParser和RouterDelegate。
RouteInformationParser負責對route信息進行解析;
RouterDelegate負責接收route的變化,然后去rebuild Router並通知listener。
下面我們使用navigation 2.0來實現一個列表和詳情跳轉的Flutter for Web頁面,我們希望能夠在路由跳轉時更新瀏覽器的URL,並且能夠通過類似於"/lesson/1"的路由跳轉到相應的課程頁面。
首先,我們在main.dart中有以下代碼:
void main() {
runApp(LessonsApp());
}
class LessonsApp extends StatefulWidget {
@override
StatecreateState() => _LessonsAppState();
}
class _LessonsAppState extends State{
LessonRouterDelegate _routerDelegate = LessonRouterDelegate();
LessonRouteInformationParser _routeInformationParser =
LessonRouteInformationParser();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Lessons App',
routerDelegate: _routerDelegate,
routeInformationParser: _routeInformationParser,
);
}
}
我們在App的根Widget中使用MaterialApp.router()指定了routerDelegate和routeInformationParser。
其中LessonRouteInformationParser的實現如下:
class LessonRouteInformationParser
extends RouteInformationParser{
//解析URL
@override
FutureparseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location);
// Handle '/'
if (uri.pathSegments.length == 0) {
return LessonRoutePath.home();
}
if (uri.pathSegments.length == 2) {
if (uri.pathSegments[0] != 'lesson') return LessonRoutePath.unknown();
var remaining = uri.pathSegments[1];
var id = int.tryParse(remaining).toString();
if (id == null) return LessonRoutePath.unknown();
return LessonRoutePath.details(id);
}
// Handle unknown routes
return LessonRoutePath.unknown();
}
我們實現了parseRouteInformation()方法來對route解析,其中LessonRoutePath是我們定義的類來存儲route的信息。
LessonRouterDelegate的實現如下:
class LessonRouterDelegate extends RouterDelegate
with ChangeNotifier, PopNavigatorRouterDelegateMixin{
final GlobalKeynavigatorKey;
Lesson _selectedLesson;
bool show404 = false;
LessonRouterDelegate() : navigatorKey = GlobalKey();
//置空后導航欄url就沒有了
LessonRoutePath get currentConfiguration {
if (show404) {
return LessonRoutePath.unknown();
}
return _selectedLesson == null
? LessonRoutePath.home()
: LessonRoutePath.details(_selectedLesson.id);
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
//影響頁面順序和返回按鈕
MaterialPage(
key: ValueKey('LessonsListPage'),
child: LearningHistoriesPage(
pushDetail: pushDetial,
),
),
if (show404)
MaterialPage(key: ValueKey('UnknownPage'), child: UnknownScreen())
else if (_selectedLesson != null)
MaterialPage(
key: ValueKey('LessonsDetailPage'),
child: LessonDetailsScreen(id: _selectedLesson.id),
),
// LessonDetailsPage(lesson: _selectedLesson)
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
// Update the list of pages by setting _selectedLesson to null
_selectedLesson = null;
show404 = false;
notifyListeners();
return true;
},
);
}
void pushDetial(String id) {
_selectedLesson = Lesson(id);
notifyListeners();
}
}
build()方法中根據不同的狀態來控制Navigator中的頁面,比如,當用戶選擇一門課程時就會在pages的最上層添加一個詳情的Widget。
通過以上代碼實現的效果如下:
我們發現跳轉到詳情頁時導航欄的URL沒有更改,我們只需要復寫RouteInformationParser的restoreRouteInformation()方法即可。
@override
RouteInformation restoreRouteInformation(LessonRoutePath path) {
if (path.isUnknown) {
return RouteInformation(location: '/404');
}
if (path.isHomePage) {
return RouteInformation(location: '/');
}
if (path.isDetailsPage) {
return RouteInformation(location: '/lesson/${path.id}');
}
return null;
}
如果要實現在地址欄輸入URL跳轉到相應的頁面,需要實現RouterDelegate的setNewRoutePath()方法。
@override
Future<void> setNewRoutePath(LessonRoutePath path) async {
if (path.isUnknown) {
_selectedLesson = null;
show404 = true;
return;
}
if (path.isDetailsPage) {
if (int.parse(path.id) < 0) {
show404 = true;
return;
}
_selectedLesson = Lesson(path.id);
} else {
_selectedLesson = null;
}
show404 = false;
}
這樣,我們就實現了一套Flutter for Web的路由,最終的效果如下:
3.2 使用Federated plugin來支持多平台
在客戶端的開發中,Flutter官方為我們提供了plugin模板來支持native與Flutter的通信。針對多端一體化的開發,Flutter官方提出了Federated plugin的概念。
Federated plugin支持為不同的平台提供不同的實現,它可以把不同平台的不同實現拆分到不同的包里。而且Federated plugin易於擴展,在不影響原有實現的基礎上就可以將你的plugin擴展到新的平台。
比如我們要實現一個同時支持android、iOS和web三端的埋點庫plugin。Plugin的yaml文件需要按照下面這種方式定義:
flutter:
plugin:
platforms:
android:
package: com.beike.log
pluginClass: LogPlugin
ios:
pluginClass: LogPlugin
web:
pluginClass: WebLog
fileName: web_log.dart
由上面配置可以看到,android和iOS需要各端單獨實現,web則是由一個dart文件實現。
實現過程中我們可以定義一個抽象類來定義我們的埋點庫會調用的方法,比如我們的埋點庫定一個抽象類Log:
abstract class Log extends PlatformInterface {
[省略部分實現]
static void logEvent(String eventName, Map params) async =>
throw UnimplementedError('logEvent() has not been implemented.');
}
然后各個平台的實現需要去繼承這個抽象類並且實現其中的方法,比如在web中我們可以這樣實現:
class WebLog extends Log {
static void logEvent(String eventName, Map params) async {
[省略部分實現]
}
}
這樣當我們需要調用埋點方法的時候直接調用logEvent()方法的就可以,不用像下面代碼一樣根據平台去做判斷調用各自的實現。
if(kIsWeb) {
[調用web實現]
} else{
[調用native實現]
}
在定義了以上plugin以后,現在我們來看如何實現我們的web_log.dart。
3.3 Dart與JavaScript互調
Dart為我們提供了dart:js庫來使dart和js交互。使用dart:js庫我們可以在dart代碼中創建js的實例,調用js代碼,讀寫js中對象的屬性等。
我們在埋點庫建設中遇到了一個問題,對於native的埋點,我們可以通過Flutter的platform channel來調用native原有的埋點能力,web端我們也有web團隊開發的比較成熟的埋點庫,那如何去復用這種能力呢?這時候我們就用到了dart與js的互調能力。
我們web端原有的埋點庫有以下方法來向服務器上報埋點:
window.ULOG.send = function (evtid, param) {
return new Promise((resolve, reject) => {
[省略部分實現]
resolve(response);
}
};
為了能夠調用到ULOG的send方法,我們可以通過js_util的getProperty()方法來獲取到ULOG,然后通過js_util提供的callMethod方法進行方法調用,代碼如下:
import 'dart:js_util/js_util.dart' as js_util;
import 'dart:html' as html;
class WebLog extends Log {
static void logEvent(String eventName, Map params) async {
Object ULOG = js_util.getProperty(html.window, 'ULOG');
js_util.callMethod(ULOG, 'send', [eventName, params]);
}
}
這樣我們就可以調用到我們js中原有的ULOG.send()方法。
我們的web埋點庫除了負責將請求發出去之外,還需要關心請求返回的數據,這時候就使用到了js_util中的promiseToFuture,promiseToFuture可以將js中的promise轉換為dart中的future,並接收返回的值。我們WebLog中修改后的代碼如下:
class WebLog {
dynamic logEvent(String eventName, Map params) async {
Object ULOG = js_util.getProperty(html.window, 'ULOG');
dynamic promise = js_util.callMethod(ULOG, 'send', [eventName, params]);
if (promise == null) {
return null;
}
Future future = js_util.promiseToFuture(promise);
dynamic ret = await future;
return ret;
}
}
除了dart調用js之外,我們也可能會用到js調用dart。
如下面代碼,我們在dart側定義了bar()方法,我們可以通過調用js_util 的setProperty ()方法將bar()設置為window的一個屬性foo,當js調用window的foo時就會調用到bar()方法。
class Hello {
static exampleMethod() {
if (js_util.hasProperty(html.window, 'foo') == false) {
js_util.setProperty(html.window, "foo", js.allowInterop(bar()));
}
}
static bar() {
print('Calling function bar');
}
}
四、總結
本文首先講述了Flutter for Web的發展現狀和應用場景。然后從配置、編譯、部署及渲染分別進行了介紹。然后結合埋點庫的例子一起學習了如何開發Federated plugin,完成js與dart的互調。
在多端一體化的探索中,貝殼找房Flutter團隊還做了加載優化、性能優化、性能監控等工作,將在后續的文章中與大家分享。