跳到主要内容

用法

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= 设置 time 的 minutes 属性值。调用 time.isDinnerTime() 调用 time 的 isDinnerTime 属性中的函数并返回值。与顶层成员和 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] 获取 arrayi 个槽位中的值,而 array[i] = i.toJS 将该槽位中的值设置为 i.toJS。其他 JS 运算符由 dart:js_interop 中的实用函数公开。

最后,与任何其他扩展类型一样,你可以在互操作类型中声明任何external 成员。使用互操作值的布尔 getter 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,你需要至少为其中一个编写不同的名称。类似地,如果你想定义多个引用同一 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 顶层成员,但由于编译器可以从表示类型和 on-type 判断出它是 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 值,请查看如何模拟 JavaScript 互操作对象

dart:js_interop 和 dart:js_interop_unsafe

#

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

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

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

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

dart
JSFunction f = console['log'];

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