目录

用法

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

通常,您可以通过在全局 JS 作用域中的某个位置使其可用,来访问 JavaScript API。要调用并从该 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

与互操作成员和互操作类型不同,只有当您在库的 @JS() 注释中提供非空值时,Dart 才会在 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 将其转换,这将使用 JS 函数包装 Dart 函数。然后,通过互操作成员将包装的函数传递给 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 提供了动态获取、设置和调用属性的功能。