跳到主要内容

用法

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

通常,您通过在全局 JS 作用域中的某个位置使其可用,来访问 JavaScript API。要从此 API 调用和接收 JS 值,您可以使用external interop 成员。要为 JS 值构造和提供类型,您可以使用和声明interop 类型,其中也包含 interop 成员。要将 Dart 值(如 ListFunction)传递给 interop 成员或从 JS 值转换为 Dart 值,您可以使用转换函数,除非 interop 成员包含原始类型

Interop 类型

#

当与 JS 值交互时,您需要为其提供 Dart 类型。您可以通过使用或声明 interop 类型来做到这一点。Interop 类型要么是由 Dart 提供的“JS 类型”,要么是包装 interop 类型的扩展类型

Interop 类型允许您为 JS 值提供接口,并允许您为其成员声明 interop API。它们也用于其他 interop API 的签名中。

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

Window 是任意 JSObject 的 interop 类型。不能运行时保证 Window 实际上是 JS Window。对于为同一值定义的任何其他 interop 接口也没有冲突。如果您想检查 Window 实际上是否是 JS Window,您可以通过 interop 检查 JS 值的类型

您还可以通过包装 Dart 提供的 JS 类型来声明自己的 interop 类型

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

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

Interop 类型通常也应该实现它们的表示类型,以便它们可以在期望表示类型的地方使用,例如在 package:web 提供的许多 API 中。

Interop 成员

#

external interop 成员为 JS 成员提供了符合语言习惯的语法。它们允许您为其参数和返回值编写 Dart 类型签名。可以在这些成员的签名中编写的类型具有限制。interop 成员对应的 JS API 由其声明位置、名称、Dart 成员的类型以及任何重命名的组合确定。

顶层 Interop 成员

#

给定以下 JS 成员

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

您可以像这样为它们编写 interop 成员

dart
@JS()
external String get name;

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

@JS()
external bool isNameEmpty();

在这里,全局作用域中存在属性 name 和函数 isNameEmpty。要访问它们,您可以使用顶层 interop 成员。要获取和设置 name,请声明并使用具有相同名称的 interop getter 和 setter。要使用 isNameEmpty,请声明并调用具有相同名称的 interop 函数。您可以声明顶层 interop getter、setter、方法和字段。Interop 字段等效于 getter 和 setter 对。

顶层 interop 成员必须使用 @JS() 注解声明,以将它们与其他 external 顶层成员区分开来,例如可以使用 dart:ffi 编写的成员。

Interop 类型成员

#

给定如下所示的 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;

您可以像这样为其编写 interop 接口

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;
}

在 interop 类型中,您可以声明几种不同类型的 external interop 成员

  • 构造函数。当调用时,仅具有位置参数的构造函数会创建一个新的 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 和字段。

  • 运算符。在 interop 类型中只允许使用两个 external interop 运算符:[][]=。这些是实例成员,它们匹配 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 中的实用程序函数公开。

最后,与任何其他扩展类型一样,您可以在 interop 类型中声明任何external 成员。使用 interop 值的布尔 getter isMidnight 就是这样一个例子。

Interop 类型上的扩展成员

#

您还可以在 interop 类型的扩展中编写 external 成员。例如

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

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

参数

#

external interop 方法只能包含位置参数和可选参数。这是因为 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 编写多个 interop 成员,以避免传入 null。如果您声明一个带有显式默认值的参数,您会收到一个警告,提示该值将被忽略。

@JS()

#

有时,以与书写名称不同的名称引用 JS 属性很有用。例如,如果您想编写两个指向同一 JS 属性的 external API,则需要为其中至少一个编写不同的名称。同样,如果您想定义引用同一 JS 接口的多个 interop 类型,则需要重命名其中至少一个。另一个例子是 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 代码。

您还可以重命名 interop 类型

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

  external static int now();
}

调用 JSDate() 会导致 JS 调用 new Date()。同样,调用 JSDate.now() 会导致 JS 调用 Date.now()

此外,您可以命名整个库空间,为这些类型中的所有 interop 顶层成员、interop 类型和 static interop 成员添加前缀。如果您想避免向全局 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

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

dart
library interop_library;

import 'dart:js_interop';

@JS()
external void method();

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

您还可以为库、顶层成员和 interop 类型编写以 . 分隔的多个命名空间

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(),依此类推。

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

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

@JS() 还告诉编译器,成员或类型旨在被视为 JS interop 成员或类型。所有顶层成员都需要它(无论是否具有值),以将它们与其他 external 顶层成员区分开来,但通常可以在 interop 类型上和内部以及扩展成员上省略它,因为编译器可以从表示类型和 on-type 中判断它是 JS interop 类型。

将 Dart 函数和对象导出到 JS

#

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

例如,此代码转换了一个 Dart 函数,并使用 interop 将其设置在全局属性中,然后在 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');

以这种方式导出的函数具有类型限制,类似于 interop 成员的限制。

有时,导出整个 Dart 接口以便 JS 可以与 Dart 对象交互很有用。为此,请使用 @JSExport 将 Dart 类标记为可导出,并使用 createJSInteropWrapper 包装该类的实例。有关此技术的更详细解释,包括如何模拟 JS 值,请查看如何模拟 JavaScript 互操作对象

dart:js_interopdart:js_interop_unsafe

#

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

  • globalContext,它表示编译器用于查找 interop 成员和类型的全局作用域。
  • 用于检查 JS 值类型的助手
  • JS 运算符
  • dartifyjsify,它们检查某些 JS 值的类型,并将它们转换为 Dart 值,反之亦然。当您知道 JS 值的类型时,请首选使用特定的转换,因为额外的类型检查可能很昂贵。
  • importModule,它允许您将模块动态导入为 JSObject
  • isA,它允许您检查 JS interop 值是否是由类型参数指定的 JS 类型的实例。

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

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

dart
JSFunction f = console['log'];

可以使用字符串而不是声明名为 log 的 interop 成员来访问属性。dart:js_interop_unsafe 还提供了动态检查、获取、设置和调用属性的函数。