將 Flutter 新增到任何 Web 應用程式
瞭解將 Flutter 檢視嵌入 Web 內容的不同方式。
Flutter 檢視和 Web 內容可以透過多種方式組合以構建 Web 應用程式。請根據您的用例選擇以下方式之一:
全屏模式
#在全屏模式下,Flutter Web 應用程式控制整個瀏覽器視窗,並在渲染時完全覆蓋其視口。
這是新 Flutter Web 專案的預設嵌入模式,無需額外配置。
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<script src="flutter_bootstrap.js" defer></script>
</body>
</html>
當啟動 Flutter Web 時,如果沒有引用 multiViewEnabled 或 hostElement,它將使用全屏模式。
要了解有關 flutter_bootstrap.js 檔案的更多資訊,請查閱自定義應用初始化。
iframe 嵌入
#
建議在透過 iframe 嵌入 Flutter Web 應用程式時使用全屏模式。嵌入 iframe 的頁面可以根據需要調整其大小和位置,Flutter 將完全填充它。
<iframe src="https://url-to-your-flutter/index.html"></iframe>
要了解有關 iframe 優缺點的更多資訊,請查閱 MDN 上的 內聯框架元素 (Inline Frame element) 文件。
嵌入模式
#Flutter Web 應用程式還可以將內容渲染到另一個 Web 應用程式的任意數量的元素(通常是 div)中;這被稱為“嵌入模式”(或“多檢視”)。
在此模式下:
- Flutter Web 應用程式可以啟動,但在使用
addView新增第一個“檢視”之前不會進行渲染。 - 宿主應用程式可以從嵌入的 Flutter Web 應用程式中新增或刪除檢視。
- 當檢視被新增或刪除時,Flutter 應用程式會收到通知,從而可以相應地調整其元件(widgets)。
啟用多檢視模式
#在 initializeEngine 方法中設定 multiViewEnabled: true 以啟用多檢視模式,如下所示:
{{flutter_js}}
{{flutter_build_config}}
_flutter.loader.load({
onEntrypointLoaded: async function onEntrypointLoaded(engineInitializer) {
let engine = await engineInitializer.initializeEngine({
multiViewEnabled: true, // Enables embedded mode.
});
let app = await engine.runApp();
// Make this `app` object available to your JS app.
}
});
從 JS 管理 Flutter 檢視
#要新增或刪除檢視,請使用 runApp 方法返回的 app 物件:
// Adding a view...
let viewId = app.addView({
hostElement: document.querySelector('#some-element'),
});
// Removing viewId...
let viewConfig = app.removeView(viewId);
從 Dart 處理檢視變更
#檢視的新增和刪除會透過 WidgetsBinding 類的 didChangeMetrics 方法 傳達給 Flutter。
附加到 Flutter 應用的完整檢視列表可以透過 WidgetsBinding.instance.platformDispatcher.views 可迭代物件獲取。這些檢視的型別為 FlutterView。
要將內容渲染到每個 FlutterView 中,您的 Flutter 應用需要建立一個 View 元件。View 元件可以組合在 ViewCollection 元件 下。
以下示例來自 Multi View Playground,它將上述內容封裝在一個 MultiViewApp 元件中,該元件可用作您應用的根元件。一個 WidgetBuilder 函式 會為每個 FlutterView 執行:
import 'dart:ui' show FlutterView;
import 'package:flutter/widgets.dart';
/// Calls [viewBuilder] for every view added to the app to obtain the widget to
/// render into that view. The current view can be looked up with [View.of].
class MultiViewApp extends StatefulWidget {
const MultiViewApp({super.key, required this.viewBuilder});
final WidgetBuilder viewBuilder;
@override
State<MultiViewApp> createState() => _MultiViewAppState();
}
class _MultiViewAppState extends State<MultiViewApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_updateViews();
}
@override
void didUpdateWidget(MultiViewApp oldWidget) {
super.didUpdateWidget(oldWidget);
// Need to re-evaluate the viewBuilder callback for all views.
_views.clear();
_updateViews();
}
@override
void didChangeMetrics() {
_updateViews();
}
Map<Object, Widget> _views = <Object, Widget>{};
void _updateViews() {
final Map<Object, Widget> newViews = <Object, Widget>{};
for (final FlutterView view in WidgetsBinding.instance.platformDispatcher.views) {
final Widget viewWidget = _views[view.viewId] ?? _createViewWidget(view);
newViews[view.viewId] = viewWidget;
}
setState(() {
_views = newViews;
});
}
Widget _createViewWidget(FlutterView view) {
return View(
view: view,
child: Builder(
builder: widget.viewBuilder,
),
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
return ViewCollection(views: _views.values.toList(growable: false));
}
}
有關更多資訊,請查閱 API 文件中的 WidgetsBinding mixin,或開發期間使用的 Multi View Playground 倉庫。
在 Dart 中用 runWidget 替換 runApp
#
Flutter 的 runApp 函式 假設至少有一個可用於渲染的檢視(implicitView)。然而,在 Flutter Web 的多檢視模式下,implicitView 不再存在,因此 runApp 會因 Unexpected null value 錯誤而失敗。
在多檢視模式下,您的 main.dart 必須改為呼叫 runWidget 函式。它不需要 implicitView,並且只會渲染到已顯式新增到您應用中的檢視中。
以下示例使用上述 MultiViewApp 在每個可用的 FlutterView 上渲染 MyApp() 元件的副本:
void main() {
runWidget(
MultiViewApp(
viewBuilder: (BuildContext context) => const MyApp(),
),
);
}
識別檢視
#每個 FlutterView 在附加時都會由 Flutter 分配一個識別符號。此 viewId 可用於唯一標識每個檢視、檢索其初始配置或決定在其中渲染什麼。
渲染的 FlutterView 的 viewId 可以從其 BuildContext 中獲取,如下所示:
class SomeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Retrieve the `viewId` where this Widget is being built:
final int viewId = View.of(context).viewId;
// ...
同樣,從 MultiViewApp 的 viewBuilder 方法中,viewId 可以這樣獲取:
MultiViewApp(
viewBuilder: (BuildContext context) {
// Retrieve the `viewId` where this Widget is being built:
final int viewId = View.of(context).viewId;
// Decide what to render based on `viewId`...
},
)
閱讀有關 View.of 建構函式 的更多資訊。
初始檢視配置
#Flutter 檢視在啟動時可以接收來自 JS 的任何初始化資料。這些值透過 addView 方法的 initialData 屬性傳遞,如下所示:
// Adding a view with initial data...
let viewId = app.addView({
hostElement: someElement,
initialData: {
greeting: 'Hello, world!',
randomValue: Math.floor(Math.random() * 100),
}
});
在 Dart 中,initialData 可作為一個 JSAny 物件使用,可透過 dart:ui_web 庫中的頂級 views 屬性訪問。資料通過當前檢視的 viewId 進行訪問,如下所示:
final initialData = ui_web.views.getInitialData(viewId) as YourJsInteropType;
要了解如何定義 YourJsInteropType 類以對映從 JS 傳遞的 initialData 物件,從而在 Dart 程式中實現型別安全,請查閱 dart.dev 上的 JS 互操作性 (JS Interoperability)。
檢視約束
#預設情況下,嵌入的 Flutter Web 檢視將其 hostElement 的大小視為不可變屬性,並將其佈局嚴格限制在可用空間內。
在 Web 上,元素的固有大小通常會影響頁面的佈局(例如 img 或 p 標籤可以使內容圍繞它們重排)。
將檢視新增到 Flutter Web 時,您可以為其配置約束,以告知 Flutter 該檢視應如何進行佈局:
// Adding a view with initial data...
let viewId = app.addView({
hostElement: someElement,
viewConstraints: {
maxWidth: 320,
minHeight: 0,
maxHeight: Infinity,
}
});
從 JS 傳遞的檢視約束需要與嵌入 Flutter 的 hostElement 的 CSS 樣式相容。例如,如果 CSS 中傳遞了 max-height: 100px,但給 Flutter 傳遞了 maxHeight: Infinity,Flutter 不會嘗試“修復”這種矛盾的常量。
要了解更多資訊,請檢視 ViewConstraints 類 和 理解約束 (Understanding constraints)。
自定義元素 (hostElement)
#
您可以將單檢視 Flutter Web 應用嵌入到您網頁的任何 HTML 元素中。
要告知 Flutter Web 在哪個元素中進行渲染,請將一個帶有 config 欄位的物件傳遞給 _flutter.loader.load 函式,該物件指定一個 HTMLElement 作為 hostElement。
_flutter.loader.load({
config: {
hostElement: document.getElementById('flutter_host'),
}
});
要了解有關其他配置選項的更多資訊,請查閱 自定義 Web 應用初始化。