内容

用法

JS 互操作提供了从 Dart 与 JavaScript API 交互的机制。它允许你调用这些 API 并使用显式、惯用的语法与你从它们获取的值进行交互。

通常,你可以通过将 JavaScript API 放在 全局 JS 范围 中的某个位置来访问它。要从这个 API 调用和接收 JS 值,可以使用 external 互操作成员。为了构造 JS 值的类型并为其提供类型,可以使用和声明 互操作类型,其中也包含互操作成员。要将 Dart 值(如 ListFunction)传递给互操作成员或从 JS 值转换为 Dart 值,请使用 转换函数,除非互操作成员 包含基本类型

互操作类型

#

与 JS 值交互时,你需要为其提供一个 Dart 类型。你可以通过使用或声明互操作类型来实现这一点。互操作类型是 Dart 提供的 "JS 类型" 或是包装了互操作类型的 扩展类型

互操作类型允许你为 JS 值提供一个接口,并让你为其成员声明互操作 API。它们也用于其他互操作 API 的签名中。

dart
extension type Window(JSObject _) implements JSObject {}

Window 是任意 JSObject 的互操作类型。没有 运行时保证 Window 实际上是 JS Window。它也不会与为相同值定义的任何其他互操作接口冲突。如果你要检查 Window 实际上是否为 JS Window,可以通过 通过互操作检查 JS 值的类型

你也可以通过包装 Dart 提供的 JS 类型来声明你自己的互操作类型

dart
extension type Array._(JSArray<JSAny?> _) implements JSArray<JSAny?> {
  external Array();
}

在大多数情况下,你可能会使用 JSObject 作为 表示类型 来声明互操作类型,因为你可能正在与没有 Dart 提供的互操作类型的 JS 对象进行交互。

互操作类型通常还应该 实现 它们的表示类型,以便它们可以在需要表示类型的任何地方使用,例如 package:web 中的许多 API 中。

互操作成员

#

external 互操作成员为 JS 成员提供了惯用的语法。它们允许你为其参数和返回值编写 Dart 类型签名。可以在这些成员的签名中编写的类型有 限制。互操作成员对应的 JS API 由其声明位置、名称、它是什么类型的 Dart 成员以及任何 重命名 的组合来决定。

顶层互操作成员

#

假设有以下 JS 成员

js
globalThis.name = 'global';
globalThis.isNameEmpty = function() {
  return globalThis.name.length == 0;
}

你可以为它们编写互操作成员,如下所示

dart
@JS()
external String get name;

@JS()
external set name(String value);

@JS()
external bool isNameEmpty();

这里存在一个在全局范围内公开的属性 name 和一个函数 isNameEmpty。要访问它们,可以使用顶层互操作成员。要获取和设置 name,请声明并使用具有相同名称的互操作 getter 和 setter。要使用 isNameEmpty,请声明并调用具有相同名称的互操作函数。你可以声明顶层互操作 getter、setter、方法和字段。互操作字段等同于 getter 和 setter 对。

顶层互操作成员必须用 @JS() 注释进行声明,以将其与其他 external 顶层成员区分开来,例如可以使用 dart:ffi 编写的那些成员。

互操作类型成员

#

假设有一个如下所示的 JS 接口

js
class Time {
  constructor(hours, minutes) {
    this._hours = Math.abs(hours) % 24;
    this._minutes = arguments.length == 1 ? 0 : Math.abs(minutes) % 60;
  }

  static dinnerTime = new Time(18, 0);

  static getTimeDifference(t1, t2) {
    return new Time(t1.hours - t2.hours, t1.minutes - t2.minutes);
  }

  get hours() {
    return this._hours;
  }

  set hours(value) {
    this._hours = Math.abs(value) % 24;
  }

  get minutes() {
    return this._minutes;
  }

  set minutes(value) {
    this._minutes = Math.abs(value) % 60;
  }

  isDinnerTime() {
    return this.hours == Time.dinnerTime.hours && this.minutes == Time.dinnerTime.minutes;
  }
}
// Need to expose the type to the global scope.
globalThis.Time = Time;

你可以为其编写一个互操作接口,如下所示

dart
extension type Time._(JSObject _) implements JSObject {
  external Time(int hours, int minutes);
  external factory Time.onlyHours(int hours);

  external static Time dinnerTime;
  external static Time getTimeDifference(Time t1, Time t2);

  external int hours;
  external int minutes;
  external bool isDinnerTime();

  bool isMidnight() => hours == 0 && minutes == 0;
}

