内容

迁移至 package:web

Dart 的 package:web 公开了对浏览器 API 的访问,从而支持 Dart 应用程序和 Web 之间的互操作。使用 package:web 与浏览器进行交互,并操作 DOM 中的对象和元素。

dart
import 'package:web/web.dart';

void main() {
  final div = document.querySelector('div')!;
  div.text = 'Text set at ${DateTime.now()}';
}

package:webdart:html

#

package:web 的目标是通过解决现有 Dart Web 库的几个问题,来改革 Dart 公开 Web API 的方式

  1. Wasm 兼容性

    只有使用 dart:js_interopdart:js_interop_unsafe 的包才与 Wasm 兼容。package:web 基于 dart:js_interop,因此默认情况下,它在 dart2wasm 上受支持。

    在编译为 Wasm 时,不支持 dart:htmldart:svg 等 Dart 核心 Web 库。

  2. 保持现代化

    package:web 使用 Web IDL 自动生成 IDL 中每个声明的 互操作成员互操作类型。与 dart:html 中的额外成员和抽象不同,直接生成引用可以让 package:web 更加简洁、易于理解、更加一致,并且能够与 Web 开发的未来保持同步。

  3. 版本控制

    由于它是一个软件包,因此 package:webdart:html 这样的库更容易进行版本控制,并且避免在它演变时破坏用户代码。它还使代码不那么排他,更开放,可以接受贡献。开发人员可以创建自己的 备用互操作声明,并将其与 package:web 一起使用,而不会产生冲突。


这些改进自然会导致 package:webdart:html 之间出现一些实现差异。影响现有软件包最大的更改,例如 IDL 重命名类型测试,将在随后的迁移部分中进行解决。虽然我们出于简洁目的只引用 dart:html,但相同的迁移模式适用于任何其他 Dart 核心 Web 库,例如 dart:svg

dart:html 迁移

#

删除 dart:html 导入,并用 package:web/web.dart 替换它

dart
import 'dart:html' as html; // Remove
import 'package:web/web.dart' as web; // Add

web 添加到 pubspec 中的 dependencies

yaml
dependencies:
  web: ^0.5.0

以下部分介绍了从 dart:htmlpackage:web 的一些常见迁移问题。

对于任何其他迁移问题,请查看 dart-lang/web 代码库并提交问题。

重命名

#

dart:html 中的许多符号已从其原始 IDL 声明中重命名,以更符合 Dart 样式。例如,appendChild 变为 appendHTMLElement 变为 HtmlElement,等等。

相比之下,为了减少混淆,package:web 使用 IDL 定义中的原始名称。提供了一个 dart fix 来转换在 dart:htmlpackage:web 之间重命名的类型。

在更改导入后,任何重命名的对象都将成为新的“未定义”错误。你可以通过以下方式解决这些问题

  • 通过运行 dart fix --dry-run 从 CLI 中解决。
  • 在你的 IDE 中,通过选择 dart fix重命名为“package:web 名称

dart fix 涵盖了许多常见的类型重命名。如果你遇到一个没有 dart fix 来重命名的 dart:html 类型,请首先通过提交 问题 让我们知道。

然后,你可以尝试通过查找 dart:html 成员的定义来手动发现现有 dart:html 成员的 package:web 类型名称。dart:html 成员定义上的 @Native 注释的值告诉编译器将该类型的任何 JS 对象视为它注释的 Dart 类。例如,@Native 注释告诉我们 dart:htmlHtmlElement 成员的原生 JS 名称是 HTMLElement,因此 package:web 名称也将是 HTMLElement

dart
@Native("HTMLElement")
class HtmlElement extends Element implements NoncedElement { }

要查找 package:web 中未定义成员的 dart:html 定义,请尝试以下方法之一

  • 在 IDE 中按住 Ctrl 或 Command 键单击未定义的名称,然后选择转到定义
  • dart:html API 文档 中搜索该名称,并在注释下查看其页面。

同样,你可能会发现一个未定义的 package:web API,其对应的 dart:html 成员的定义使用关键字 native。检查该定义是否使用 @JSName 注释进行重命名;该注释的值会告诉你该成员在 package:web 中使用的名称。

dart
@JSName('appendChild')
Node append(Node node) native;

native 是一个内部关键字,在此上下文中与 external 的含义相同。

