跳至主要内容

将 Flutter 添加到任何 Web 应用

Flutter 视图和 Web 内容可以通过多种方式组合,构建出一个 Web 应用。根据您的使用场景,选择以下其中一种方式:

全屏模式

#

在全屏模式下,Flutter Web 应用会接管整个浏览器窗口,并在渲染时完全覆盖其视口。

这是新的 Flutter Web 项目的默认嵌入模式,无需额外配置。

html
<!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 会将其完全填充。

html
<iframe src="https://url-to-your-flutter/index.html"></iframe>

要了解有关 iframe 的优缺点的更多信息,请查看 MDN 上的 内联框架元素 文档。

嵌入模式

#

Flutter Web 应用还可以将内容渲染到另一个 Web 应用的任意数量的元素(通常为 div)中;这称为“嵌入模式”(或“多视图”)。

在此模式下

  • Flutter Web 应用可以启动,但直到使用 `addView` 添加第一个“视图”后才会开始渲染。
  • 宿主应用可以向嵌入的 Flutter Web 应用添加或移除视图。
  • 当添加或移除视图时,Flutter 应用会收到通知,以便相应地调整其小部件。

启用多视图模式

#

启用多视图模式,在 `initializeEngine` 方法中设置 `multiViewEnabled: true`,如下所示

flutter_bootstrap.js
js
{{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` 对象

js
// 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` 小部件中,该小部件可用作应用的根小部件。对于每个 `FlutterView`,都会运行一个 `WidgetBuilder` 函数

multi_view_app.dart
dart
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` 混合,或开发过程中使用的 Multi View Playground 代码库

在 Dart 中将 `runApp` 替换为 `runWidget`

#

Flutter 的 `runApp` 函数 假设至少有一个可供渲染的视图(`implicitView`),但在 Flutter Web 的多视图模式下,`implicitView` 不再存在,因此 `runApp` 将开始出现 `Unexpected null value` 错误。

在多视图模式下,您的 `main.dart` 必须调用 `runWidget` 函数。它不需要 `implicitView`,并且只会渲染到已明确添加到应用中的视图中。

以下示例使用上面描述的 `MultiViewApp` 在每个可用的 `FlutterView` 上渲染 `MyApp()` 小部件的副本

main.dart
dart
void main() {
  runWidget(
    MultiViewApp(
      viewBuilder: (BuildContext context) => const MyApp(),
    ),
  );
}

识别视图

#

每个 `FlutterView` 在附加时都会由 Flutter 分配一个标识符。此 `viewId` 可用于唯一标识每个视图,检索其初始配置或确定在其中渲染什么内容。

渲染的 `FlutterView` 的 `viewId` 可以通过其 `BuildContext` 这样获取

dart
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`

dart
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` 属性传递,如下所示

js
// 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` 访问,如下所示

dart
final initialData = ui_web.views.getInitialData(viewId) as YourJsInteropType;

要了解如何定义 `YourJsInteropType` 类以映射从 JS 传递的 `initialData` 对象,使其在您的 Dart 程序中类型安全,请查看 dart.dev 上的:JS 交互操作

视图约束

#

默认情况下,嵌入式 Flutter Web 视图将 `hostElement` 的大小视为一个不可变属性,并将其布局严格限制在可用空间内。

在 Web 上,元素的固有大小通常会影响页面的布局(例如,可以在其周围重新调整内容的 `img` 或 `p` 标记)。

向 Flutter Web 添加视图时,您可以使用约束对其进行配置,以告知 Flutter 如何布局视图

js
// Adding a view with initial data...
let viewId = app.addView({
  hostElement: someElement,
  viewConstraints: {
    maxWidth: 320,
    minHeight: 0,
    maxHeight: Infinity,
  }
});

从 JS 传递的视图约束需要与嵌入 Flutter 的 `hostElement` 的 CSS 样式兼容。例如,Flutter 不会尝试“修复”诸如在 CSS 中传递 `max-height: 100px`,但在 Flutter 中传递 `maxHeight: Infinity` 之类的矛盾常量。

要了解更多信息,请查看 `ViewConstraints` 类理解约束

自定义元素(`hostElement`)

#

Flutter 3.10 到 3.24 之间
您可以将单个视图的 Flutter Web 应用嵌入到 Web 页面上的任何 HTML 元素中。

要告诉 Flutter Web 将渲染到哪个元素,请将包含 `config` 字段的对象传递给 `_flutter.loader.load` 函数,该字段指定 `HTMLElement` 作为 `hostElement`。

js
_flutter.loader.load({
  config: {
    hostElement: document.getElementById('flutter_host'),
  }
});

要了解有关其他配置选项的更多信息,请查看 自定义 Web 应用初始化