在互操作类型中,你可以声明几种不同类型的 external 互操作成员

  • 构造函数。调用时,只有位置参数的构造函数将创建一个新的 JS 对象,该对象使用 new 定义了由扩展类型名称决定的构造函数。例如,在 Dart 中调用 Time(0, 0) 将生成一个看起来像 new Time(0, 0) 的 JS 调用。类似地,调用 Time.onlyHours(0) 将生成一个看起来像 new Time(0) 的 JS 调用。请注意,两个构造函数的 JS 调用遵循相同的语义,无论它们是给定 Dart 名称还是工厂。

    • 对象文字构造函数。有时创建只包含一些属性及其值的 JS 对象文字 很有用。为了做到这一点,你声明一个只有命名参数的构造函数,其中参数的名称将是属性名称

      dart
      extension type Options._(JSObject o) implements JSObject {
        external Options({int a, int b});
        external int get a;
        external int get b;
      }

      Options(a: 0, b: 1) 的调用将导致创建 JS 对象 {a: 0, b: 1}。该对象由调用参数定义,因此调用 Options(a: 0) 将导致 {a: 0}。你可以通过 external 实例成员获取或设置对象的属性。

  • static 成员。与构造函数一样,这些成员使用扩展类型名称来生成 JS 代码。例如,调用 Time.getTimeDifference(t1, t2) 将生成一个看起来像 Time.getTimeDifference(t1, t2) 的 JS 调用。类似地,调用 Time.dinnerTime 将导致一个看起来像 Time.dinnerTime 的 JS 调用。与顶层一样,你可以声明 static 方法、getter、setter 和字段。

  • 实例成员。与其他 Dart 类型一样,这些成员需要一个实例才能使用。这些成员获取、设置或调用实例上的属性。例如

    dart
      final time = Time(0, 0);
      print(time.isDinnerTime()); // false
      final dinnerTime = Time.dinnerTime;
      time.hours = dinnerTime.hours;
      time.minutes = dinnerTime.minutes;
      print(time.isDinnerTime()); // true

    调用 dinnerTime.hours 获取 dinnerTimehours 属性值。类似地,调用 time.minutes= 设置 timeminutes 属性值。调用 time.isDinnerTime() 会调用 timeisDinnerTime 属性中的函数并返回其值。与顶层成员和 static 成员一样,您也可以声明实例方法、getter、setter 和字段。

  • 运算符。在互操作类型中,只允许两个 external 互操作运算符:[][]=。这些是与 JS 的 属性访问器 语义匹配的实例成员。例如,您可以像这样声明它们

    dart
    extension type Array(JSArray<JSNumber> _) implements JSArray<JSNumber> {
      external JSNumber operator [](int index);
      external void operator []=(int index, JSNumber value);
    }

    调用 array[i] 获取 array 中第 i 个插槽的值,而 array[i] = i.toJS 将该插槽的值设置为 i.toJS。其他 JS 运算符通过 dart:js_interop 中的 实用函数 公开。

最后,与任何其他扩展类型一样,您可以在互操作类型中声明任何 external 成员isMidnight 就是一个这样的例子。

对互操作类型的扩展成员

#

您也可以在互操作类型的 扩展 中编写 external 成员。例如

dart
extension on Array {
  external int push(JSAny? any);
}

调用 push 的语义与它在 Array 定义中的语义完全相同。扩展可以具有 external 实例成员和运算符,但不能具有 external static 成员或构造函数。与互操作类型一样,您可以在扩展中编写任何非 external 成员。当互操作类型没有公开您需要的 external 成员,并且您不想创建新的互操作类型时,这些扩展非常有用。

参数

#

external 互操作方法只能包含位置参数和可选参数。这是因为 JS 成员只接受位置参数。唯一的例外是对象字面量构造函数,它们只能包含命名参数。

与非 external 方法不同,可选参数不会被它们的默认值替换,而是会被省略。例如

dart
external int push(JSAny? any, [JSAny? any2]);

在 Dart 中调用 array.push(0.toJS) 将导致 JS 调用 array.push(0.toJS),而不是 array.push(0.toJS, null)。这允许用户不必为同一个 JS API 编写多个互操作成员来避免传递 null。如果您声明一个带有显式默认值的参数,您将收到警告,说明该值将被忽略。

@JS()

#

有时使用与写入的名称不同的名称引用 JS 属性很有用。例如,如果您想编写两个指向同一个 JS 属性的 external API,则需要为其中至少一个 API 编写不同的名称。类似地,如果您想定义多个引用同一个 JS 接口的互操作类型,则需要重命名其中至少一个。另一个例子是 JS 名称无法在 Dart 中编写,例如 $a

为了做到这一点,您可以使用带有常量字符串值的 @JS() 注解。例如