类型测试

#

使用 dart:html 的代码通常会利用 is 等运行时检查。当与 dart:html 对象一起使用时,isas 验证该对象是 @Native 注释中的 JS 类型。相比之下,所有 package:web 类型都具体化为 JSObject。这意味着运行时类型测试将导致 dart:htmlpackage:web 类型之间的行为不同。

为了能够执行类型测试,请将使用 is 类型测试的任何 dart:html 代码迁移到使用 interop 方法,例如 instanceOfString 或更方便且类型化的 isA 帮助器(从 Dart 3.4 开始提供)。JS 类型页面的 兼容性、类型检查和强制转换 部分详细介绍了替代方案。

dart
obj is Window; // Remove
obj.instanceOfString('Window'); // Add

类型签名

#

dart:html 中的许多 API 在其类型签名中支持各种 Dart 类型。由于 dart:js_interop 限制 了可以编写的类型,因此 package:web 中的某些成员现在要求你在调用该成员之前转换该值。了解如何从 JS 类型页面的 转换 部分使用 interop 转换方法。

dart
window.addEventListener('click', callback); // Remove
window.addEventListener('click', callback.toJS); // Add

通常,你可以发现哪些方法需要转换,因为它们会标记为异常的某种变体

A value of type '...' can't be assigned to a variable of type 'JSFunction?'

条件导入

#

代码通常会根据是否支持 dart:html 使用条件导入来区分原生和 web

dart
export 'src/hw_none.dart'
    if (dart.library.io) 'src/hw_io.dart'
    if (dart.library.html) 'src/hw_html.dart';

但是,由于在编译到 Wasm 时不支持 dart:html,因此正确的替代方法现在是使用 dart.library.js_interop 来区分原生和 web

dart
export 'src/hw_none.dart'
    if (dart.library.io) 'src/hw_io.dart'
    if (dart.library.js_interop) 'src/hw_web.dart';

虚拟调度和模拟

#

dart:html 类支持虚拟分派,但由于 JS interop 使用扩展类型,因此 不可能 进行虚拟分派。同样,使用 package:web 类型的dynamic 调用不会按预期工作(或者,它们可能只是碰巧继续工作,但会在删除 dart:html 时停止),因为它们的成员只能静态使用。迁移所有依赖于虚拟分派来避免此问题的代码。

虚拟分派的一个用例是模拟。如果你有一个模拟类implements一个 dart:html 类,则不能使用它来实现 package:web 类型。相反,最好模拟 JS 对象本身。有关更多信息,请参阅 模拟教程

native API

#

dart:html 类还可能包含具有非平凡实现的 API。这些成员可能存在或不存在于 package:web 帮助程序 中。如果你的代码依赖于该实现的具体内容,你也许能够复制必要的代码。但是,如果你认为这样做不可行,或者该代码对其他用户也有好处,请考虑提交一个问题或上传一个拉取请求到 package:web 以支持该成员。

区域

#

dart:html 中,回调会自动分区。package:web 中并非如此。当前分区中没有回调的自动绑定。

如果这对你应用程序很重要,你仍然可以使用分区,但你必须通过绑定回调来自己编写分区。有关更多详细信息,请参阅 #54507。目前还没有转换 API 或 帮助程序 可以自动执行此操作。

帮助程序

#

package:web 的核心包含 external 互操作成员,但不提供 dart:html 默认提供的其他功能。为了减轻这些差异,package:web 包含 帮助程序,以在处理无法通过核心互操作直接获得的许多用例时提供其他支持。帮助程序库包含各种成员,以公开 Dart Web 库中的一些旧功能。

例如,核心 package:web 仅支持添加和删除事件侦听器。相反,你可以使用 流帮助程序,它可以轻松地使用 Dart Stream 订阅事件,而无需自己编写该代码。

dart
// dart:html version
InputElement htmlInput = InputElement();
await htmlInput.onBlur.first;

// package:web version
HTMLInputElement webInput = document.createElement('input') as HTMLInputElement;
await webInput.onBlur.first;

你可以在 package:web/helpers 的存储库中找到所有帮助程序及其文档。它们将不断更新,以帮助用户迁移并使其更容易使用 Web API。

示例

#

以下是一些已从 dart:html 迁移到 package:web 的软件包示例