dart
extension type Array._(JSArray<JSAny?> _) implements JSArray<JSAny?> {
  external int push(JSNumber number);
  @JS('push')
  external int pushString(JSString string);
}

调用 pushpushString 将导致使用 push 的 JS 代码。

您也可以重命名互操作类型

dart
@JS('Date')
extension type JSDate._(JSObject _) implements JSObject {
  external JSDate();

  external static int now();
}

调用 JSDate() 将导致 JS 调用 new Date()。类似地,调用 JSDate.now() 将导致 JS 调用 Date.now()

此外,您可以对整个库进行命名空间,这将在这些类型中的所有互操作顶层成员、互操作类型和 static 互操作成员之前添加前缀。如果您想避免向全局 JS 范围添加太多成员,这将非常有用。

dart
@JS('library1')
library;

import 'dart:js_interop';

@JS()
external void method();

extension type JSType._(JSObject _) implements JSObject {
  external JSType();

  external static int get staticMember;
}

调用 method() 将导致 JS 调用 library1.method(),调用 JSType() 将导致 JS 调用 new library1.JSType(),调用 JSType.staticMember 将导致 JS 调用 library1.JSType.staticMember

与互操作成员和互操作类型不同,Dart 只有在您在库的 @JS() 注解中提供非空值时才会在 JS 调用中添加库名称。它不使用库的 Dart 名称作为默认值。

dart
library interop_library;

import 'dart:js_interop';

@JS()
external void method();

调用 method() 将导致 JS 调用 method(),而不是 interop_library.method()

您也可以为库、顶层成员和互操作类型编写用 . 分隔的多个命名空间

dart
@JS('library1.library2')
library;

import 'dart:js_interop';

@JS('library3.method')
external void method();

@JS('library3.JSType')
extension type JSType._(JSObject _) implements JSObject {
  external JSType();
}

调用 method() 将导致 JS 调用 library1.library2.library3.method(),调用 JSType() 将导致 JS 调用 new library1.library2.library3.JSType(),等等。

但是,您不能在互操作类型成员或互操作类型的扩展成员的 @JS() 注解中使用带 . 的值。

如果未向 @JS() 提供值或该值为空,则不会发生任何重命名。

@JS() 还告诉编译器成员或类型旨在被视为 JS 互操作成员或类型。对于所有顶层成员,它都是必需的(带或不带值),以将它们与其他 external 顶层成员区分开来,但在互操作类型内部以及扩展成员上通常可以省略,因为编译器可以从表示类型和类型上判断它是 JS 互操作类型。

将 Dart 函数和对象导出到 JS

#

以上部分展示了如何从 Dart 调用 JS 成员。导出 Dart 代码以便它可以在 JS 中使用也很有用。要将 Dart 函数导出到 JS,首先使用 Function.toJS 转换它,该函数将 Dart 函数包装在 JS 函数中。然后,通过互操作成员将包装后的函数传递给 JS。此时,其他 JS 代码就可以调用它了。

例如,这段代码转换了一个 Dart 函数并使用互操作将其设置在一个全局属性中,然后在 JS 中调用该属性

dart
import 'dart:js_interop';

@JS()
external set exportedFunction(JSFunction value);

void printString(JSString string) {
  print(string.toDart);
}

void main() {
  exportedFunction = printString.toJS;
}
js
globalThis.exportedFunction('hello world');

以这种方式导出的函数具有与互操作成员相似的 限制

有时将整个 Dart 接口导出以便 JS 可以与 Dart 对象交互非常有用。为此,使用 @JSExport 将 Dart 类标记为可导出,并使用 createJSInteropWrapper 包装该类的实例。有关此技术的更详细说明,包括如何模拟 JS 值,请参见 模拟教程

dart:js_interopdart:js_interop_unsafe

#

dart:js_interop 包含所有必要的成员,包括 @JS、JS 类型、转换函数和各种实用函数。实用函数包括

  • globalContext,它代表编译器用来查找互操作成员和类型的全局范围。
  • 检查 JS 值类型的帮助程序
  • JS 运算符
  • dartifyjsify,它们检查某些 JS 值的类型并将它们转换为 Dart 值,反之亦然。如果您知道 JS 值的类型,请优先使用特定转换,因为额外的类型检查可能会很昂贵。
  • importModule,它允许您动态地将模块导入为 JSObject

将来可能会在此库中添加更多实用程序。

dart:js_interop_unsafe 包含允许您动态查找属性的成员。例如

dart
JSFunction f = console['log'];

我们没有声明名为 log 的互操作成员,而是使用字符串来表示该属性。dart:js_interop_unsafe 提供了动态获取、设置和调用属性的功能。